2026-07-20 00:11:56 +08:00
---
id : T-501
title : 机器指纹 machine_hash
phase : 5
deps : [ T-001]
2026-07-20 00:19:13 +08:00
status : DONE
2026-07-20 00:11:56 +08:00
created : 2026-07-19
issue : null
2026-07-20 00:19:13 +08:00
context_ref : 94ea334487d7180b4af0fb59c129ff678d2c4ad0
2026-07-20 00:11:56 +08:00
claim_branch : null
2026-07-20 00:19:13 +08:00
work_branch : agent/codex/T-501
2026-07-20 00:11:56 +08:00
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` 。
2026-07-20 00:19:13 +08:00
- 2026-07-19:领取任务,基于 `94ea334487d7180b4af0fb59c129ff678d2c4ad0` 在 `agent/codex/T-501` 执行;先重跑基线,再实现纯 core 算法和双端受限采集边界。
- 2026-07-19:完成。新增 Go 1.20 兼容的 `core/licensing.DeriveMachineHash` ,以严格 ASCII GUID 规范化、固定域分隔和卷序列号生成小写 SHA-256;无效输入只返回无敏感值 sentinel。双端 `platform/windows.MachineHash` 短暂读取注册表和 Windows 目录所在卷,所有采集/规范化失败均折叠为不泄漏原始值的错误;非 Windows stub 保持 `ErrUnsupported` 。局部 core/双端平台测试、完整 core/双端 app 测试和 Windows amd64 构建、`./scripts/verify_phase0.ps1` 、上下文与治理校验均通过。