Code-level audit of the authorization chain: domain-separated machine hash with no raw identifiers in errors or storage, strict License v1 verification (base64 re-encode round-trip, exact 9-field shape, timestamp round-trip, machine-hash binding), and revocation state with clock-rollback defense and revoked-before-expiry ordering. Snapshots to the UI carry no license ID, signature, path or raw Windows identifiers; defaults fail closed. Catalog/License/Revocation share one canonical JSON implementation. No security defect found. Records two product decisions needed: the revocation list is a hard dependency for any authorization (missing or >38-day-old list blocks all launches, in tension with the offline licensing vision and lacking a distribution plan), and trial is a P0 requirement plus MVP acceptance criterion that is deliberately not implemented, leaving 02-requirements in conflict with the architecture and task docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
11 KiB
Phase 5 审查(T-501 ~ T-503:机器指纹 / 许可证验签 / 授权界面与撤销)
审查范围:
8d81276(T-501 机器指纹 machine_hash)、cd76f7f(T-502 Ed25519 离线许可证验证)、76f6108(T-503 授权界面、离线导入与撤销状态)。 审查视角:全栈开发工程师 + 安全审计。 审查方式:静态代码审计 + 攻击路径推演(伪造许可证、跨机复制、时钟操纵、隐私泄露)。因 WSL 无 Go,未执行测试。 日期:2026-07-20
总体判断
Phase 5 的密码学与隐私实现质量高,未发现验签绕过、跨机复制通过或原始硬件标识泄露的路径。 三条链(指纹派生 → 许可证验签 → 撤销状态)都 fail-closed,且 core/internal/canonicaljson 被 Catalog / License / Revocation 共用一套实现,避免了第二套 canonicalizer 漂移——这是很好的纪律。
但本轮发现两个需要产品裁定的问题(均非代码缺陷,代码与架构文档一致):
- 撤销名单是授权的硬依赖——没有名单或名单过期超过宽限,所有软件都无法启动,与"离线授权 / 永久使用"的愿景存在张力,且缺分发方案。
- "试用"是
02-requirements的 P0 需求与验收项,但未实现,且02-requirements与架构/任务文档口径不一致。
逐模块核验
T-501 · 机器指纹(licensing/machine_hash + platform/windows)
- 域分隔 + 定长拼接:
"softbox.machine-hash.v1\x00" + 规范化 GUID + "\x00" + %08x 卷序列号→ SHA-256 → 64 位小写 hex。域前缀防跨协议碰撞,NUL 分隔防拼接歧义。 - 严格规范化:GUID 必须恰好 36 字符、纯 ASCII、位置 8/13/18/23 为
-、其余为 hex,统一小写;不合法即ErrInvalidMachineGUID。 - 隐私正确:错误值"intentionally contains no source value";平台层"only for the duration of this call"读取
MachineGuid(注册表)与 Windows 目录所在卷序列号;不使用 MAC,原始 GUID / 序列号不落盘、不进事件/UI/日志。 - 平台隔离:
machine_hash_windows.go(//go:build windows)+machine_hash_stub.go(非 Windows fail-closed)。
关于"严格双来源"的取舍:实现为 2-of-2,任一来源变化即指纹变化。早期架构风险表曾写"多标识加权 + 容错",现已同步更新为"v1 固定双来源、缺失即拒绝;不在客户端做部分匹配,后续按签发端 rebind/申诉流程处理"(04-architecture.md)。文档与实现一致,且该取舍更安全——客户端部分匹配会削弱绑定强度。代价是硬件/系统变动必然需要签发端换绑,故换绑流程从"可选"变为必需的运营依赖。
T-502 · 许可证验签(licensing/verifier + internal/canonicaljson)
验签链顺序正确且严格:
- 校验注入公钥 32 字节(复制持有)、
expectedMachineHash为 64 位小写 hex。 canonicaljson.Parse(共用受限解析:拒绝重复字段、非整数数字等)→ 取顶层signature→ 删除 →canonicaljson.Marshal得签名字节 →ed25519.Verify。decodeCanonicalSignature做 base64 重编码回比(EncodeToString(sig) != value即拒),彻底排除 CR/LF、空白、padding 变体等非唯一表示——这正是 Phase 1 对 Catalog 提出的收紧,在此已正确落地。hasExactLicenseShape:恰好 9 个字段、类型逐一匹配,多一个少一个都拒。decodeLicense:DisallowUnknownFields+UseNumber+ 尾随数据拒绝。validateLicense:schema_version==1、license_id/machine_hash/policy 正则、issued_at时间戳格式回比(非规范写法拒绝)、products 非空 + 正则 + 去重。license.MachineHash != expectedMachineHash→ErrMachineMismatch:跨机复制被拒。
所有错误均为无内容 sentinel,不回显文档;core 不含任何私钥,公钥由调用方显式注入。
T-503 · 撤销状态与授权服务(licensing/revocation + application/authorization)
撤销验签与 License 同构(共用 canonical、精确 5 字段、签名回比、时间戳回比、ID 正则 + 去重),并强制 MaxRevocationValidity = 31 天 的签名有效期上限。
StateFor 状态机(安全关键):
- 时钟回拨防御:
now.Before(GeneratedAt)→Unavailable。把系统时钟调到名单生成之前不会让过期名单重新"变新",而是 fail closed。 - 有效期边界在求值时二次校验(不只解析时)。
Revoked判定先于过期判定:已撤销的许可证在任何新鲜度状态下都是Revoked。- 未过期 →
Current;过期后 7 天内 →Grace;之后 →Unavailable。
授权服务:
- 快照净化正确:
AuthorizationSnapshot{State, MachineHash, Products},AuthorizedProduct明确"contains no license ID, source path, signature or source document"——UI 拿不到 license ID、签名、JSON、文件路径或原始 Windows 标识;MachineHash为派生值,可安全展示(用户申请换绑需要)。 - 仅
Current/Grace授权;Revoked/Unavailable一律不授权。 IsAuthorized(productID)按 product ID 而非 app ID 判定,与 T-503 把启动门禁改为 product 维度一致。- fail-closed 默认:
UnconfiguredAuthorizationLoader发布unconfigured+ 空 Products;bootstrap 对未配置发Unconfigured、对其他错误发Unavailable,两者都不授权;导入失败发ImportFailed+ 空 Products。 - UI 纪律:点击只入队,文件选择/读取/验签/写盘全在后台,经 relay 回 UI goroutine。
需优化项(按优先级)
O1 · 撤销名单是授权硬依赖,与"离线授权"愿景存在张力(需产品裁定 + 分发方案)
现状(已核实):
list, found, err := service.store.LoadRevocations(service.revocationVerifier)
if err != nil || !found {
return unavailableAuthorizationSnapshot(...), ErrAuthorizationUnavailable
}
没有撤销名单(!found)或名单验签失败 → Unavailable → 空 Products → IsAuthorized 对一切返回 false → 所有软件都无法启动。 叠加新鲜度上限:一份名单最多覆盖 31 天有效期 + 7 天宽限 = 38 天(自 generated_at 起)。
影响:
- 首次授权必须已有撤销名单:用户导入了合法许可证,但从未获得撤销名单 → 一个软件也启动不了。
- 纯离线用户约 38 天后全部失效:必须周期性获得新签名名单,否则授权中断。
- 这与
01-vision.md的"离线授权…离线可验证"和"永久使用与更新权分离"的直观预期存在张力:许可证确实可离线验签,但整体授权并非可无限期离线。
代码与 04-architecture.md("unavailable/revoked 一律不可授权")一致,故非缺陷;但这是一个尚未被显式裁定的产品后果。
建议:
- 产品层显式裁定:"离线超过约 38 天即停止授权"是否可接受;若不可接受,考虑对永久许可证放宽(如缺名单时降级为"仅允许已装软件启动、不允许新装/更新"),而非一刀切拒绝。
- 无论如何裁定,都必须有分发方案:撤销名单随许可证文件一并交付 / 安装包内置初始名单 / 首次联网自动获取,否则首次离线授权不可能成立。
- 把结论写进
01-vision.md与02-requirements.md,避免"离线授权"被误读为"无限期离线可用"。
O2 · "试用"是 P0 需求与验收项,但未实现(文档口径冲突,需裁定)
现状(已核实当前文档,非记忆):
02-requirements.md:35(功能清单,P0):"机器绑定许可证 | 导入许可证,离线验证,区分正式版/试用版/授权错误"02-requirements.md:67(MVP 验收):"试用与正式状态在盒子和子软件中一致"02-requirements.md:17(用户角色):普通用户可"试用"- 但
docs/tasks/T-503.md:"本任务不实现本地试用计时、到期、绕过授权或自助换绑";04-architecture.md:193:"试用/到期/自助换绑/申诉仍不在客户端实现" - 代码核实:
supports_trial仅作为元数据从 app.json 透传到installed-app.json,无任何试用计时/到期逻辑(authorization.go注释亦承认 "trial expiration which License v1 does not carry");无许可证时IsAuthorized恒 false,不存在试用启动路径。
影响: 需求文档声明 P0 且列入 MVP 验收,实现与架构/任务文档明确排除——两侧文档互相冲突,按现状 MVP 验收无法通过"试用与正式状态一致"这一条。
建议: 二选一并同步文档:
- (a) 补一个试用任务(需先在
api.md定义 License v1 如何表达试用/到期,或定义无许可证时的本地试用计时与防回拨策略);或 - (b) 把试用从 MVP P0 降级到 V1.1/V2,同步修改
02-requirements.md的功能清单与验收标准。
不要保留当前的文档冲突状态。
O3 · License 与 Revocation 共用同一授权公钥
现状: RevocationVerifier 注释说明"Callers may inject the same controlled authorization key used for License v1, but no fallback key exists"。
影响: 单密钥泄露同时意味着可伪造许可证与伪造/替换撤销名单(后者可用于抹掉撤销)。v1 简化可接受,但两类文档的信任影响不同。
建议: 后续协议升级(与 key ID / rotation 一并)时评估签名密钥分离;当前至少在发布端设计文档中记录该单点。
O4 · 无生产装配(与 Phase 3/4 一致)
现状: 双端 cmd/softbox/main.go 均注入 UnconfiguredAuthorizationLoader;无真实信任根、许可证文件来源、撤销获取与文件选择 composition。
影响: 与 Phase 3/4 结论一致——无头 core 的授权边界已完成;端到端授权闭环未接通。不注入 allow-all 是好纪律。M4(自更新 + 授权完成,MVP 验收)仍需端到端装配。
建议: 随发布配置(信任根公钥、许可证/撤销分发)一并落地,并在 current-state.md 保持准确表述。
结论
Phase 5 的密码学实现可作为授权基线:指纹派生域分隔且隐私正确、许可证与撤销验签严格(含 base64 回比、字段精确匹配、时间戳回比)、时钟回拨防御到位、快照净化与 fail-closed 默认正确、canonical JSON 单一实现共用。建议处理顺序:
- [产品裁定 · 最高] O1:裁定离线授权上限与降级策略,并确定撤销名单的分发方案(随许可证交付 / 内置初始名单 / 首次联网获取);结论回写愿景与需求。
- [消除文档冲突] O2:裁定试用归属(补任务 或 降级出 MVP),同步
02-requirements.md与架构/任务文档。 - [发布端记录] O3:在 softbox-catalog 设计中记录 License/Revocation 单密钥现状,随 key ID/轮换一并评估分离。
- [装配] O4:随发布配置落地端到端授权,保持"无头 core 已完成、端到端未接通"的准确表述。
声明:本报告为静态审计与攻击路径推演,逐项核了实现(域分隔哈希与 GUID 规范化、base64 重编码回比、9 字段精确形状、
ErrMachineMismatch跨机拒绝、StateFor时钟回拨与 revoked 优先、快照净化字段、IsAuthorized状态门禁、双端UnconfiguredAuthorizationLoader装配);O1/O2 的事实均来自当前文档与代码重读,非记忆推断。因 WSL 无 Go 未执行测试,测试通过为 Codex 自述。