Implement authorization import and revocation checks (T-503)

This commit is contained in:
ila
2026-07-20 09:07:30 +08:00
parent b4453130de
commit 76f6108496
36 changed files with 2197 additions and 68 deletions
+26 -6
View File
@@ -176,6 +176,8 @@ T-102 Phase 1 原型进一步固定:
"entrypoint": "JsonParser.exe",
"working_directory": ".",
"min_os": "windows-7-sp1",
"product_id": "product-json-parser",
"supports_trial": true,
"requires_admin": false,
"files": [
{
@@ -191,7 +193,7 @@ T-102 Phase 1 原型进一步固定:
- Schema 位于 `schemas/installed-app.schema.json`;未知字段、非法 SemVer、ID/目录不匹配、非 `386|amd64` 架构、非 stable channel、共享 Windows 安全相对路径以外的文件名、大小写折叠重复路径和非法 SHA-256 均拒绝。
- `files` 必须是数组;v1 可以为空。T-302 从实际已验证、CRC/长度检查后写入 staging 的每个 payload 文件计算路径、size 和 SHA-256 并写入完整清单;可选 `files.json` 不是新的信任根,仍保留给后续修复功能。
- T-401 新安装必须写入 `entrypoint`、`working_directory`、`min_os`、`requires_admin`,其值来自已与可信 Catalog 精确比对的 `app.json`。前 3 个字段沿用共享安全相对路径/已知系统版本规则(working directory 额外允许 `.`);entrypoint 必须也是 `files` 内的普通文件。为保持 v1 可读,历史记录可缺少这些可选字段,但启动用例不得猜测默认 EXE、工作目录或最低系统,必须 fail closed。
- T-401/T-503 新安装必须写入 `entrypoint`、`working_directory`、`min_os`、`product_id`、`supports_trial`、`requires_admin`;它们均来自已经验证的 `app.json`,其中 product/trial 元数据由已验证 ZIP 携带,不能由 UI 或 Catalog 显示名称推断。前 3 个字段沿用共享安全相对路径/已知系统版本规则(working directory 额外允许 `.`);entrypoint 必须也是 `files` 内的普通文件。为保持 v1 可读,历史记录可缺少这些可选字段,但启动用例不得猜测默认 EXE、工作目录、最低系统或 product ID,必须 fail closed;`supports_trial` 仅是声明能力,绝不授予本地试用。
- 写入使用 app 目录内临时文件 + `installed-app.json.backup` 原子替换;主文件缺失时可读取中断遗留 backup,但所有读取都重新严格校验。
- SemVer 比较遵循 2.0.0:major/minor/patch 与 prerelease 参与 precedence,build metadata 不影响更新判断。
@@ -252,7 +254,7 @@ T-302 在 Switcher 的 health 阶段先运行必需的注入 health check,再
### 2.6.1 受控启动与失败码
`core/application/launch` 的请求仅接受 app ID。它从本地记录取得启动元数据,确认 `current` 与工作目录是真实受控目录、entrypoint 是 `current` 内列入 `files` 的普通非 symlink 文件,再依次检查最低系统、授权和该精确 entrypoint 的运行状态,最后调用平台启动器。请求或 UI 不得提供 EXE、路径、参数、URL 或 shell command;授权 checker 是必需注入边界,许可证策略仍由 T-501~T-503 实现。
`core/application/launch` 的请求仅接受 app ID。它从本地记录取得启动元数据,确认 `current` 与工作目录是真实受控目录、entrypoint 是 `current` 内列入 `files` 的普通非 symlink 文件,再依次检查最低系统、已验证 `product_id` 的授权和该精确 entrypoint 的运行状态,最后调用平台启动器。请求或 UI 不得提供 EXE、路径、参数、URL 或 shell command;缺少 product ID 的历史记录返回 `authorization_unavailable`,不得以 app ID、名称或 `supports_trial` 代替。
Windows 平台用 Toolhelp32 快照枚举,并以 `QueryFullProcessImageName` 的规范绝对路径作最终身份匹配;同名 EXE 只可作为查询优化,不能成为运行结论。快照/枚举/候选路径查询失败必须返回错误,不能当作“不在运行”。启动器只接收已验证的绝对 entrypoint 和工作目录:普通启动不经命令 shell 或 PATH 搜索;`requires_admin` 使用固定 `runas` Windows 动词且不传参数。非 Windows stub 对兼容、进程检测和启动均返回明确不支持错误。
@@ -327,7 +329,25 @@ License v1 只允许示例中的九个顶层字段,全部必需,禁止未知
验证器先拒绝不安全 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。
T-503 将通过验证的原始 document 限制在 `licenses/v1/<sha256>.license`:输入和每次读取均限制 1 MiB,根/子目录/文件必须是真实对象,文件名只能为 digest 形式;写入以 0600 临时文件 `Sync`/`Close` 后 rename,重复 document 幂等。未知、链接、超限或损坏缓存均 fail closed。更新、安装和自更新不得覆盖 `licenses/`。盒子和子软件都必须独立重新验证签名、machine_hash、product ID 和下面的撤销状态。
### 3.2 Revocation List v1 与离线宽限
```json
{
"schema_version": 1,
"generated_at": "2026-07-20T00:00:00Z",
"expires_at": "2026-07-27T00:00:00Z",
"revoked_license_ids": ["lic-example"],
"signature": "..."
}
```
Revocation List v1 的 Schema 为 `schemas/revocation-list.schema.json`。只允许这五个必需字段;使用与 License v1 相同的受限 canonical JSON、32 字节调用方注入授权公钥和严格标准 padded Base64 Ed25519 签名。`generated_at`/`expires_at` 是精确 UTC 秒级文本,后者必须晚于前者且间隔不超过 31 天;撤销 ID 可以为空但不得重复,格式与 License ID 相同。列表缓存为 `licenses/revocations-v1.json`,仅在验签成功后原子替换。
在调用方注入的时钟下,已列出的 License ID 立即为 `revoked`;未列出且列表未过期为 `current`;过期后(含恰好边界)最多 7 天为 `grace`;没有缓存、验签/布局失败、列表来自未来或超过宽限为 `unavailable`。`unavailable` 不能启动受保护软件,绝不以无撤销列表或网络失败作 allow-all 回退。事件、日志和 UI 只携带授权状态、machine_hash、product ID、正式/非永久与 rebind policy,不携带 license ID、JSON、签名、路径或原始机器来源。
License v1 没有到期字段:`perpetual:false` 只显示为“非永久许可证”,不是本地试用时钟。`supports_trial:true` 只表示包支持服务端试用;本仓库不创建未签名 trial、到期、换绑或申诉请求。授权页可以显示签名的 rebind policy 和说明,但不修改 machine binding。
## 4. application 事件合约(core → UI)
@@ -346,13 +366,13 @@ T-502 的 core verifier 接收调用方注入的 public key 和 expected hash;
| InstallCompleted | 原子切换成功 + 健康检查通过 | app_id, version | 状态 → installed |
| InstallRolledBack | 切换失败恢复 backup | app_id, error_code | 状态 → rollback 完成提示 |
| AppStarted / AppExited | 进程启动/退出检测 | app_id, pid | 状态 → running / installed |
| LicenseChanged | 许可证导入/撤销 | products | 授权视图刷新 |
| LicenseChanged | 授权启动加载、导入或撤销状态更新 | `state`, machine_hash, product ID、正式/非永久、rebind policy | 授权视图刷新;不得带 license ID、JSON、签名、路径或原始机器来源 |
| IconReady | 图标已完成可信加载与后台解码 | request_id, app_id, icon_ref, DPI, image.Image | 有界 relay 唤醒窗口;UI Frame 验证仍为最新请求后创建 ImageOp |
| IconFailed | 图标加载、校验或解码失败(取消不发布) | request_id, app_id, icon_ref, DPI, error_code | UI 记录诊断;仅保留同 icon_ref/DPI 的既有可信图标,否则继续占位 |
错误码为稳定英文枚举(如 `hash_mismatch`, `zip_path_escape`, `disk_full`, `app_running`, `signature_invalid`),UI 负责本地化文案。图标事件只使用 `unavailable`、`invalid_content`、`unsafe_cache`,原始网络错误只返回后台调用方/日志,不得进入 UI payload。`unsafe_cache` 详情只使用 event 中已经验证的 app ID、icon_ref 与 DPI 生成 `<digest>-<dpi>.icon` locator;不得把 cache root、原始错误或 link target 补进 event/UI。
T-616 固定 Catalog 启动事件的强类型 payload:`CatalogRefreshed` 只携带已经验签、协议校验、目标过滤并转为内存 `CatalogListItem` 的深拷贝快照,`source` 只允许 `remote` 或 `cache`;`CatalogRejected` 不携带列表、URL、路径、密钥或原始错误,只允许 `catalog_source_unconfigured`、`catalog_load_failed`。UI 必须拒绝类型、source、item 身份或 code 不匹配的 payload,且失败不得清除已成功显示的快照。当前生产 cmd 没有真实 URL/公钥/发布配置,故启动时只发布 `catalog_source_unconfigured`;不得以 testdata、环境变量、未签名本地文件或 allow-all verifier 填充目录。
T-616/T-503 固定 Catalog/Authorization 启动事件的强类型 payload:`CatalogRefreshed` 只携带已经验签、协议校验、目标过滤并转为内存 `CatalogListItem` 的深拷贝快照,`source` 只允许 `remote` 或 `cache`;`CatalogRejected` 不携带列表、URL、路径、密钥或原始错误,只允许 `catalog_source_unconfigured`、`catalog_load_failed`。`LicenseChanged` 只接受状态/产品的深拷贝净化快照;UI 必须拒绝类型、身份或 code 不匹配的 payload。当前生产 cmd 没有真实 URL/公钥/发布配置,故分别发布未配置状态;不得以 testdata、环境变量、未签名本地文件或 allow-all verifier 填充目录或授权。
## 5. CLI 参数合约
@@ -418,5 +438,5 @@ V1.1:命名管道 `\\.\pipe\softbox.<app-id>`,盒子发送 `{"command": "prepare
- 清单密钥 ID/轮换字段定稿后同步 `schemas/`。
- 错误码完整枚举表。
- 图标资源从内容哈希到下载位置/分辨率变体的发布端映射格式。
- 撤销名单的结构与宽限期时长。
- 撤销列表获取/缓存刷新协议、授权 key 轮换与发布端签发流程(Revocation List v1 结构与 7 天宽限已冻结)。
- 硬件或系统卷变化后的服务端 rebind 资格、频率和人工申诉流程(客户端 machine_hash v1 不做部分匹配或加权)。