feat(server): 配置接口接入 licence 验签
This commit is contained in:
@@ -1,9 +1,18 @@
|
|||||||
import { oc } from '@orpc/contract'
|
import { oc } from '@orpc/contract'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { licenceEnvelopeSchema } from '@/server/licence'
|
||||||
|
|
||||||
|
const licenceOutput = z
|
||||||
|
.object({
|
||||||
|
licenceId: z.string().describe('验签通过后的 licence 标识'),
|
||||||
|
expireTime: z.string().describe('授权到期日,格式为 YYYY-MM-DD'),
|
||||||
|
isExpired: z.boolean().describe('当前 licence 是否已过期(按 UTC 自然日计算)'),
|
||||||
|
})
|
||||||
|
.describe('当前已安装 licence 的验证后元数据')
|
||||||
|
|
||||||
const configOutput = z
|
const configOutput = z
|
||||||
.object({
|
.object({
|
||||||
licence: z.string().nullable().describe('当前本地 licence,未设置时为 null'),
|
licence: licenceOutput.nullable().describe('当前本地已验证 licence 的元数据,未设置时为 null'),
|
||||||
fingerprint: z.string().describe('UX 本机计算得到的设备特征码(SHA-256)'),
|
fingerprint: z.string().describe('UX 本机计算得到的设备特征码(SHA-256)'),
|
||||||
hasPlatformPublicKey: z.boolean().describe('是否已配置平台公钥'),
|
hasPlatformPublicKey: z.boolean().describe('是否已配置平台公钥'),
|
||||||
hasPgpPrivateKey: z.boolean().describe('是否已配置 OpenPGP 私钥'),
|
hasPgpPrivateKey: z.boolean().describe('是否已配置 OpenPGP 私钥'),
|
||||||
@@ -12,7 +21,11 @@ const configOutput = z
|
|||||||
.meta({
|
.meta({
|
||||||
examples: [
|
examples: [
|
||||||
{
|
{
|
||||||
licence: 'LIC-8F2A-XXXX',
|
licence: {
|
||||||
|
licenceId: 'LIC-20260319-0025',
|
||||||
|
expireTime: '2027-03-19',
|
||||||
|
isExpired: false,
|
||||||
|
},
|
||||||
fingerprint: '9a3b7c1d2e4f5a6b8c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b',
|
fingerprint: '9a3b7c1d2e4f5a6b8c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b',
|
||||||
hasPlatformPublicKey: true,
|
hasPlatformPublicKey: true,
|
||||||
hasPgpPrivateKey: true,
|
hasPgpPrivateKey: true,
|
||||||
@@ -33,7 +46,7 @@ export const get = oc
|
|||||||
operationId: 'configGet',
|
operationId: 'configGet',
|
||||||
summary: '读取本机身份配置',
|
summary: '读取本机身份配置',
|
||||||
description:
|
description:
|
||||||
'查询 UX 当前本地身份配置状态。\n\n典型用途:页面初始化时检测授权状态、加密前检查平台公钥、签名前检查私钥是否就绪。\n\n返回内容:\n- licence:当前持久化授权码,未设置时为 null;\n- fingerprint:设备特征码(本机自动计算);\n- hasPlatformPublicKey:是否已写入平台公钥;\n- hasPgpPrivateKey:是否已写入 OpenPGP 私钥。',
|
'查询 UX 当前本地身份配置状态。\n\n典型用途:页面初始化时检测授权状态、验签前检查平台公钥、签名前检查私钥是否就绪。\n\n返回内容:\n- licence:当前已验证 licence 的元数据,未设置时为 null;\n- fingerprint:设备特征码(本机自动计算);\n- hasPlatformPublicKey:是否已写入平台公钥;\n- hasPgpPrivateKey:是否已写入 OpenPGP 私钥。',
|
||||||
tags: ['Config'],
|
tags: ['Config'],
|
||||||
})
|
})
|
||||||
.input(z.object({}).describe('空请求体,仅触发读取当前配置'))
|
.input(z.object({}).describe('空请求体,仅触发读取当前配置'))
|
||||||
@@ -46,16 +59,18 @@ export const setLicence = oc
|
|||||||
operationId: 'configSetLicence',
|
operationId: 'configSetLicence',
|
||||||
summary: '写入本地 licence',
|
summary: '写入本地 licence',
|
||||||
description:
|
description:
|
||||||
'写入或更新本机持久化 licence。\n\n调用时机:设备首次激活、授权码变更、授权修复。\n\n约束与行为:\n- 仅接收 licence 文本;\n- fingerprint 由本机自动计算,不允许外部覆盖;\n- 成功后返回最新配置快照,便于前端立即刷新授权状态。',
|
'写入或更新本机持久化 licence。\n\n调用时机:设备首次激活、授权码变更、授权修复。\n\n约束与行为:\n- 接收 `.lic` 文件内容对应的 JSON 信封,而不是文件上传;\n- 使用已配置的平台公钥对 payload 原始字符串做 SHA256withRSA 验签;\n- 仅在验签通过且 expire_time 未过期时持久化;\n- fingerprint 由本机自动计算,不允许外部覆盖;\n- 成功后返回最新配置快照,便于前端立即刷新授权状态。',
|
||||||
tags: ['Config'],
|
tags: ['Config'],
|
||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
z
|
licenceEnvelopeSchema.meta({
|
||||||
.object({
|
examples: [
|
||||||
licence: z.string().min(1).describe('本地持久化的 licence'),
|
{
|
||||||
})
|
payload: 'eyJsaWNlbmNlX2lkIjoiTElDLTIwMjYwMzE5LTAwMjUiLCJleHBpcmVfdGltZSI6IjIwMjctMDMtMTkifQ==',
|
||||||
.meta({
|
signature:
|
||||||
examples: [{ licence: 'LIC-8F2A-XXXX' }],
|
'aLd+wwpz1W5AS0jgE/IstSNjCAQ5estQYIMqeLXRWMIsnKxjZpCvC8O5q/G5LEBBLJXnbTk8N6IMTUx295nf2HQYlXNtJkWiBeUXQ6/uzs0RbhCeRAWK2Hx4kSsmiEv4AHGLb4ozI2XekTc+40+ApJQYqaWbDu/NU99TmDm3/da1VkKpQxH60BhSQVwBtU67w9Vp3SpWm8y1faQ7ci5WDtJf1JZaS70kPXoGeA5018rPeMFlEzUp10yDlGW6RcrT7Dm+r7zFyrFznLK+evBEvTf9mMGWwZZP3q9vJtC/wFt1t5zNHdkb27cTwc9yyqGMWdelXQAQDnoisn2Jzi06KA==',
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(configOutput)
|
.output(configOutput)
|
||||||
@@ -92,7 +107,7 @@ export const setPlatformPublicKey = oc
|
|||||||
operationId: 'configSetPlatformPublicKey',
|
operationId: 'configSetPlatformPublicKey',
|
||||||
summary: '写入本地平台公钥',
|
summary: '写入本地平台公钥',
|
||||||
description:
|
description:
|
||||||
'写入或更新本机持久化平台公钥(Base64 编码 SPKI DER)。\n\n调用时机:设备授权初始化、平台公钥轮换。\n\n约束与行为:\n- 仅接收平台 RSA 公钥文本;\n- 公钥保存在本地,设备授权密文接口会自动读取,无需每次传参;\n- 成功后返回最新配置快照,可用于确认 hasPlatformPublicKey 状态。',
|
'写入或更新本机持久化平台公钥(Base64 编码 SPKI DER)。\n\n调用时机:设备授权初始化、平台公钥轮换。\n\n约束与行为:\n- 仅接收可解析的平台 RSA 公钥文本;\n- 公钥保存在本地,设备授权密文接口和 licence 验签都会自动读取,无需每次传参;\n- 若平台公钥发生变化,已安装 licence 会被清空,需要重新安装已签名 licence;\n- 成功后返回最新配置快照,可用于确认 hasPlatformPublicKey 状态。',
|
||||||
tags: ['Config'],
|
tags: ['Config'],
|
||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import { validatePgpPrivateKey } from '@furtherverse/crypto'
|
import { validatePgpPrivateKey, validateRsaPublicKey } from '@furtherverse/crypto'
|
||||||
import { ORPCError } from '@orpc/server'
|
import { ORPCError } from '@orpc/server'
|
||||||
|
import { isLicenceExpired, verifyAndDecodeLicenceEnvelope } from '@/server/licence'
|
||||||
import { ensureUxConfig, setUxLicence, setUxPgpPrivateKey, setUxPlatformPublicKey } from '@/server/ux-config'
|
import { ensureUxConfig, setUxLicence, setUxPgpPrivateKey, setUxPlatformPublicKey } from '@/server/ux-config'
|
||||||
import { db } from '../middlewares'
|
import { db } from '../middlewares'
|
||||||
import { os } from '../server'
|
import { os } from '../server'
|
||||||
|
|
||||||
const toConfigOutput = (config: {
|
const toConfigOutput = (config: {
|
||||||
licence: string | null
|
licenceId: string | null
|
||||||
|
licenceExpireTime: string | null
|
||||||
fingerprint: string
|
fingerprint: string
|
||||||
platformPublicKey: string | null
|
platformPublicKey: string | null
|
||||||
pgpPrivateKey: string | null
|
pgpPrivateKey: string | null
|
||||||
}) => ({
|
}) => ({
|
||||||
licence: config.licence,
|
licence:
|
||||||
|
config.licenceId && config.licenceExpireTime
|
||||||
|
? {
|
||||||
|
licenceId: config.licenceId,
|
||||||
|
expireTime: config.licenceExpireTime,
|
||||||
|
isExpired: isLicenceExpired(config.licenceExpireTime),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
fingerprint: config.fingerprint,
|
fingerprint: config.fingerprint,
|
||||||
hasPlatformPublicKey: config.platformPublicKey != null,
|
hasPlatformPublicKey: config.platformPublicKey != null,
|
||||||
hasPgpPrivateKey: config.pgpPrivateKey != null,
|
hasPgpPrivateKey: config.pgpPrivateKey != null,
|
||||||
@@ -22,7 +31,28 @@ export const get = os.config.get.use(db).handler(async ({ context }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const setLicence = os.config.setLicence.use(db).handler(async ({ context, input }) => {
|
export const setLicence = os.config.setLicence.use(db).handler(async ({ context, input }) => {
|
||||||
const config = await setUxLicence(context.db, input.licence)
|
const currentConfig = await ensureUxConfig(context.db)
|
||||||
|
|
||||||
|
if (!currentConfig.platformPublicKey) {
|
||||||
|
throw new ORPCError('PRECONDITION_FAILED', {
|
||||||
|
message: 'Platform public key is not configured. Call config.setPlatformPublicKey first.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = verifyAndDecodeLicenceEnvelope(input, currentConfig.platformPublicKey)
|
||||||
|
if (isLicenceExpired(payload.expire_time)) {
|
||||||
|
throw new ORPCError('BAD_REQUEST', {
|
||||||
|
message: 'licence has expired',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await setUxLicence(context.db, {
|
||||||
|
payload: input.payload,
|
||||||
|
signature: input.signature,
|
||||||
|
licenceId: payload.licence_id,
|
||||||
|
expireTime: payload.expire_time,
|
||||||
|
})
|
||||||
|
|
||||||
return toConfigOutput(config)
|
return toConfigOutput(config)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -38,6 +68,14 @@ export const setPgpPrivateKey = os.config.setPgpPrivateKey.use(db).handler(async
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const setPlatformPublicKey = os.config.setPlatformPublicKey.use(db).handler(async ({ context, input }) => {
|
export const setPlatformPublicKey = os.config.setPlatformPublicKey.use(db).handler(async ({ context, input }) => {
|
||||||
|
try {
|
||||||
|
validateRsaPublicKey(input.platformPublicKey)
|
||||||
|
} catch (error) {
|
||||||
|
throw new ORPCError('BAD_REQUEST', {
|
||||||
|
message: `Invalid platform public key: ${error instanceof Error ? error.message : 'unable to parse'}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const config = await setUxPlatformPublicKey(context.db, input.platformPublicKey)
|
const config = await setUxPlatformPublicKey(context.db, input.platformPublicKey)
|
||||||
return toConfigOutput(config)
|
return toConfigOutput(config)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user