From 94ea334d1c7b1e381de57f4618199305f56cad83 Mon Sep 17 00:00:00 2001 From: ila Date: Mon, 20 Jul 2026 00:11:56 +0800 Subject: [PATCH] Define machine fingerprint contract (T-501) --- docs/00-ai-start-here.md | 4 +-- docs/04-architecture.md | 4 +-- docs/06-tasks.md | 2 +- docs/api.md | 4 +-- docs/current-state.md | 2 +- docs/tasks/T-501.md | 57 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 docs/tasks/T-501.md diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index be2a1bd..b2fc81b 100644 --- a/docs/00-ai-start-here.md +++ b/docs/00-ai-start-here.md @@ -47,7 +47,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端, ## 当前阶段 -当前项目已完成 Phase 0~2、T-301~T-303、T-615 与审核整改 `T-604`~`T-614`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线、双适配器交互契约、`VisibleItems` 快照生命周期、双端 Gio shell 职责拆分、unsafe cache 安全诊断/runbook、ZIP 中央目录/EOCD(含 ZIP64)预扫描、安装文件/目录/journal 的代码层耐久顺序、Catalog canonicalization/签名静态 corpus,以及同句柄 Catalog size/SHA→严格 app.json→staging/switch/回滚安装链均已关闭;T-303 已将 verified-package 的 staging 前磁盘/运行状态预检和稳定失败码落实到 core,T-615 已将 ZIP 输入/staging 输出 I/O 分界、平台磁盘满分类和清理失败落实到 core。T-401 已完成完整路径进程检测、受控启动和 Switcher 临界区复查;T-402 已完成子软件更新编排;T-403 已完成受限 SoftBoxUpdater、自身 EXE 健康确认与可恢复切换;T-616 已完成 production cmd 的 Catalog 快照事件投递和明确空态诊断;T-617 已修复 prepared rename+sync 后的自更新恢复不一致并补足无头故障恢复/health flag 测试。物理断电、文件锁与杀毒软件干扰验证保留到 T-601 发布前环境验证。 +当前项目已完成 Phase 0~2、T-301~T-303、T-615 与审核整改 `T-604`~`T-617`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线、双适配器交互契约、`VisibleItems` 快照生命周期、双端 Gio shell 职责拆分、unsafe cache 安全诊断/runbook、ZIP 中央目录/EOCD(含 ZIP64)预扫描、安装文件/目录/journal 的代码层耐久顺序、Catalog canonicalization/签名静态 corpus,以及同句柄 Catalog size/SHA→严格 app.json→staging/switch/回滚安装链均已关闭;T-303 已将 verified-package 的 staging 前磁盘/运行状态预检和稳定失败码落实到 core,T-615 已将 ZIP 输入/staging 输出 I/O 分界、平台磁盘满分类和清理失败落实到 core。T-401 已完成完整路径进程检测、受控启动和 Switcher 临界区复查;T-402 已完成子软件更新编排;T-403 已完成受限 SoftBoxUpdater、自身 EXE 健康确认与可恢复切换;T-616 已完成 production cmd 的 Catalog 快照事件投递和明确空态诊断;T-617 已修复 prepared rename+sync 后的自更新恢复不一致并补足无头故障恢复/health flag 测试。T-501 已正式冻结 machine_hash v1:严格 MachineGuid 与 Windows 目录所在卷序列号构成带域分隔的 SHA-256,任一来源不可用即拒绝。物理断电、文件锁与杀毒软件干扰验证保留到 T-601 发布前环境验证。 优先路径: @@ -55,7 +55,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端, 2. 已完成 Phase 1:清单验签、ZIP 安全解压、原子切换回滚原型。 3. 已完成 Phase 2 与 T-301:清单/列表/详情/图标缓存 + 可恢复下载队列。 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 参数拒绝测试。下一步按路线图正式落成 Phase 5 的 T-501。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-601 仍须补真实 Windows 环境的断电/干扰注入。 ## 领取任务规则 diff --git a/docs/04-architecture.md b/docs/04-architecture.md index 520d179..ccfa902 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -190,7 +190,7 @@ T-613 已把该状态机的代码层耐久顺序收敛为:payload 的 CRC/长度 T-403 的盒子自更新由独立、无 Gio 的 `SoftBoxUpdater.exe` 完成。它仅接受正 PID、绝对 `/app` 和绝对 `/staging/`;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。新版仅能从自身 `/app/SoftBox.exe` 在匹配的已激活 transaction 下写最小 `/self-update-health.json` 确认;确认前旧 backup 不删除,失败优先 restore,文件锁导致 restore 失败则保留 journal/backup 而不强杀。它不提供下载、签名校验、版本选择或 UI 触发,可信 package→staging 链继续后置。 -授权:平台层采集多个稳定硬件标识 → 清洗生成 machine_hash(不保存原始序列号/MAC)→ 服务端 Ed25519 私钥签发许可证 → 客户端内置公钥离线验签;许可证与程序文件、用户配置分开保存;子软件必须独立再次验证,不能只信盒子。 +授权:平台层短暂读取严格规范化的 `MachineGuid` 与 Windows 目录所在卷序列号两个必需来源 → 用固定域分隔 SHA-256 生成 `machine_hash` v1(不保存原始 GUID、序列号或 MAC;任一来源失败即拒绝)→ 服务端 Ed25519 私钥签发许可证 → 客户端内置公钥离线验签;许可证与程序文件、用户配置分开保存;子软件必须独立再次验证,不能只信盒子。core 只实现纯 hash 算法,Windows 采集留在双端 `platform/windows`,非 Windows stub fail closed。 ## 六、关键技术难点 @@ -202,7 +202,7 @@ T-403 的盒子自更新由独立、无 Gio 的 `SoftBoxUpdater.exe` 完成。 | 双 Gio 版本 API 差异 | v0.6.0 与 v0.10.1 控件 API 不同 | 共享 ViewModel 与交互语义,Gio 适配层各自实现 | | Win7 API 兼容 | 新 API 进导入表则 Win7 无法启动 | 动态加载 + 降级;Toolhelp32/GetFileVersionInfo 等 Win7 已有 API 优先 | | 断点续传与任务恢复 | 重启后恢复未完成任务 | 任务元数据持久化 + HTTP Range;超时指数退避 | -| 机器指纹稳定性 | 硬件变动导致正版误判 | 多标识加权 + 容错 + 换绑申诉流程 | +| 机器指纹稳定性 | 系统重装/卷或设备变动导致正版误判 | v1 固定双来源、缺失即拒绝;不在客户端做部分匹配,后续按签发端 rebind/申诉流程处理 | 高风险模块先做最小原型(Phase 1),不要等整个系统搭完才验证。 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index d2fbbb6..d1cf156 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -117,7 +117,7 @@ T-617 已关闭 T-403 自更新的 `prepared` rename 后目录 sync 失败会留 | ID | 任务 | 依赖 | 验收要点 | | --- | --- | --- | --- | -| T-501 | 机器指纹(machine_hash) | T-001 | 平台层采集多标识→清洗生成 hash;不落原始序列号/MAC;非 Windows stub 可测 | +| T-501 | 机器指纹(machine_hash) | T-001 | 固定 MachineGuid + Windows 系统卷序列号的 v1 域分隔 SHA-256;任一来源失败拒绝、不落原始标识,非 Windows stub 可测 | | T-502 | Ed25519 许可证验证 | T-501 | licensing 模块离线验签;复制到不匹配机器被拒;测试用专用密钥对 | | T-503 | 授权界面与试用/导入/换绑 | T-502, T-204 | 导入、授权列表、试用状态展示;撤销名单验签与宽限期 | diff --git a/docs/api.md b/docs/api.md index 6949e6c..e89047a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -319,7 +319,7 @@ Windows 平台用 Toolhelp32 快照枚举,并以 `QueryFullProcessImageName` } ``` -- machine_hash 由平台层多个稳定硬件标识清洗生成;许可证中**不保存**原始序列号和 MAC。 +- `machine_hash` v1 固定为平台层对两个必需的瞬时来源计算的 64 字符小写 SHA-256:去首尾空白并小写化的 36 位 ASCII `MachineGuid`、以及 Windows 目录所在卷的 8 位小写十六进制序列号,以 `"softbox.machine-hash.v1\\x00" || guid || "\\x00" || volume_serial` 为输入。任一来源读取/规范化失败即拒绝,不存在单来源、MAC、加权或环境变量回退;许可证、存储、事件、UI 和日志均**不保存**原始 GUID、卷序列号或 MAC。 - 客户端用内置 Ed25519 公钥离线验签;许可证保存于 `licenses/`,与程序文件、用户配置分离;更新不得覆盖。 - 撤销名单同样签名并缓存,网络失败保留宽限期。 - 盒子负责导入/展示/管理;**子软件必须用 sdk 的 licensing 逻辑独立再验证**(签名 + machine_hash + product_id),决定正式版/试用版/授权错误。 @@ -414,4 +414,4 @@ V1.1:命名管道 `\\.\pipe\softbox.`,盒子发送 `{"command": "prepare - 错误码完整枚举表。 - 图标资源从内容哈希到下载位置/分辨率变体的发布端映射格式。 - 撤销名单的结构与宽限期时长。 -- machine_hash 的标识来源清单与加权算法(平台层内部文档)。 +- 硬件或系统卷变化后的服务端 rebind 资格、频率和人工申诉流程(客户端 machine_hash v1 不做部分匹配或加权)。 diff --git a/docs/current-state.md b/docs/current-state.md index 8920790..ffd8542 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -45,7 +45,7 @@ 任务状态以 `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。T-601 的物理断电与干扰故障注入仍保留为发布前环境验证。 +- 正在进行:无;`T-501` 已正式落成,冻结为严格 MachineGuid + Windows 系统卷序列号的 machine_hash v1,下一步领取实现。T-601 的物理断电与干扰故障注入仍保留为发布前环境验证。 ## 当前可运行内容 diff --git a/docs/tasks/T-501.md b/docs/tasks/T-501.md new file mode 100644 index 0000000..a93427b --- /dev/null +++ b/docs/tasks/T-501.md @@ -0,0 +1,57 @@ +--- +id: T-501 +title: 机器指纹 machine_hash +phase: 5 +deps: [T-001] +status: TODO +created: 2026-07-19 +issue: null +context_ref: null +claim_branch: null +work_branch: null +write_paths: + - docs/tasks/T-501.md + - core/licensing/ + - app-modern/platform/windows/ + - app-win7/platform/windows/ + - docs/00-ai-start-here.md + - docs/04-architecture.md + - docs/06-tasks.md + - docs/api.md + - docs/current-state.md +--- + +## 问题 / 背景 + +MVP 许可证必须在离线验签前可靠地比较 `machine_hash`,但仓库尚没有 `core/licensing` 或平台机器标识采集实现。`machine_hash` 必须在 modern 和 Win7 两端得到相同的稳定值;它只能是可传递给许可证的摘要,不能使原始设备标识、MAC、卷序列号或注册表值进入许可证、存储、事件、UI 或日志。 + +此前协议只说“多个稳定硬件标识清洗生成 hash”,同时又把来源和加权算法列为待定。这不足以让许可证签发端、两个客户端和后续 T-502 使用同一不可歧义的值。本任务冻结 v1 的最小双来源算法;它不假装解决硬件更换容错或换绑工作流。 + +## 方案 + +1. 在 Go 1.20 兼容且不 import Windows/Gio 的 `core/licensing` 新增纯函数 `DeriveMachineHash(machineGUID string, systemVolumeSerial uint32) (string, error)`。它只接受严格的 Windows GUID(去首尾空白后为 36 位 ASCII UUID,统一为小写)和 uint32 卷序列号;输出为 64 个小写十六进制字符:`SHA-256("softbox.machine-hash.v1\\x00" || normalized_machine_guid || "\\x00" || 8 位小写十六进制 volume_serial)`。GUID 缺失、非 ASCII、格式不合法或控制字符均返回稳定的无敏感值错误;不提供单来源、MAC 或环境变量回退。 +2. 在两端 `platform/windows` 提供同名 `MachineHash() (string, error)` composition 边界。Windows 实现仅在进程内短暂读取 `HKLM\\SOFTWARE\\Microsoft\\Cryptography` 的 `MachineGuid`,并通过 `GetWindowsDirectory` 定位 Windows 目录所在根卷,再以 Win7 已支持的 `GetVolumeInformationW` wrapper 读取卷序列号,最后调用 core 纯函数。采集或规范化失败只返回泛化 sentinel,错误文本不得包含原始标识、卷号、路径或注册表值;不写文件、不发事件、不打日志。 +3. 为非 Windows 构建提供可测试的 fail-closed stub,返回既有 `ErrUnsupported`。保留采集拼装的注入 seam,使 Linux 无头测试能覆盖“任一来源失败/非法值不泄漏且不降级”及与 core 输出相同的结果;Windows API 调用本身通过交叉编译验证,不将宿主机器的真实标识写进测试或测试输出。 +4. 固定 API、架构、路线图和项目快照中的 v1 来源、规范化、算法和隐私边界。明确 v1 要求两个来源都可用;设备或系统卷变化须由后续许可证 rebind 策略处理,不能在客户端悄悄采用加权、部分匹配或降级 hash。T-502 只消费此函数返回的 hash 并完成 Ed25519 许可证解析/验签/比对。 + +## 验收要点 + +- 纯 core 算法在 Go 1.20 可编译,对大小写/首尾空白等价 GUID 生成确定的 64 字符小写 SHA-256;非法 GUID、空值、控制字符和不同卷号均有受测的 fail-closed 结果,不存在隐式 MAC、单来源或随机回退。 +- 双端 Windows 层均使用相同的 core 算法与相同的两个来源;不修改宽泛 `Platform` 接口、不把 Windows API 带入 core,也不在静态导入表中引入 Win7 不支持 API。Windows 目录/卷查询、注册表读取或 core 拒绝时不暴露任何 raw identifier,且不产生持久化副作用。 +- Linux/非 Windows 下 `MachineHash` 稳定返回 `ErrUnsupported`;可注入的采集 seam 覆盖 GUID 读取失败、卷读取失败和非法 GUID,测试断言错误文本不含伪造 raw input。测试资料、断言、文档和执行记录均不得记录真实主机标识。 +- `go -C core vet ./...`、`go -C core test -count=1 ./...`、modern 与 Win7 的 `go test -count=1 ./...`、双端 Windows amd64 主程序构建、`./scripts/verify_phase0.ps1`、`python scripts/validate_agent_context.py` 与 `python scripts/validate_harness_governance.py` 全部通过。 + +## 边界(不改什么) + +- 不实现许可证 JSON 解析、Ed25519 验签、公钥、导入/试用/换绑 UI、撤销名单、许可证存储、服务端签发或子软件 SDK;这些属于 T-502/T-503 与仓库外服务端。 +- 不收集 MAC、BIOS/CPU/磁盘序列号、用户名、网络信息或广告标识;不持久化、显示、上传或记录 `MachineGuid`、卷序列号或其他原始标识。 +- 不修改 Catalog、下载、安装、更新、自更新、Gio Layout、数据目录或 Windows API 兼容策略;不把开发机实际机器摘要写入源码、fixtures、文档或提交。 + +## 协作约束 + +- 当前项目为单 Agent 串行模式;当前 Agent 独占全部 `write_paths`,自行完成规格、实现、测试、审查、状态更新和提交,不启动子 Agent。 +- 本规格提交后才领取任务:将本文件改为 `DOING`,记录当时 HEAD 至 `context_ref` 与 `work_branch: agent/codex/T-501`,重跑基线后再实现。完成后只更新本任务为 `DONE`,不提前落成或实现 T-502。 + +## 执行记录 + +- 2026-07-19:正式落成。冻结 machine_hash v1 的两个必需来源、严格规范化和带域分隔的 SHA-256 输入;拒绝单来源/加权回退,避免后续许可证签发端与双客户端生成不一致的摘要。确认两套现有 `x/sys/windows` 版本均提供 Win7 可用的 `GetWindowsDirectory` 和 `GetVolumeInformation`。