Define license verification task (T-502)
This commit is contained in:
@@ -55,7 +55,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端,
|
|||||||
2. 已完成 Phase 1:清单验签、ZIP 安全解压、原子切换回滚原型。
|
2. 已完成 Phase 1:清单验签、ZIP 安全解压、原子切换回滚原型。
|
||||||
3. 已完成 Phase 2 与 T-301:清单/列表/详情/图标缓存 + 可恢复下载队列。
|
3. 已完成 Phase 2 与 T-301:清单/列表/详情/图标缓存 + 可恢复下载队列。
|
||||||
4. 已完成 T-604:modern/Win7 workspace 与 Gio 版本解析彻底隔离。
|
4. 已完成 T-604:modern/Win7 workspace 与 Gio 版本解析彻底隔离。
|
||||||
5. 已完成 T-606~T-615:图标缓存资源边界、UI 线程事件接线、双 Gio 适配器交互契约、`VisibleItems` generation 生命周期、双端 `shell.go` 同 package 镜像职责拆分、unsafe cache 诊断/人工恢复指引、ZIP 中央目录/EOCD 预扫描、安装耐久顺序、Catalog 静态签名向量,以及 staging 输出 I/O 根因与磁盘满诊断;已完成 T-302/T-303:已验签 Catalog 选择与同句柄 size/SHA、严格 app.json、安全 staging/switch/健康与记录写回滚链路,以及 staging 前磁盘/运行状态预检与稳定失败码。T-401 已完成进程检测、受控启动与切换临界区复查;T-402 已完成关闭确认、自然退出等待和更新编排;T-403 已完成 SoftBoxUpdater 的受限 transaction、PID 自然退出、固定健康启动与恢复;T-616 已完成 Catalog 快照启动投递与明确空态诊断;T-617 已完成 prepared rename+sync 的恢复修复、五 phase Recover 与双端 health flag 参数拒绝测试;T-501 已完成严格双来源的 machine_hash v1。下一步按路线图正式落成 Phase 5 的 T-502。T-601 仍须补真实 Windows 环境的断电/干扰注入。
|
5. 已完成 T-606~T-615:图标缓存资源边界、UI 线程事件接线、双 Gio 适配器交互契约、`VisibleItems` generation 生命周期、双端 `shell.go` 同 package 镜像职责拆分、unsafe cache 诊断/人工恢复指引、ZIP 中央目录/EOCD 预扫描、安装耐久顺序、Catalog 静态签名向量,以及 staging 输出 I/O 根因与磁盘满诊断;已完成 T-302/T-303:已验签 Catalog 选择与同句柄 size/SHA、严格 app.json、安全 staging/switch/健康与记录写回滚链路,以及 staging 前磁盘/运行状态预检与稳定失败码。T-401 已完成进程检测、受控启动与切换临界区复查;T-402 已完成关闭确认、自然退出等待和更新编排;T-403 已完成 SoftBoxUpdater 的受限 transaction、PID 自然退出、固定健康启动与恢复;T-616 已完成 Catalog 快照启动投递与明确空态诊断;T-617 已完成 prepared rename+sync 的恢复修复、五 phase Recover 与双端 health flag 参数拒绝测试;T-501 已完成严格双来源的 machine_hash v1。T-502 已正式冻结 License v1 的共享 canonical JSON、Ed25519 验签和 machine_hash 比对,下一步领取实现。T-601 仍须补真实 Windows 环境的断电/干扰注入。
|
||||||
|
|
||||||
## 领取任务规则
|
## 领取任务规则
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ T-613 已把该状态机的代码层耐久顺序收敛为:payload 的 CRC/长度
|
|||||||
|
|
||||||
T-403 的盒子自更新由独立、无 Gio 的 `SoftBoxUpdater.exe` 完成。它仅接受正 PID、绝对 `<root>/app` 和绝对 `<root>/staging/<request-id>`;request ID 不走 CLI,而是从已准备 staging 的规范目录名派生并复验。旧 PID 自然退出后才检查/恢复上次 journal,随后以 `prepared → target_backed_up → staging_activated → launched → committed` 把 `app` 与同 root staging 受限 rename。T-617 固定 `prepared` 的恢复依据为经 `Lstat` 验证的实际 target/backup 拓扑:仅 target 存在且 backup 不存在才删除 journal;target 缺失且 managed backup 存在必须先 restore;其余组合保留 journal/材料并 fail closed。因此首次 `app → backup` rename 已完成但目录 sync 失败时,当前调用会立即尝试该受限收敛,下一次 Recover 也不会再盲删 journal。`core/updater` 只依赖 PID waiter、固定 launcher、health waiter 和目录同步接口;Windows 的 `OpenProcess(SYNCHRONIZE)`/短等待、无 shell 固定 health 启动和 `FlushFileBuffers` 均保留在两端 `platform/windows`,非 Windows stub fail closed。新版仅能从自身 `<root>/app/SoftBox.exe` 在匹配的已激活 transaction 下写最小 `<root>/self-update-health.json` 确认;确认前旧 backup 不删除,失败优先 restore,文件锁导致 restore 失败则保留 journal/backup 而不强杀。它不提供下载、签名校验、版本选择或 UI 触发,可信 package→staging 链继续后置。
|
T-403 的盒子自更新由独立、无 Gio 的 `SoftBoxUpdater.exe` 完成。它仅接受正 PID、绝对 `<root>/app` 和绝对 `<root>/staging/<request-id>`;request ID 不走 CLI,而是从已准备 staging 的规范目录名派生并复验。旧 PID 自然退出后才检查/恢复上次 journal,随后以 `prepared → target_backed_up → staging_activated → launched → committed` 把 `app` 与同 root staging 受限 rename。T-617 固定 `prepared` 的恢复依据为经 `Lstat` 验证的实际 target/backup 拓扑:仅 target 存在且 backup 不存在才删除 journal;target 缺失且 managed backup 存在必须先 restore;其余组合保留 journal/材料并 fail closed。因此首次 `app → backup` rename 已完成但目录 sync 失败时,当前调用会立即尝试该受限收敛,下一次 Recover 也不会再盲删 journal。`core/updater` 只依赖 PID waiter、固定 launcher、health waiter 和目录同步接口;Windows 的 `OpenProcess(SYNCHRONIZE)`/短等待、无 shell 固定 health 启动和 `FlushFileBuffers` 均保留在两端 `platform/windows`,非 Windows stub fail closed。新版仅能从自身 `<root>/app/SoftBox.exe` 在匹配的已激活 transaction 下写最小 `<root>/self-update-health.json` 确认;确认前旧 backup 不删除,失败优先 restore,文件锁导致 restore 失败则保留 journal/backup 而不强杀。它不提供下载、签名校验、版本选择或 UI 触发,可信 package→staging 链继续后置。
|
||||||
|
|
||||||
授权:平台层短暂读取严格规范化的 `MachineGuid` 与 Windows 目录所在卷序列号两个必需来源 → 用固定域分隔 SHA-256 生成 `machine_hash` v1(不保存原始 GUID、序列号或 MAC;任一来源失败即拒绝)→ 服务端 Ed25519 私钥签发许可证 → 客户端内置公钥离线验签;许可证与程序文件、用户配置分开保存;子软件必须独立再次验证,不能只信盒子。core 只实现纯 hash 算法,Windows 采集留在双端 `platform/windows`,非 Windows stub fail closed。
|
授权:平台层短暂读取严格规范化的 `MachineGuid` 与 Windows 目录所在卷序列号两个必需来源 → 用固定域分隔 SHA-256 生成 `machine_hash` v1(不保存原始 GUID、序列号或 MAC;任一来源失败即拒绝)→ 服务端 Ed25519 私钥对删除顶层 `signature` 后的受限 canonical License v1 JSON 签名 → 客户端 core 以调用方注入的 32 字节公钥离线验签、严格验证九字段并精确比对 machine_hash/product ID;许可证与程序文件、用户配置分开保存;子软件必须独立再次验证,不能只信盒子。受限 canonical JSON 为 Catalog/License 共用的 `core/internal` 实现;core 不读许可证文件也不含生产 key,Windows 采集留在双端 `platform/windows`,非 Windows stub fail closed。导入/存储/试用/撤销/rebind/UI composition 仍属于 T-503。
|
||||||
|
|
||||||
## 六、关键技术难点
|
## 六、关键技术难点
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -118,7 +118,7 @@ T-617 已关闭 T-403 自更新的 `prepared` rename 后目录 sync 失败会留
|
|||||||
| ID | 任务 | 依赖 | 验收要点 |
|
| ID | 任务 | 依赖 | 验收要点 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| T-501 | 机器指纹(machine_hash) | T-001 | 固定 MachineGuid + Windows 系统卷序列号的 v1 域分隔 SHA-256;任一来源失败拒绝、不落原始标识,非 Windows stub 可测 |
|
| T-501 | 机器指纹(machine_hash) | T-001 | 固定 MachineGuid + Windows 系统卷序列号的 v1 域分隔 SHA-256;任一来源失败拒绝、不落原始标识,非 Windows stub 可测 |
|
||||||
| T-502 | Ed25519 许可证验证 | T-501 | licensing 模块离线验签;复制到不匹配机器被拒;测试用专用密钥对 |
|
| T-502 | Ed25519 许可证验证 | T-501 | 固定 License v1 九字段、共享受限 canonical JSON、canonical Base64 Ed25519 签名与严格 machine_hash 比对;静态语料无私钥,复制到不匹配机器拒绝 |
|
||||||
| T-503 | 授权界面与试用/导入/换绑 | T-502, T-204 | 导入、授权列表、试用状态展示;撤销名单验签与宽限期 |
|
| T-503 | 授权界面与试用/导入/换绑 | T-502, T-204 | 导入、授权列表、试用状态展示;撤销名单验签与宽限期 |
|
||||||
|
|
||||||
### Phase 6 · Win7 加固与发布
|
### Phase 6 · Win7 加固与发布
|
||||||
|
|||||||
+9
-4
@@ -319,10 +319,15 @@ Windows 平台用 Toolhelp32 快照枚举,并以 `QueryFullProcessImageName`
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `machine_hash` v1 固定为平台层对两个必需的瞬时来源计算的 64 字符小写 SHA-256:去首尾空白并小写化的 36 位 ASCII `MachineGuid`、以及 Windows 目录所在卷的 8 位小写十六进制序列号,以 `"softbox.machine-hash.v1\\x00" || guid || "\\x00" || volume_serial` 为输入。任一来源读取/规范化失败即拒绝,不存在单来源、MAC、加权或环境变量回退;许可证、存储、事件、UI 和日志均**不保存**原始 GUID、卷序列号或 MAC。
|
### 3.1 License v1 wire contract
|
||||||
- 客户端用内置 Ed25519 公钥离线验签;许可证保存于 `licenses/`,与程序文件、用户配置分离;更新不得覆盖。
|
|
||||||
- 撤销名单同样签名并缓存,网络失败保留宽限期。
|
License v1 只允许示例中的九个顶层字段,全部必需,禁止未知或重复字段。`license_id` 匹配 `^lic-[a-z0-9][a-z0-9-]{0,59}$`;`products` 至少有一个、无重复、每项匹配软件包 `app.json` `product_id` 的 `^[a-z0-9-]+$`;`machine_hash` 必须是 64 位小写 hex;`issued_at` 必须精确为 `YYYY-MM-DDTHH:MM:SSZ`;`update_policy` 与 `rebind_policy` 均匹配 `^[a-z0-9][a-z0-9._:-]{0,127}$`。`schema_version` 只能为 `1`。
|
||||||
- 盒子负责导入/展示/管理;**子软件必须用 sdk 的 licensing 逻辑独立再验证**(签名 + machine_hash + product_id),决定正式版/试用版/授权错误。
|
|
||||||
|
签发端先删除顶层 `signature`,再以 Catalog v1 相同的受限 canonical JSON 生成 signing bytes:输入必须是有效 UTF-8、无未配对 JSON surrogate、无重复 object key,且数字只能为非 `-0` 的十进制整数;递归按 object key 字典序输出、没有非必要空白。`signature` 是对该字节串的 Ed25519 签名,编码必须是**精确 canonical 的标准 padded Base64**(64 个签名字节)。`testdata/license/` 的静态 corpus 是跨实现的规范向量;不得签原始 JSON 文本或仅部分字段。
|
||||||
|
|
||||||
|
验证器先拒绝不安全 JSON/签名,再严格验证字段与签名,最后将许可证中的 hash 与调用方从 T-501 得到的 64 位小写 expected hash 精确比较。未配置或非法 public key、任何格式/验签失败、hash 不匹配、未知产品均 fail closed;错误、事件和 UI 不得包含 license JSON、signature、license ID 或任何原始机器来源。`machine_hash` v1 固定由短暂读取的规范化 `MachineGuid` 与 Windows 目录所在卷序列号生成:`SHA-256("softbox.machine-hash.v1\\x00" || guid || "\\x00" || volume_serial)`;许可证、存储、事件、UI 和日志均**不保存**原始 GUID、卷序列号或 MAC。
|
||||||
|
|
||||||
|
T-502 的 core verifier 接收调用方注入的 public key 和 expected hash;当前仓库没有生产 trust root,不能以测试 key、环境变量或 allow-all 代替。`perpetual`、update/rebind policy 在本任务只作为已签名元数据,不实施过期、trial、撤销名单、宽限期或 rebind 决策。许可证文件保存于 `licenses/` 且更新不得覆盖,但文件导入/存储、授权页面和启动用例接线留给 T-503;届时盒子和子软件都必须独立再验证签名、machine_hash 与 product ID。
|
||||||
|
|
||||||
## 4. application 事件合约(core → UI)
|
## 4. application 事件合约(core → UI)
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
## 当前快照
|
## 当前快照
|
||||||
|
|
||||||
- 日期:2026-07-19
|
- 日期:2026-07-20
|
||||||
- 阶段:Phase 2 已完成(T-201~T-204)并由 T-616 补齐启动 Catalog 快照投递/空态诊断;Phase 3 的 T-301 可恢复下载队列、T-302 安装流程整合、T-303 失败处理/磁盘预检查与 T-615 staging 输出 I/O/磁盘满诊断整改已完成;审核整改 T-604~T-617 与 Phase 4 的 T-401 进程检测、受控启动和切换临界区复查、T-402 子软件更新编排、T-403 受限盒子自更新事务及 T-617 恢复状态机/测试整改已完成;Phase 5 的 T-501 严格双来源 machine_hash v1 已完成
|
- 阶段:Phase 2 已完成(T-201~T-204)并由 T-616 补齐启动 Catalog 快照投递/空态诊断;Phase 3 的 T-301 可恢复下载队列、T-302 安装流程整合、T-303 失败处理/磁盘预检查与 T-615 staging 输出 I/O/磁盘满诊断整改已完成;审核整改 T-604~T-617 与 Phase 4 的 T-401 进程检测、受控启动和切换临界区复查、T-402 子软件更新编排、T-403 受限盒子自更新事务及 T-617 恢复状态机/测试整改已完成;Phase 5 的 T-501 严格双来源 machine_hash v1 已完成
|
||||||
- 技术栈:根 Go 1.25 workspace 只纳入 core/app-modern,`app-win7/go.work` 独立纳入 core/app-win7;版本闸门证明 modern Gio v0.10.1 与 win7 Gio v0.6.0 不交叉解析
|
- 技术栈:根 Go 1.25 workspace 只纳入 core/app-modern,`app-win7/go.work` 独立纳入 core/app-win7;版本闸门证明 modern Gio v0.10.1 与 win7 Gio v0.6.0 不交叉解析
|
||||||
- 生产代码:core 已有 Catalog/本地状态/存储、共享 Windows 安全相对路径策略与静态跨实现 canonicalization/Ed25519 vector corpus(拒绝非法 surrogate、`-0` 和非唯一 Base64 signature,大整数保持 token)、安全 ZIP 解压/回滚原型及 T-302/T-303/T-615 安装 use case(`core/application/install.InstallService` 只取已过滤 Catalog entry + architecture,强制注入 disk/storage-failure/target-state checker;`Extractor.ExtractVerifiedFileWithCheck` 在同一普通文件句柄按 size→SHA-256→EOCD/ZIP64→严格 app.json→已规划 payload 的 staging 前预检→安全 staging 的顺序处理,空间要求为 payload+64 MiB,ZIP 输入错误与 staging 创建/write/sync/close 错误分界并保留原始 I/O 链;平台可识别的后者磁盘满返回 `disk_full`,其余输出 I/O 返回稳定 code 且不触发 switch;清理失败可观察,Recover 仅删除已验证 layout 内的残留 staging;每个实际 payload 文件 hash 与受验证 entrypoint/working directory/min_os/requires_admin 写入 installed-app;health 或记录写失败经 Switcher 回滚;更新 current→backup 紧邻前复查精确 entrypoint,明确运行/检测故障保持旧版本并清理 staging),transaction/switch/rollback/recovery 的 journal、rename、清理经统一 fail-closed 耐久栅栏,Windows 使用目录句柄 FlushFileBuffers)、纯 core `application/launch`(只接收 app ID、受控 current/普通 entrypoint/兼容/授权/运行状态/启动器接口全部 fail closed)、双端 Toolhelp 完整映像路径检测/Win7 可用系统版本判断/无参数受控启动与非 Windows fail-closed stub、发布稳定只读 generation 的无 IO 软件列表模型、按 key in-flight + 流式有界读取 + 32 MiB/256-key LRU 的可信图标缓存、图标 Load/Decode 事件发布用例、有界 application event relay,以及默认并发 2 的持久可恢复下载队列;modern/win7 主循环已接 relay/Invalidate,AppShell 已实现搜索/分类/视图、惰性列表、详情右栏、完整图标失败 identity 生命周期与仅 `unsafe_cache` 可见的安全 locator/人工恢复提示,并按 root/header/catalog/detail/style 同 package 镜像职责拆文件
|
- 生产代码:core 已有 Catalog/本地状态/存储、共享 Windows 安全相对路径策略与静态跨实现 canonicalization/Ed25519 vector corpus(拒绝非法 surrogate、`-0` 和非唯一 Base64 signature,大整数保持 token)、安全 ZIP 解压/回滚原型及 T-302/T-303/T-615 安装 use case(`core/application/install.InstallService` 只取已过滤 Catalog entry + architecture,强制注入 disk/storage-failure/target-state checker;`Extractor.ExtractVerifiedFileWithCheck` 在同一普通文件句柄按 size→SHA-256→EOCD/ZIP64→严格 app.json→已规划 payload 的 staging 前预检→安全 staging 的顺序处理,空间要求为 payload+64 MiB,ZIP 输入错误与 staging 创建/write/sync/close 错误分界并保留原始 I/O 链;平台可识别的后者磁盘满返回 `disk_full`,其余输出 I/O 返回稳定 code 且不触发 switch;清理失败可观察,Recover 仅删除已验证 layout 内的残留 staging;每个实际 payload 文件 hash 与受验证 entrypoint/working directory/min_os/requires_admin 写入 installed-app;health 或记录写失败经 Switcher 回滚;更新 current→backup 紧邻前复查精确 entrypoint,明确运行/检测故障保持旧版本并清理 staging),transaction/switch/rollback/recovery 的 journal、rename、清理经统一 fail-closed 耐久栅栏,Windows 使用目录句柄 FlushFileBuffers)、纯 core `application/launch`(只接收 app ID、受控 current/普通 entrypoint/兼容/授权/运行状态/启动器接口全部 fail closed)、双端 Toolhelp 完整映像路径检测/Win7 可用系统版本判断/无参数受控启动与非 Windows fail-closed stub、发布稳定只读 generation 的无 IO 软件列表模型、按 key in-flight + 流式有界读取 + 32 MiB/256-key LRU 的可信图标缓存、图标 Load/Decode 事件发布用例、有界 application event relay,以及默认并发 2 的持久可恢复下载队列;modern/win7 主循环已接 relay/Invalidate,AppShell 已实现搜索/分类/视图、惰性列表、详情右栏、完整图标失败 identity 生命周期与仅 `unsafe_cache` 可见的安全 locator/人工恢复提示,并按 root/header/catalog/detail/style 同 package 镜像职责拆文件
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准。本节只写项目级摘要:
|
任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准。本节只写项目级摘要:
|
||||||
|
|
||||||
- 已完成:Phase 0 的 `T-001`~`T-004`;Phase 1 的 `T-101`、`T-102`、`T-103`;Phase 2 的 `T-201`~`T-204` 与 `T-616`;Phase 3 的 `T-301`~`T-303` 与 `T-615`;审核整改 `T-604`~`T-617`;Phase 4 的 `T-401`~`T-403`;Phase 5 的 `T-501`。
|
- 已完成:Phase 0 的 `T-001`~`T-004`;Phase 1 的 `T-101`、`T-102`、`T-103`;Phase 2 的 `T-201`~`T-204` 与 `T-616`;Phase 3 的 `T-301`~`T-303` 与 `T-615`;审核整改 `T-604`~`T-617`;Phase 4 的 `T-401`~`T-403`;Phase 5 的 `T-501`。
|
||||||
- 正在进行:无;下一步可按路线图正式落成 T-502(离线 Ed25519 许可证验证,依赖 T-501)。T-601 的物理断电与干扰故障注入仍保留为发布前环境验证。
|
- 正在进行:无;`T-502` 已正式落成,冻结 License v1 的九字段、共享受限 canonical JSON、Ed25519 验签与严格 machine_hash 比对,下一步领取实现。T-601 的物理断电与干扰故障注入仍保留为发布前环境验证。
|
||||||
|
|
||||||
## 当前可运行内容
|
## 当前可运行内容
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
id: T-502
|
||||||
|
title: Ed25519 离线许可证验证
|
||||||
|
phase: 5
|
||||||
|
deps: [T-501]
|
||||||
|
status: TODO
|
||||||
|
created: 2026-07-20
|
||||||
|
issue: null
|
||||||
|
context_ref: null
|
||||||
|
claim_branch: null
|
||||||
|
work_branch: null
|
||||||
|
write_paths:
|
||||||
|
- docs/tasks/T-502.md
|
||||||
|
- core/internal/canonicaljson/
|
||||||
|
- core/catalog/canonical.go
|
||||||
|
- core/catalog/canonical_vectors_test.go
|
||||||
|
- core/licensing/
|
||||||
|
- schemas/license.schema.json
|
||||||
|
- testdata/license/
|
||||||
|
- docs/00-ai-start-here.md
|
||||||
|
- docs/04-architecture.md
|
||||||
|
- docs/06-tasks.md
|
||||||
|
- docs/api.md
|
||||||
|
- docs/current-state.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题 / 背景
|
||||||
|
|
||||||
|
T-501 已能从 Windows 平台得到不含原始硬件标识的 `machine_hash`,但客户端尚不能解析、验签或比较许可证。因此复制许可证到另一台机器没有可执行的拒绝逻辑,`application/launch.AuthorizationChecker` 也还没有可由后续 composition 使用的可信授权事实。
|
||||||
|
|
||||||
|
现有许可证 JSON 只有字段草案,未指定签名载荷、重复字段、未知字段、签名编码、时间格式或机器摘要格式的 fail-closed 规则。Catalog 已有经过静态 corpus 覆盖的受限 canonical JSON 和 Ed25519 模式;另写一套近似实现会让同一服务端签发的协议在两个安全模块中漂移。
|
||||||
|
|
||||||
|
## 方案
|
||||||
|
|
||||||
|
1. 把 Catalog 当前的受限 JSON 解析与 canonical 输出下沉到仅 `core/` 可见的 `core/internal/canonicaljson`,保持 Catalog 导出的错误身份和静态 corpus 行为不变。该实现只接受有效 UTF-8、无未配对 surrogate、无重复 object key、整数(拒绝 `-0`、小数和指数),按递归字典序 key、无多余空白的 JSON 生成 signing bytes;Catalog 保留薄 wrapper,避免改变既有公开行为。
|
||||||
|
2. 在纯 Go 1.20 的 `core/licensing` 增加 `License`、`Verifier` 和只读产品授权查询。`NewVerifier(publicKey)` 复制并严格要求 32 字节 Ed25519 公钥;`Verify(document, expectedMachineHash)` 先以共享 canonical JSON 移除顶层 `signature` 得到 signing bytes,要求 64 字节标准 padded Base64 签名,再验签、严格 decode 和验证字段,最后精确比较调用方提供的 64 位小写 `machine_hash`。任何失败返回稳定、无许可证内容的 sentinel;成功结果深拷贝 products,不保存 document、签名或原始机器来源。
|
||||||
|
3. 冻结 License v1:只允许 `schema_version`、`license_id`、`machine_hash`、`products`、`issued_at`、`perpetual`、`update_policy`、`rebind_policy`、`signature` 九个顶层字段,全部必需且不得重复/未知。`license_id`、product 与 policy 分别匹配 `^lic-[a-z0-9][a-z0-9-]{0,59}$`、`^[a-z0-9-]+$`、`^[a-z0-9][a-z0-9._:-]{0,127}$`;products 非空且不重复,`issued_at` 是精确 RFC3339 UTC 秒级文本,`machine_hash` 必须为 T-501 的 64 字符小写 hex。canonical signing bytes 是删除顶层 `signature` 后的共享受限 canonical JSON;`schema_version` 只能为 1。`perpetual`、update/rebind policy 仅作为已签名元数据,本任务不实施 expiry、trial、revocation 或 rebind 决策。
|
||||||
|
4. 新增 `schemas/license.schema.json` 和只含虚构产品、测试公钥、已签名 document/签名载荷的静态 license corpus。绝不提交生产公钥、任何生产或测试私钥、真实许可证 ID、机器摘要或注册码。测试证明有效语料、对象键/空白等价 canonicalization、篡改、错误 key、重复/未知字段、非 canonical Base64、非法 JSON/数字、错误机器摘要、产品查询以及输入/输出深拷贝均 fail closed;现有 Catalog corpus 继续覆盖下沉后的兼容性。
|
||||||
|
5. 更新 API/架构/路线图和项目快照:当前 core verifier 只接收调用方注入的 public key 和 T-501 hash,尚无生产信任根、许可证文件读写、UI、试用、撤销名单、update/rebind 策略或启动用例 composition。T-503 才从受控 `licenses/` 文件导入、展示并把经验证的 product 授权接到盒子与子软件流程。
|
||||||
|
|
||||||
|
## 验收要点
|
||||||
|
|
||||||
|
- `core/licensing` 不 import Gio、Windows、文件系统或网络,在 Go 1.20 下能离线验证 v1 许可证;public key、document、expected machine hash、signature、字段形状或产品授权任一不可信时 fail closed。复制同一已签名 document 到不同 expected hash 必须返回 `ErrMachineMismatch`,而不是“未授权”或成功。
|
||||||
|
- License v1 的签名覆盖所有除顶层 `signature` 外字段,签名文本仅接受 canonical padded standard Base64 的 64 字节 Ed25519 值;重复/未知字段、额外 JSON、未配对 surrogate、`-0`、小数/指数、非 UTC 秒级时间、大小写错误 hash、空/重复 products 和无效 policy/ID 都被拒绝,错误和 API 返回不包含许可证 JSON、signature、ID 或机器标识。
|
||||||
|
- Catalog 的现有 canonical corpus 继续原样通过;许可证静态 corpus 不含私钥或真实值,却能固定跨实现 signing bytes 和验签结论。返回 `License` 的 products 及输入 public key 均无法在调用后改变 verifier 内部授权结论。
|
||||||
|
- `schemas/license.schema.json`、`docs/api.md`、架构和任务验收严格一致;文档明确 T-502 不提供生产公钥、文件导入、持久化、trial/revocation/rebind 或 UI,不能用 allow-all checker 宣称启动授权闭环已完成。
|
||||||
|
- `go -C core vet ./...`、`go -C core test -count=1 ./...`、两个 app 的 `go test -count=1 ./...`、两个 Windows amd64 主程序构建、`./scripts/verify_phase0.ps1`、`python scripts/validate_agent_context.py` 与 `python scripts/validate_harness_governance.py` 全部通过。
|
||||||
|
|
||||||
|
## 边界(不改什么)
|
||||||
|
|
||||||
|
- 不提交生产公钥、任何私钥、真实许可证/机器摘要/注册码;不发明环境变量、网络取 key、allow-all verifier 或未签名许可证回退。
|
||||||
|
- 不读写 `licenses/`、不实现文件导入、原子存储、撤销名单、宽限期、试用/过期、换绑、SDK 分发、Gio 授权页或 `application/launch` composition;这些由 T-503 和发布配置处理。
|
||||||
|
- 不修改 T-501 的机器来源/摘要算法、Catalog 信任语义、下载、安装、更新、自更新、Windows API 或 Gio Layout;Catalog canonical 重构仅限保持现有行为的内部复用,不改变 manifest 协议。
|
||||||
|
|
||||||
|
## 协作约束
|
||||||
|
|
||||||
|
- 当前项目为单 Agent 串行模式;当前 Agent 独占全部 `write_paths`,自行完成规格、实现、测试、审查、状态更新和提交,不启动子 Agent。
|
||||||
|
- 本规格提交后才领取任务:将本文件改为 `DOING`,填写当时 HEAD 至 `context_ref` 与 `work_branch: agent/codex/T-502`,重跑基线后再实现。完成时只更新本任务为 `DONE`,不提前落成或实现 T-503。
|
||||||
|
|
||||||
|
## 执行记录
|
||||||
|
|
||||||
|
- 2026-07-20:正式落成。冻结 License v1 的九字段、canonical signing bytes、严格 Ed25519/机器摘要验证与不含私钥的静态语料;决定提取并复用既有 Catalog canonical JSON,而不是复制安全解析器。明确生产 trust root、文件导入、trial/revocation/rebind 与 UI 均留给后续任务。
|
||||||
Reference in New Issue
Block a user