feat(server): 配置接口接入 licence 验签
This commit is contained in:
@@ -1,9 +1,18 @@
|
||||
import { oc } from '@orpc/contract'
|
||||
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
|
||||
.object({
|
||||
licence: z.string().nullable().describe('当前本地 licence,未设置时为 null'),
|
||||
licence: licenceOutput.nullable().describe('当前本地已验证 licence 的元数据,未设置时为 null'),
|
||||
fingerprint: z.string().describe('UX 本机计算得到的设备特征码(SHA-256)'),
|
||||
hasPlatformPublicKey: z.boolean().describe('是否已配置平台公钥'),
|
||||
hasPgpPrivateKey: z.boolean().describe('是否已配置 OpenPGP 私钥'),
|
||||
@@ -12,7 +21,11 @@ const configOutput = z
|
||||
.meta({
|
||||
examples: [
|
||||
{
|
||||
licence: 'LIC-8F2A-XXXX',
|
||||
licence: {
|
||||
licenceId: 'LIC-20260319-0025',
|
||||
expireTime: '2027-03-19',
|
||||
isExpired: false,
|
||||
},
|
||||
fingerprint: '9a3b7c1d2e4f5a6b8c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b',
|
||||
hasPlatformPublicKey: true,
|
||||
hasPgpPrivateKey: true,
|
||||
@@ -33,7 +46,7 @@ export const get = oc
|
||||
operationId: 'configGet',
|
||||
summary: '读取本机身份配置',
|
||||
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'],
|
||||
})
|
||||
.input(z.object({}).describe('空请求体,仅触发读取当前配置'))
|
||||
@@ -46,16 +59,18 @@ export const setLicence = oc
|
||||
operationId: 'configSetLicence',
|
||||
summary: '写入本地 licence',
|
||||
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'],
|
||||
})
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
licence: z.string().min(1).describe('本地持久化的 licence'),
|
||||
})
|
||||
.meta({
|
||||
examples: [{ licence: 'LIC-8F2A-XXXX' }],
|
||||
licenceEnvelopeSchema.meta({
|
||||
examples: [
|
||||
{
|
||||
payload: 'eyJsaWNlbmNlX2lkIjoiTElDLTIwMjYwMzE5LTAwMjUiLCJleHBpcmVfdGltZSI6IjIwMjctMDMtMTkifQ==',
|
||||
signature:
|
||||
'aLd+wwpz1W5AS0jgE/IstSNjCAQ5estQYIMqeLXRWMIsnKxjZpCvC8O5q/G5LEBBLJXnbTk8N6IMTUx295nf2HQYlXNtJkWiBeUXQ6/uzs0RbhCeRAWK2Hx4kSsmiEv4AHGLb4ozI2XekTc+40+ApJQYqaWbDu/NU99TmDm3/da1VkKpQxH60BhSQVwBtU67w9Vp3SpWm8y1faQ7ci5WDtJf1JZaS70kPXoGeA5018rPeMFlEzUp10yDlGW6RcrT7Dm+r7zFyrFznLK+evBEvTf9mMGWwZZP3q9vJtC/wFt1t5zNHdkb27cTwc9yyqGMWdelXQAQDnoisn2Jzi06KA==',
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
.output(configOutput)
|
||||
@@ -92,7 +107,7 @@ export const setPlatformPublicKey = oc
|
||||
operationId: 'configSetPlatformPublicKey',
|
||||
summary: '写入本地平台公钥',
|
||||
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'],
|
||||
})
|
||||
.input(
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { validatePgpPrivateKey } from '@furtherverse/crypto'
|
||||
import { validatePgpPrivateKey, validateRsaPublicKey } from '@furtherverse/crypto'
|
||||
import { ORPCError } from '@orpc/server'
|
||||
import { isLicenceExpired, verifyAndDecodeLicenceEnvelope } from '@/server/licence'
|
||||
import { ensureUxConfig, setUxLicence, setUxPgpPrivateKey, setUxPlatformPublicKey } from '@/server/ux-config'
|
||||
import { db } from '../middlewares'
|
||||
import { os } from '../server'
|
||||
|
||||
const toConfigOutput = (config: {
|
||||
licence: string | null
|
||||
licenceId: string | null
|
||||
licenceExpireTime: string | null
|
||||
fingerprint: string
|
||||
platformPublicKey: 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,
|
||||
hasPlatformPublicKey: config.platformPublicKey != 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 }) => {
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -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 }) => {
|
||||
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)
|
||||
return toConfigOutput(config)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user