通用授权INTEGRATION DOCS

PROTOCOL V2 · INTEGRATION GUIDE

通用客户端授权
接入文档

面向 Windows、macOS、Linux 和 Electron 客户端的完整实施指南。涵盖信任建立、加密协议、授权生命周期、错误处理与上线验收。

V2 PROTOCOLP-256ES256JWE + JWSSELF-HOSTED

01 / QUICK START

新客户端快速接入

下面是一次真实接入从安装到持续使用的完整闭环。先把客户端能力准备好,再让公开密钥、挑战、激活和凭证校验按固定顺序发生;私钥与完整授权码始终不离开受保护的本机环境。

客户端先集成这四项

01
发布配置

将服务地址、appId、协议版本与核对过的服务 ES256 签名公钥固定进正式构建。

02
设备密钥库

签名私钥只用于 ES256 请求证明;加密私钥只用于 ECDH 解密发给本机的 JWE,均存入系统凭据库。

03
协议客户端

负责生成随机数、导出公开 JWK、构造 JWE 密文及对精确方法与路径签发 ES256 proof。

04
授权状态门

集中保存已验签凭证、离线截止时间与功能开关;界面和功能都只读取这一份状态。

配置示例要求
服务地址https://license.example.com生产环境只允许 HTTPS
应用编号my-desktop-app小写字母、数字和连字符,发布后稳定
协议版本2拒绝未知版本
服务签名公钥P-256 Public JWK构建时固定,运行时不得静默替换

从首次运行到换机:谁做什么

  1. 01
    客户端:固定信任并检查连通性

    调用 GET /health 确认服务存活;读取 GET /v2/keys 后只与发布时固定的服务签名公钥人工核对,不能静默覆盖信任锚。

  2. 02
    客户端:生成两把设备密钥,上传公开 JWK

    首次安装生成一把 ES256 签名密钥和一把 ECDH 加密密钥;保留私钥在本机密钥库,向 POST /v2/activation/challenge 提交 appId、随机数和两份公开 JWK。

  3. 03
    客户端:验证挑战,再提交激活

    验证服务 ES256 签名、kid、appId、随机数与过期时间。随后把完整授权码放进 JWE 内层,对密文摘要、方法与精确路径生成设备 ES256 proof,调用 POST /v2/activation/complete

  4. 04
    客户端:验签保存,心跳或停用

    解密并验证授权凭证后,仅保存 deviceHandle、脱敏授权码、已验签凭证和离线截止时间。用 POST /v2/devices/heartbeat 刷新;换机前调用 POST /v2/devices/deactivate

客户端配置GET /healthGET /v2/keys两套 P-256 密钥challenge验证挑战complete验签并保存heartbeat / deactivate

Electron / Node.js 接入骨架

以下代码演示客户端应该准备什么。它刻意只给出密钥生成、公开数据和状态保存的骨架;JWE、JWS 的精确编码与验签必须按后面的“安全信封”章节实现,不能用简化实现替代。

01 · GENERATE TWO DEVICE KEY PAIRS
async function generateDeviceKeyPairs() {
  const signing = await crypto.subtle.generateKey(
    { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']
  );
  const encryption = await crypto.subtle.generateKey(
    { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits']
  );

  return {
    signingPublicJwk: await crypto.subtle.exportKey('jwk', signing.publicKey),
    encryptionPublicJwk: await crypto.subtle.exportKey('jwk', encryption.publicKey),
    privateKeys: { signing: signing.privateKey, encryption: encryption.privateKey },
  };
}
// privateKeys 仅交给 Windows DPAPI / Keychain / 系统密钥环保存。
02 · BUILD AND SEND CHALLENGE REQUEST
function buildChallengeRequest(keys) {
  const clientNonce = crypto.randomUUID();
  return {
    appId: APP_ID,
    appVersion: APP_VERSION,
    platform: process.platform,
    clientNonce,
    signingJwk: keys.signingPublicJwk,
    encryptionJwk: keys.encryptionPublicJwk,
  };
}

const challengeResponse = await fetch(`${LICENSE_ORIGIN}/v2/activation/challenge`, {
  method: 'POST', headers: { 'content-type': 'application/json' },
  body: JSON.stringify(buildChallengeRequest(keys)),
});
// 用固定的服务 ES256 公钥验证 challengeResponse 的 JWS、nonce、appId 与 expiresAt。
03 · ACTIVATE AND PERSIST MINIMUM STATE
const payload = await encryptLicenseAsJwe({
  licenseKey: userInputLicenseKey, challengeId, iat: now(), jti: crypto.randomUUID(),
}, keys.encryptionPublicJwk); // 按“安全信封”章节使用服务加密公钥实现

const proof = await signRequestProof({
  method: 'POST', path: '/v2/activation/complete', bodyHash: sha256(payload),
}, keys.privateKeys.signing); // 使用本机 ES256 签名私钥实现

const grant = await decryptAndVerifyGrant({ payload, proof });
await secureStore.save('license-state', {
  deviceHandle: grant.deviceHandle,
  maskedLicenseKey: mask(userInputLicenseKey),
  verifiedGrant: grant,
  offlineUntil: grant.offlineValidUntil,
});
// 不保存完整授权码;私钥也不进入普通 JSON、日志或渲染进程。
最短路径:固定服务签名公钥 → 生成并保存两套设备私钥 → 上传公开 JWK 取挑战 → 验证挑战 → JWE + ES256 proof 激活 → 验签并保存最小状态 → 定期心跳,换机前停用。

02 / REQUEST MAP

调用全景

方法与路径调用方用途保护
GET /health运维存活检查HTTPS
GET /v2/keys客户端 / CLI获取公开服务密钥固定签名公钥核对
POST /v2/activation/challenge客户端获取一次性签名挑战服务 ES256 签名
POST /v2/activation/complete客户端提交授权码并绑定设备JWE + 设备 JWS
POST /v2/devices/heartbeat客户端刷新权威授权状态JWE + 已绑定设备 JWS
POST /v2/devices/deactivate客户端释放当前设备名额JWE + 已绑定设备 JWS
POST /v2/admin/licenses/issue管理员 CLI签发新授权JWE + 管理员 JWS
POST /v2/admin/licenses/renew管理员 CLI续期、转计划、调设备数JWE + 管理员 JWS

API SANDBOX / SAFE MODE

接口调试台

用于理解接入顺序和返回结构。健康检查可对当前主机做一次真实只读检测;挑战、激活、心跳和停用全部在浏览器内模拟,模拟授权,不写入真实授权账本,也不会调用任何受保护的 V2 POST 接口。

安全边界

不要在此页输入真实授权码、设备私钥或管理员密钥。模拟产生的标识仅存在于当前页面,刷新即清除。

CONNECTION & LIFECYCLE当前主机自动带入
GET/health真实只读检测
POST/v2/activation/challenge浏览器内模拟
POST/v2/activation/complete浏览器内模拟
POST/v2/devices/heartbeat浏览器内模拟
POST/v2/devices/deactivate浏览器内模拟
REQUEST / RESPONSE等待模拟
{
  "mode": "SIMULATED",
  "message": "选择左侧接口开始调试"
}
调用日志不会请求真实授权服务
  1. 调试台已初始化;仅健康检查可执行真实只读请求。

03 / TRUST MODEL

密钥与信任模型

服务密钥

服务端使用独立的 P-256 签名密钥和加密密钥。客户端必须把核对后的签名公钥作为信任锚固定进发布配置;从网络获得的新签名公钥只能用于人工核对或受控轮换,不能直接覆盖已有信任锚。

设备密钥

每个安装实例首次运行生成两套不同密钥:签名私钥用于 ES256 请求证明,加密私钥用于解密只发给该设备的 JWE。两套公钥指纹必须不同。

  • Windows:优先 DPAPI 或 Credential Locker。
  • macOS:使用 Keychain。
  • Linux:使用 Secret Service 或系统密钥环。
  • 不得把完整授权码、设备私钥或解密载荷写入普通 JSON、日志或 Web Storage。
信任边界:能成功下载公钥不代表公钥可信。真正的信任来自客户端发布时固定并核对过的服务签名公钥。

04 / SECURE ENVELOPE

JWE + JWS 安全信封

敏感请求使用 JWE Compact 加密,算法为 ECDH-ES+A256KW + A256GCM;外层 proof 使用设备或管理员的 ES256 JWS,绑定请求方法、精确路径和密文摘要。

REQUEST PROOF PAYLOAD
{
  "method": "POST",
  "path": "/v2/devices/heartbeat",
  "bodyHash": "SHA-256(payload)-base64url"
}

每个加密内层请求还必须包含 Unix 秒级 iat 和唯一 jti。请求时间与服务端偏差不得超过五分钟;短时间重复的 jti 会被拒绝。

05 / ACTIVATION

首次激活

第一步:获取并验证挑战

用户点击激活后,先调用 POST /v2/activation/challenge,只提交应用信息、客户端随机数与两套设备公钥。验证返回 JWS 的签名、kid、appId、随机数、公钥指纹和五分钟有效期。

CHALLENGE REQUEST
{
  "appId": "my-desktop-app",
  "appVersion": "1.0.0",
  "platform": "win32",
  "clientNonce": "NEW_RANDOM_VALUE",
  "signingJwk": {"kty":"EC","crv":"P-256","x":"PUBLIC_X","y":"PUBLIC_Y"},
  "encryptionJwk": {"kty":"EC","crv":"P-256","x":"PUBLIC_X2","y":"PUBLIC_Y2"}
}

第二步:完成激活

挑战校验通过后调用 POST /v2/activation/complete。完整授权码仅放入 JWE 内层;proof 由本机设备签名私钥产生。成功后解密响应,并再次验证服务签发的 licenseGrant。

OUTER ENVELOPE
{
  "payload": "JWE_COMPACT_VALUE",
  "proof": "DEVICE_ES256_JWS"
}

本地只保存 deviceHandle、脱敏授权码、已验签授权凭证、offlineValidUntil、最近可信服务器时间及经过系统保护的设备密钥。完整授权码使用后不再保存。

06 / HEARTBEAT

心跳、刷新与离线窗口

使用已绑定设备密钥调用 POST /v2/devices/heartbeat。建议在应用启动、网络恢复、用户手动刷新以及正常运行期间调用;服务返回的建议间隔为六小时,客户端加入约 ±10% 抖动。

  • 成功:替换本地 licenseGrant 与 offlineValidUntil,再按新凭证刷新功能开关。
  • 网络失败、超时或 5xx:不是授权失效证据;签名凭证仍在离线窗口内时可以继续使用。
  • 权威失效错误:立即重置授权展示和所有受控功能。

离线窗口最长 72 小时且不超过订阅截止时间。永久计划同样需要周期心跳。客户端应同时记录可信服务器时间和单调时钟基准,防止通过系统时间回拨延长授权。

07 / DEACTIVATION

停用与设备迁移

用户确认停用本机时调用 POST /v2/devices/deactivate。只有收到服务端加密成功响应 {"deactivated":true} 后,客户端才清除设备状态和本机密钥。网络失败时保留状态并提示稍后重试。

服务端保留 90 天设备墓碑,使被停用设备再次联网时获得权威的 DEVICE_DEACTIVATED 结果,而不是被误判为普通断网。

08 / ADMIN OPERATIONS

授权签发与续期

自动化或服务器终端应使用独立管理员密钥调用加密 CLI 接口。普通客户端永远不调用这两个接口,也不携带管理员凭据。

签发:POST /v2/admin/licenses/issue

字段取值说明
planannual / monthly / perpetual授权计划
years正整数年计划时使用,默认 1
months正整数月计划时使用,默认 1
deviceLimit1–10允许激活设备数
customerRef字符串订单或客户备注,不下发客户端

续期:POST /v2/admin/licenses/renew

使用 targetPlan 选择月、年或永久,可同时调整 deviceLimit。若目标设备数小于当前激活数,返回 DEVICE_DEACTIVATION_REQUIRED,管理员先停用足够设备再重试。

完整新授权码只在签发成功的加密响应中出现一次。交付完成后,服务端账本只保存不可逆哈希。

09 / ERROR HANDLING

权威错误处理

错误含义客户端动作
LICENSE_NOT_FOUND授权不存在或已删除重置授权状态与受控功能,返回激活页
LICENSE_REVOKED授权被撤销立即停止受控功能
LICENSE_EXPIRED授权到期停止受控功能并提示续期
DEVICE_LIMIT_REACHED设备已满提示停用旧设备或联系管理员
DEVICE_DEACTIVATED本机被停用清除授权和设备身份
DEVICE_KEY_INVALID本机密钥不匹配清除损坏状态并重新激活
REPLAY_REJECTED请求被重复使用生成新 jti 并检查重试逻辑
HTTP 429触发频率限制遵循 Retry-After 并加入退避抖动
网络 / 超时 / 5xx无权威授权结论离线窗口内保留有效授权并重试
身份确认前的失败统一返回安全拒绝,避免向扫描者泄露授权、设备或签名校验的具体失败位置。

10 / CLIENT STRUCTURE

桌面客户端推荐结构

密码学和密钥访问放在 Electron 主进程或原生模块,渲染进程只读取最少的授权视图状态。

RESPONSIBILITY FLOW
Renderer UI
  └─ restricted IPC: activate / refresh / deactivate / status
       └─ Main Process LicenseService
            ├─ SecureStore: device keys + verified grant
            ├─ ProtocolClient: challenge + JWE/JWS transport
            ├─ GrantVerifier: pinned service public key
            └─ EntitlementGate: one authoritative feature state

所有页面显示和功能判断必须读取同一个 EntitlementGate。不能出现“弹窗显示未授权,但新建或修改功能仍然可用”的状态分裂。

11 / REVERSE ENGINEERING

客户端防逆向建议

本服务能显著提高复制授权和伪造服务响应的成本,但不能保证客户端绝对无法破解。本地程序始终运行在用户控制的设备上,正确目标是让一个补丁或一个泄露值不足以绕过完整授权链。

  • 固定服务端签名公钥,严格校验 kid、算法和曲线。
  • 设备密钥进入系统安全存储,渲染层永远拿不到原始私钥。
  • 正式包关闭开发者工具、源码映射、调试端口和详细协议日志。
  • 对关键模块做代码签名、资源哈希和运行时完整性检查。
  • 授权状态机可适度混淆,但不要把混淆当作密码学。
  • 高价值能力尽量由服务端做最终判断,客户端保留多处一致性校验。

12 / OPERATIONS

部署、备份与密钥轮换

生产部署使用 HTTPS 反向代理,只开放官网和 V2 协议所需端口。授权账本、审计日志、服务密钥与管理员公钥注册信息分层保存并限制文件权限。

密钥轮换顺序

  1. 发布同时信任旧、新服务签名公钥的过渡客户端。
  2. 确认过渡版本覆盖率后,切换服务签名密钥。
  3. 观察客户端心跳和错误率,最后淘汰旧公钥。

服务签名密钥是客户端信任锚,直接替换会使旧客户端拒绝新授权。备份必须包含授权账本、审计记录和密钥材料,并定期做恢复演练。

13 / ACCEPTANCE

上线验收清单

  • 正确固定公钥可激活,替换服务签名公钥后客户端拒绝响应。
  • 两套设备公钥不同,复制状态到另一台机器后无法完成签名心跳。
  • 授权不存在、撤销、过期或设备被停用时,展示与受控功能同步重置。
  • 达到设备上限不能继续激活;停用旧设备后新设备可以激活。
  • 心跳成功会原子替换授权凭证和离线截止时间。
  • 断网只在签名离线窗口内运行,时钟回拨不能延长期限。
  • 完整授权码和任何私钥都不出现在日志、崩溃报告或普通本地文件。
  • HTTP 429 使用退避;网络错误与服务端 5xx 不误判为撤销。
  • 正式安装包完成代码签名,关闭开发配置并通过完整性检查。
  • 生产备份可恢复,密钥轮换方案经过预演。