diff --git a/README.md b/README.md index c64faf4e..8922757e 100644 --- a/README.md +++ b/README.md @@ -73,4 +73,6 @@ Do not reintroduce the old root `apps/`、`services/`、`packages/`、`sdks/`、 8. **提速不能绕过底线。** 身份与租户隔离、合同事实完整性、版本保护、备份恢复、迁移兼容、TLS/密钥、失败接管、正式政策及最终公众切流授权不得为赶工而取消。 9. **单一人工发布审核人。** 本轮唯一人工发布审核人为陈钰麒(总经理);技术门禁、数据库/备份/恢复核验和证据比对由系统自动完成,不再追加第二名发布审核人。60 资产+14 Prompt 只作一次批次签名批准,系统生成 74 项应用证据;Provider、公开政策、恢复与回退也只接受该审核人的具名决定,其中每次恢复或回退只接受一条 `release_owner` 批准。离线根密钥与日常审批密钥必须用途隔离且不得复用。两名普通 UAT 账号只是租户隔离测试身份,不是审核人;租户自己的业务审批策略不受本条改变。 +RC-06 审核人只使用两个无复杂参数入口:`yuqei-infra/scripts/Initialize-ReleaseSigner.cmd` 完成一次性加密密钥初始化,候选和 RC-05 材料齐备后使用 `yuqei-infra/scripts/Approve-ReleaseCandidate.cmd` 核对一页摘要并签署一次;不得手工编辑 JSON、hash 或 pin。详见 [`project-docs/RC06-single-reviewer-one-click-signing-runbook-2026-09-27.md`](project-docs/RC06-single-reviewer-one-click-signing-runbook-2026-09-27.md)。工具代码存在不代表真实密钥或批准已经形成。 + 本轮收口唯一执行顺序是 `FREEZE → BUILD → RC05-RUN → RC06-UAT → RC07-RECOVERY → RC08-PRODUCTION`。在真实证据形成前,内部受控试点和公众开放均保持 `NO_GO`;不得把本地测试、AI 自动审核或模拟证据描述为法律批准、第三方存证或公众生产放行。效率规则不构成生产操作的长期授权;生产部署、迁移、回退、切流、真实凭据或数据使用、风险接受及法律签署仍须具名授权人明确批准。 diff --git a/docs/qa/multiscene-closeout-20260910/rc05-rc08-release-blocker-closeout-20260921.md b/docs/qa/multiscene-closeout-20260910/rc05-rc08-release-blocker-closeout-20260921.md index 51860968..de4070ed 100644 --- a/docs/qa/multiscene-closeout-20260910/rc05-rc08-release-blocker-closeout-20260921.md +++ b/docs/qa/multiscene-closeout-20260910/rc05-rc08-release-blocker-closeout-20260921.md @@ -67,21 +67,21 @@ - 每场景三个自动质量字段必须显式为 true,human/legal 字段必须明确保持未完成,`not_legal_approval=true`。 - 历史 `release_eligible` 仅接受 provider succeeded、非 timeout、business succeeded/resolved。 - 冻结源码和数据传递闭包先以安全文件描述符固定到私有临时树,再从固定字节加载执行;不再执行校验后的可变工作树模块。 -- v3 只接受陈钰麒对绑定真实 60 资产、14 Prompt、31 项处置及候选/RC-05 身份的一个批次作一次 Ed25519 签名批准;审批 keyring 必须由冻结 pin 对应的离线根密钥签发,发布执行过程须由冻结 pin 对应的服务密钥签名 74 项 apply result 和在线 hash。验证器逐项核验,旧 schema、无根信任、无服务签名或其他审核人均硬拒绝。 +- approval envelope v4 / payload v3 只接受陈钰麒对绑定真实 60 资产、14 Prompt、31 项处置及候选/RC-05 身份的一个批次作一次 Ed25519 签名批准;签名同时绑定 manifest 原始文件 SHA、payload SHA、确定性单页审阅摘要 SHA、候选精确 keyring epoch 和 root 签 keyring payload SHA。审批 keyring 必须由冻结 pin 对应的离线根密钥签发,并在根签 payload 中授权该候选的精确 manifest 原始 SHA;源码只固定长期 root/apply pin 和候选精确 epoch,不把候选专属 keyring payload 回写源码形成哈希循环。任何 keyring 轮换或撤销都必须提升 epoch、作废原候选并重新冻结。发布执行过程须由冻结 pin 对应的服务密钥签名 74 项 apply result 和在线 hash。验证器逐项核验,旧 schema、无根信任、跨候选 keyring、无服务签名或其他审核人均硬拒绝。 +- 单审核人本机工具提供两个无复杂参数入口:`Initialize-ReleaseSigner.cmd` 仅首次生成加密 PKCS#8 审批密钥,`Approve-ReleaseCandidate.cmd` 自动发现唯一待审请求、展示候选/60+14+31/RC-05 摘要、要求 6 位批准码和本人口令,签后立即调用正式验证器自验。入口只选取带 Python Software Foundation 有效签名并通过密码库预检的本机离线运行时,以 `-I -S -B` 隔离启动;20 字符长句口令、私钥及锁文件 owner/DACL、原子禁止覆盖写入、过期请求自动归档、签后请求归档及重复点击复验均由工具处理。该脚本工具不宣称抵御已控制当前 Windows 账户的恶意代码;正式操作仍以干净离线主机和固定 Python/Python DLL/cryptography 闭包 digest 为外部运行环境门禁。私钥、口令、真实签名和收据不进入源码树;根密钥和 apply 服务密钥仍是用途隔离的技术设施,不是额外审核人。操作说明见 `project-docs/RC06-single-reviewer-one-click-signing-runbook-2026-09-27.md`。 ### 验证 -- Windows RC-06/RC-07 联合套件:64 passed,1 skipped,0 failed。 -- Linux `192.168.3.62`:RC-06 安全读取器符号链接回归 1/1 passed。 -- Linux 使用的最终文件 SHA-256: - - `build-rc06-publication-batch.py`: `92d1138478c16c6ea2da57715848d0727992bf922ce2c495bc36206fc0b8efa8` - - `test_rc06_publication_batch.py`: `2bccb26b9a7fd3509b1c6550eea426ac7e940db3b90c3b923204cfa57221a2e6` +- 当前开发态 Windows 定向回归:RC-06+release manifest 共 101 passed,1 个 Windows 不支持符号链接的条件用例 skipped,0 failed;这不是冻结候选的正式证据。 +- Linux `192.168.3.62` 的既有 RC-06 安全读取器符号链接回归曾为 1/1 passed;新候选形成后必须对同一不可变文件重新执行并记录正式 evidence。 +- 当前仍在 BUILD-05 源码收口,按强制交付规则不记录“最终文件 SHA”或生成不可变证据;只在唯一候选重新冻结后生成。 ### 未完成 - RC-05 正式真实矩阵终态不存在。 - 陈钰麒对 60 资产+14 Prompt 精确 hash 的一次批次签名批准尚不存在,系统据此生成的 74 项 apply receipt 和在线 hash 快照也不存在。 -- RC-06 离线根公钥、当前审批 keyring payload 和 apply 服务公钥的冻结 pin 尚未配置;在新候选冻结前验证器会保持 fail-closed。 +- RC-06 离线根公钥、候选精确 keyring epoch 和 apply 服务公钥的冻结配置,以及根签候选专属 keyring,均尚未形成;在新候选冻结和 manifest 形成前验证器会保持 fail-closed。任何 keyring 轮换或撤销都必须变更精确 epoch、作废原候选并重新冻结,旧 epoch 不可重放。 +- 一键工具已具备初始化、系统准备待审请求、单页摘要、交互签署与自验能力,但本轮未运行真实初始化或签署;当前不存在可签的正式待审请求。 - 1 个起草 Prompt+8 个复核 Prompt 的真实运行时可执行快照不存在。 - 管理员+2 名普通成员、2 个工作区的完整 API/UI UAT 未执行。 diff --git a/project-docs/RC06-single-reviewer-one-click-signing-runbook-2026-09-27.md b/project-docs/RC06-single-reviewer-one-click-signing-runbook-2026-09-27.md new file mode 100644 index 00000000..25beb8fa --- /dev/null +++ b/project-docs/RC06-single-reviewer-one-click-signing-runbook-2026-09-27.md @@ -0,0 +1,75 @@ +# RC-06 单审核人一键初始化与签署操作说明 + +更新时间:2026-09-27 + +## 1. 适用范围与当前状态 + +本工具只处理 RC-06 的一个精确发布批次:60 个合同资产、14 个 Prompt 模块和 31 项处置结论。唯一人工审核人为陈钰麒(总经理)。离线根密钥和 apply 服务密钥由系统按用途隔离管理,它们不是人工审核人。 + +当前没有生成真实审核私钥、正式签名、候选 tag、发布收据或生产授权。内部试点、UAT、生产及公众开放继续 `NO_GO`。一次 RC-06 签名不等于资产已经发布,也不等于授权公众生产切流。 + +## 2. 审核人只需要执行的两个入口 + +### 2.1 首次初始化 + +候选冻结前,由陈钰麒在受控 Windows 电脑上双击: + +`yuqei-infra\scripts\Initialize-ReleaseSigner.cmd` + +工具会: + +1. 将加密 Ed25519 审批私钥保存在当前 Windows 用户的 `%LOCALAPPDATA%\Yuqei\ReleaseSigner\keys`; +2. 要求输入并再次确认不少于 20 个字符、非纯数字且不可过于简单的长句口令; +3. 生成可交给系统使用的公钥注册材料和公钥指纹; +4. 拒绝把私钥写入源码仓库、OneDrive、网络目录、符号链接或不属于当前账户的目录,并将目录、私钥和操作锁的 owner/DACL 收紧到当前账户与 SYSTEM; +5. 重复运行时验证已有密钥,不覆盖或偷偷换钥。 + +口令只在本机遮罩输入,不通过命令行、环境变量、密码文件、聊天或日志传递。私钥不得上传仓库、服务器、邮件、聊天、共享盘或 CI artifact。 + +双击入口只选择本机已安装、带 Python Software Foundation 有效签名且具备所需密码库的离线 Python 运行时;以 `-I -S -B` 隔离模式启动,忽略 `PYTHONPATH`、user-site、`sitecustomize` 和安装器别名,不联网安装或更新运行时。若不存在合格运行时,工具会在输入口令前停止。 + +### 2.2 最终批次签署 + +只有在系统已配置长期 root/apply-service 公钥指纹和候选精确 keyring epoch、形成唯一 commit/tag/source manifest、由离线根签发绑定该 manifest 原始 SHA 的候选专属 keyring,并完成同一候选 BUILD 与 RC-05 后,系统才会在本机 signer inbox 放入唯一待审请求。届时由陈钰麒双击: + +`yuqei-infra\scripts\Approve-ReleaseCandidate.cmd` + +工具会自动校验候选、root 签名 keyring、keyring 对精确 manifest 原始 SHA 的授权、冻结 root/apply pin、epoch、时窗、60/14/31 清单及 RC-05 终态,然后显示一页中文摘要。陈钰麒只需: + +1. 核对候选 commit/tag、manifest 原始 SHA、payload SHA、60/14/31 数量和 RC-05 状态; +2. 输入摘要显示的 6 位批准码; +3. 输入本人审核密钥口令一次。 + +工具在批准码和口令输入完成后重新取得当前时间并重读全部材料,生成最长 24 小时的批准,再自动裁剪到请求、审批密钥、keyring 和全部风险接受的最早到期时间;用户不能手工填写审批时间或身份。签名写出前会调用正式 RC-06 验证器自验,并以完整 staging+禁止覆盖的原子发布写出收据。系统会先认证并归档不同候选留下的过期请求;失败、取消、错误口令、文件漂移、多个仍有效请求或 pin 未配置都不会留下正式收据。 + +成功后待审请求自动移入 `processed`,不会阻塞下一候选。重复双击会自动复验已有收据并提示“此前已签署,无需重复操作”,不会再次签名或覆盖文件。 + +## 3. 系统自动完成的工作 + +- 生成并管理与审核密钥不同的离线根密钥和 apply 服务密钥; +- 在源码冻结前固定长期 root 公钥、候选精确 keyring epoch 和 apply 服务公钥指纹;任何 keyring 轮换或撤销都必须使原候选失效并重新冻结; +- 在 manifest 形成后,用根密钥签发只包含陈钰麒审批公钥、并授权该 manifest 原始 SHA 的候选专属 trusted keyring;keyring payload 不写回源码,避免候选 commit 与 keyring 形成哈希循环; +- 生成不可变 RC-06 manifest 和待审请求; +- 用户签名后验证 approval v4 / payload v3;payload 同时绑定候选精确 keyring epoch 和 root 签 keyring payload SHA; +- 后续通过业务 API 应用 74 项内容,并由服务密钥签署 74 项机器结果和在线 hash; +- 继续验证 1 个起草+8 个审查运行时 Prompt 快照以及 RC-06 UAT。 + +## 4. 不得执行的操作 + +- 不得手工编辑 manifest、request、keyring、approval、SHA 或签名 JSON; +- 不得把 payload SHA 当作 manifest 原始文件 SHA; +- 不得在候选冻结后修改 pin 或源码; +- 不得复用审核私钥作为离线根密钥或 apply 服务密钥; +- 不得把本地初始化、自动测试或一次 RC-06 签名描述为正式政策批准、第三方存证、司法效力证明或公众生产放行。 + +## 5. 运行环境信任边界 + +`-I -S -B`、Python Software Foundation 签名检查和本机 ACL 用于减少错误运行时、环境变量注入、跨账户读取及普通文件漂移;它们不等同于受操作系统保护的代码签名发布程序,也不能抵御已经控制当前 Windows 账户、能够键盘注入或篡改用户可写 Python/密码库的恶意进程。 + +因此,正式初始化和签署必须在发布负责人确认的干净离线 Windows 主机上执行,并在候选证据中记录实际 Python、Python DLL 和 cryptography 完整导入闭包的固定 digest。若要把“当前账户已失陷”纳入防护范围,必须另行交付代码签名 EXE/MSI 并安装到受保护目录;本 CMD/PowerShell/Python 工具不得被描述为具备该防护能力。 + +## 6. 正确顺序 + +`审核密钥初始化 → 固定长期 root/apply pin 与候选精确 epoch → 唯一 commit/tag 冻结 → BUILD → RC05-RUN → manifest 形成 → 系统根签候选专属 keyring 与待审请求 → 陈钰麒一次签署 → 自动验签和 74 项应用证据 → RC06-UAT` + +若 inbox 没有请求,工具不会生成批准;若存在不同候选的旧过期请求,工具会认证后自动归档;若仍存在多个有效请求,工具必须拒绝猜测。陈钰麒不需要手工配置或删除文件。 diff --git a/yuqei-infra/scripts/Approve-ReleaseCandidate.cmd b/yuqei-infra/scripts/Approve-ReleaseCandidate.cmd new file mode 100644 index 00000000..021be4aa --- /dev/null +++ b/yuqei-infra/scripts/Approve-ReleaseCandidate.cmd @@ -0,0 +1,17 @@ +@echo off +setlocal +chcp 65001 >nul +set "launcher=%~dp0Launch-ReleaseSigner.ps1" +set "powershell=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" +if not exist "%powershell%" ( + echo 启动失败:系统缺少受信任的 Windows PowerShell。 + echo 未创建候选批准,请将此窗口截图交给系统处理。 + pause + exit /b 2 +) +"%powershell%" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%launcher%" sign +set "result=%ERRORLEVEL%" +echo. +if not "%result%"=="0" echo 签署未完成,未创建新的候选批准。 +pause +exit /b %result% diff --git a/yuqei-infra/scripts/Initialize-ReleaseSigner.cmd b/yuqei-infra/scripts/Initialize-ReleaseSigner.cmd new file mode 100644 index 00000000..731fdff6 --- /dev/null +++ b/yuqei-infra/scripts/Initialize-ReleaseSigner.cmd @@ -0,0 +1,17 @@ +@echo off +setlocal +chcp 65001 >nul +set "launcher=%~dp0Launch-ReleaseSigner.ps1" +set "powershell=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" +if not exist "%powershell%" ( + echo 启动失败:系统缺少受信任的 Windows PowerShell。 + echo 未创建审核密钥,请将此窗口截图交给系统处理。 + pause + exit /b 2 +) +"%powershell%" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%launcher%" initialize +set "result=%ERRORLEVEL%" +echo. +if not "%result%"=="0" echo 初始化未完成,未创建审核密钥或候选批准。 +pause +exit /b %result% diff --git a/yuqei-infra/scripts/Launch-ReleaseSigner.ps1 b/yuqei-infra/scripts/Launch-ReleaseSigner.ps1 new file mode 100644 index 00000000..e18095ee --- /dev/null +++ b/yuqei-infra/scripts/Launch-ReleaseSigner.ps1 @@ -0,0 +1,90 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateSet("initialize", "sign", "runtime-check")] + [string]$Command +) + +$ErrorActionPreference = "Stop" +$env:PSModulePath = @( + (Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\Modules") + (Join-Path $env:ProgramFiles "WindowsPowerShell\Modules") +) -join ";" + +function Get-TrustedPythonCandidates { + $candidatePaths = [System.Collections.Generic.List[string]]::new() + foreach ($pattern in @( + (Join-Path $env:LOCALAPPDATA "Python\pythoncore-*\python.exe") + (Join-Path $env:LOCALAPPDATA "Programs\Python\Python*\python.exe") + (Join-Path $env:ProgramFiles "Python*\python.exe") + )) { + Get-ChildItem -Path $pattern -File -ErrorAction SilentlyContinue | + ForEach-Object { $candidatePaths.Add($_.FullName) } + } + foreach ($registryRoot in @( + "Registry::HKEY_CURRENT_USER\Software\Python\PythonCore" + "Registry::HKEY_LOCAL_MACHINE\Software\Python\PythonCore" + )) { + Get-ChildItem -LiteralPath $registryRoot -ErrorAction SilentlyContinue | + ForEach-Object { + $properties = Get-ItemProperty -LiteralPath (Join-Path $_.PSPath "InstallPath") -ErrorAction SilentlyContinue + if ($properties.ExecutablePath) { + $candidatePaths.Add([string]$properties.ExecutablePath) + } elseif ($properties.'(default)') { + $candidatePaths.Add((Join-Path ([string]$properties.'(default)') "python.exe")) + } + } + } + + $trusted = foreach ($candidatePath in $candidatePaths | Sort-Object -Unique) { + if (-not (Test-Path -LiteralPath $candidatePath -PathType Leaf)) { continue } + $item = Get-Item -LiteralPath $candidatePath -Force + if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { continue } + if ($item.FullName -like "*\Microsoft\WindowsApps\*") { continue } + $version = [version]::new( + [Math]::Max(0, $item.VersionInfo.FileMajorPart), + [Math]::Max(0, $item.VersionInfo.FileMinorPart), + [Math]::Max(0, $item.VersionInfo.FileBuildPart), + [Math]::Max(0, $item.VersionInfo.FilePrivatePart) + ) + if ($version -lt [version]"3.11") { continue } + $signature = Get-AuthenticodeSignature -LiteralPath $item.FullName + if ( + $signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or + $null -eq $signature.SignerCertificate -or + $signature.SignerCertificate.Subject -notlike "*CN=Python Software Foundation*" + ) { continue } + [pscustomobject]@{ Path = $item.FullName; Version = $version } + } + return $trusted | Sort-Object Version -Descending +} + +try { + $signerScript = Join-Path $PSScriptRoot "rc06-single-reviewer-signing.py" + if (-not (Test-Path -LiteralPath $signerScript -PathType Leaf)) { + throw "The release signer program is missing" + } + Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue + Remove-Item Env:PYTHONHOME -ErrorAction SilentlyContinue + Remove-Item Env:PYTHONSTARTUP -ErrorAction SilentlyContinue + Remove-Item Env:PYTHONUSERBASE -ErrorAction SilentlyContinue + $runtime = $null + $ErrorActionPreference = "Continue" + foreach ($candidate in @(Get-TrustedPythonCandidates)) { + $preflightOutput = @(& $candidate.Path -I -S -B $signerScript runtime-check 2>&1) + if ($LASTEXITCODE -eq 0) { + $runtime = $candidate + break + } + } + if ($null -eq $runtime) { + $ErrorActionPreference = "Stop" + throw "No trusted offline Python runtime has the required cryptography dependency" + } + & $runtime.Path -I -S -B $signerScript $Command + exit $LASTEXITCODE +} catch { + Write-Host ("Signer startup failed: " + $_.Exception.Message) -ForegroundColor Red + Write-Host "No reviewer key or candidate approval was created." + exit 2 +} diff --git a/yuqei-infra/scripts/build-rc06-publication-batch.py b/yuqei-infra/scripts/build-rc06-publication-batch.py index 84913118..7c5377ba 100644 --- a/yuqei-infra/scripts/build-rc06-publication-batch.py +++ b/yuqei-infra/scripts/build-rc06-publication-batch.py @@ -31,20 +31,22 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey SCHEMA_VERSION = "yuqei.rc06-publication-batch.v3" DISPOSITION_SCHEMA_VERSION = "yuqei.rc06-golden-dispositions.v2" IMAGE_SCHEMA_VERSION = "yuqei.rc06-image-identities.v1" -APPROVAL_SCHEMA_VERSION = "yuqei.rc06-publication-batch-approval.v3" -APPROVAL_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-publication-batch-approval-payload.v1" -APPROVAL_KEYRING_SCHEMA_VERSION = "yuqei.rc06-publication-approval-keyring.v2" +APPROVAL_SCHEMA_VERSION = "yuqei.rc06-publication-batch-approval.v4" +APPROVAL_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-publication-batch-approval-payload.v3" +APPROVAL_REVIEW_SUMMARY_SCHEMA_VERSION = "yuqei.rc06-single-reviewer-summary.v1" +APPROVAL_KEYRING_SCHEMA_VERSION = "yuqei.rc06-publication-approval-keyring.v3" APPLY_SCHEMA_VERSION = "yuqei.rc06-publication-system-item-apply-receipt.v3" APPLY_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-publication-system-item-apply-payload.v1" APPROVAL_SIGNATURE_DOMAIN = b"YUQEI_RC06_PUBLICATION_BATCH_APPROVAL_V1\0" APPROVAL_KEYRING_SIGNATURE_DOMAIN = b"YUQEI_RC06_APPROVAL_KEYRING_V1\0" APPLY_SIGNATURE_DOMAIN = b"YUQEI_RC06_SYSTEM_APPLY_RECEIPT_V1\0" TRUSTED_APPROVAL_ROOT_KEY_ID = "yuqei-rc06-approval-root-v1" -# Provision these immutable pins only when the source candidate is frozen. Empty -# values deliberately keep approval/apply validation fail-closed in development. +# Provision the long-lived root/apply pins and exact candidate keyring epoch +# before source freeze. A root-signed keyring then authorizes the exact frozen +# manifest raw SHA without creating a source/keyring hash cycle. Rotation or +# revocation requires a new epoch and therefore a new frozen candidate. TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256 = "" -TRUSTED_APPROVAL_KEYRING_PAYLOAD_SHA256 = "" -MIN_APPROVAL_KEYRING_EPOCH = 0 +FROZEN_APPROVAL_KEYRING_EPOCH = 0 TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256 = "" RC05_SUMMARY_SCHEMA_VERSION = "multiscene-evaluation-run.v1" RC05_LEDGER_SCHEMA_VERSION = "multiscene-call-ledger.v1" @@ -131,6 +133,7 @@ GOLDEN_INVENTORY_PATH = ( ) FROZEN_IMPLEMENTATION_PATHS = ( "yuqei-infra/scripts/build-rc06-publication-batch.py", + "yuqei-infra/scripts/rc06-single-reviewer-signing.py", "yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/contract_templates.py", "yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/contract_scene_assets.py", "yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/template_inputs.py", @@ -2376,16 +2379,198 @@ def validate_manifest(payload: Any) -> dict[str, Any]: return manifest +def build_approval_review_summary( + payload: Any, + *, + manifest_raw_sha256: str, +) -> dict[str, Any]: + """Build the exact, human-readable projection approved by the sole reviewer.""" + + manifest = validate_manifest(payload) + candidate = manifest["candidate"] + terminal = candidate["rc05_terminal_evidence"] + manifest_raw_sha256 = _require_sha( + manifest_raw_sha256, + "publication batch raw SHA-256", + ) + summary = { + "schema_version": APPROVAL_REVIEW_SUMMARY_SCHEMA_VERSION, + "decision_scope": "approve_exact_rc06_publication_batch", + "reviewer": dict(SOLE_RELEASE_REVIEWER), + "candidate": { + "commit": candidate["commit"], + "tag": candidate["tag"], + "source_manifest_sha256": candidate["source_manifest_sha256"], + "source_manifest_payload_sha256": candidate["source_manifest_payload_sha256"], + "image_identities_sha256": candidate["image_identities_sha256"], + "image_identity_set_sha256": candidate["image_identity_set_sha256"], + "rc05_plan_sha256": candidate["rc05_plan_sha256"], + "rc05_summary_raw_sha256": terminal["summary_raw_sha256"], + "rc05_ledger_identity_raw_sha256": terminal["ledger_identity_raw_sha256"], + "rc05_campaign_identity_raw_sha256": terminal["campaign_identity_raw_sha256"], + "rc05_complete_real_green": terminal["complete_real_green"], + }, + "batch": { + "asset_count": manifest["asset_batch"]["count"], + "asset_inventory_sha256": manifest["asset_batch"]["inventory_sha256"], + "prompt_module_count": manifest["prompt_module_batch"]["count"], + "prompt_module_inventory_sha256": manifest["prompt_module_batch"]["inventory_sha256"], + "golden_disposition_count": manifest["golden_dispositions"]["count"], + "golden_disposition_counts_by_status": manifest["golden_dispositions"]["counts_by_status"], + "golden_disposition_inventory_sha256": manifest["golden_dispositions"]["inventory_sha256"], + }, + "manifest_raw_sha256": manifest_raw_sha256, + "manifest_payload_sha256": manifest["manifest_payload_sha256"], + "approval_validity_max_hours": int(MAX_APPROVAL_WINDOW.total_seconds() // 3600), + "remaining_controls_after_approval": list(BLOCKED_CONTROLS[1:]), + "claims_not_made": [ + "This approval does not itself publish any asset or Prompt module.", + "This approval does not authorize public production traffic cutover.", + "System apply evidence and executable runtime Prompt snapshots remain separate gates.", + ], + } + _assert_no_secret_material(summary, "approval review summary") + return summary + + +def _approval_risk_acceptance_deadline(manifest: Mapping[str, Any]) -> datetime | None: + """Return the earliest expiry across scoped Golden risk acceptances.""" + + deadlines = [ + _parse_time( + item["risk_acceptance"]["expires_at"], + f"Golden disposition {item['sample_id']} risk acceptance expires_at", + ) + for item in manifest["golden_dispositions"]["items"] + if item["status"] == "scoped_internal_risk_acceptance" + ] + return min(deadlines) if deadlines else None + + +def _validate_disposition_approval_window( + manifest: Mapping[str, Any], + *, + approved_at: datetime, + expires_at: datetime, +) -> None: + """Ensure every disposition decision and exception covers the approval window.""" + + for item in manifest["golden_dispositions"]["items"]: + if item["status"] == "unresolved": + raise PublicationBatchError( + "approval cannot release a batch with unresolved Golden dispositions" + ) + decided_at = _parse_time( + item["decided_at"], + f"Golden disposition {item['sample_id']} decided_at", + ) + if decided_at > approved_at: + raise PublicationBatchError( + f"Golden disposition {item['sample_id']} was decided after the approval time" + ) + if item["status"] != "scoped_internal_risk_acceptance": + continue + risk_expires_at = _parse_time( + item["risk_acceptance"]["expires_at"], + f"Golden disposition {item['sample_id']} risk acceptance expires_at", + ) + if approved_at >= risk_expires_at: + raise PublicationBatchError( + f"Golden disposition {item['sample_id']} risk acceptance expired before approval" + ) + if expires_at > risk_expires_at: + raise PublicationBatchError( + f"approval validity exceeds Golden disposition {item['sample_id']} risk acceptance" + ) + + +def build_approval_payload( + manifest: Any, + *, + manifest_raw_sha256: str, + key_id: str, + approval_keyring_epoch: int, + approval_keyring_payload_sha256: str, + approved_at: datetime, + expires_at: datetime, +) -> dict[str, Any]: + """Build the only approval payload accepted for the exact validated batch.""" + + validated = validate_manifest(manifest) + normalized_key_id = _require_string(key_id, "approval key_id", max_length=128) + if not STABLE_ID_RE.fullmatch(normalized_key_id): + raise PublicationBatchError("approval key_id is invalid") + if ( + not isinstance(approval_keyring_epoch, int) + or isinstance(approval_keyring_epoch, bool) + or approval_keyring_epoch < 1 + ): + raise PublicationBatchError("approval keyring epoch is invalid") + normalized_keyring_payload_sha256 = _require_sha( + approval_keyring_payload_sha256, + "approval keyring payload SHA-256", + ) + if approved_at.tzinfo is None or approved_at.utcoffset() is None: + raise PublicationBatchError("approval approved_at must include a timezone") + if expires_at.tzinfo is None or expires_at.utcoffset() is None: + raise PublicationBatchError("approval expires_at must include a timezone") + if expires_at <= approved_at or expires_at - approved_at > MAX_APPROVAL_WINDOW: + raise PublicationBatchError( + "approval validity must be greater than zero and no longer than seven days" + ) + if not validated["publication_control"]["golden_dispositions_complete"]: + raise PublicationBatchError("approval cannot release a batch with unresolved Golden dispositions") + _validate_disposition_approval_window( + validated, + approved_at=approved_at, + expires_at=expires_at, + ) + manifest_raw_sha256 = _require_sha( + manifest_raw_sha256, + "publication batch raw SHA-256", + ) + review_summary = build_approval_review_summary( + validated, + manifest_raw_sha256=manifest_raw_sha256, + ) + return { + "schema_version": APPROVAL_PAYLOAD_SCHEMA_VERSION, + "batch_manifest_raw_sha256": manifest_raw_sha256, + "batch_manifest_payload_sha256": validated["manifest_payload_sha256"], + "decision": "approved", + "approved_at": approved_at.isoformat(timespec="seconds"), + "expires_at": expires_at.isoformat(timespec="seconds"), + "key_id": normalized_key_id, + "approval_keyring_epoch": approval_keyring_epoch, + "approval_keyring_payload_sha256": normalized_keyring_payload_sha256, + "approver": {**SOLE_RELEASE_REVIEWER, "human_confirmed": True}, + "asset_payload_sha256s": { + item["template_id"]: item["asset_payload_sha256"] + for item in validated["asset_batch"]["items"] + }, + "prompt_module_payload_sha256s": { + item["key"]: item["module_payload_sha256"] + for item in validated["prompt_module_batch"]["items"] + }, + "golden_disposition_inventory_sha256": validated["golden_dispositions"][ + "inventory_sha256" + ], + "review_summary_sha256": _sha256_json(review_summary), + } + + def _require_sole_release_reviewer(value: Any, label: str) -> dict[str, Any]: reviewer = _require_keys( value, label, ("approver_id", "name", "role", "organization", "human_confirmed"), ) - actual = { - field: _require_string(reviewer[field], f"{label} {field}", max_length=256) - for field in ("approver_id", "name", "role", "organization") - } + actual = {} + for field in ("approver_id", "name", "role", "organization"): + normalized = _require_string(reviewer[field], f"{label} {field}", max_length=256) + if reviewer[field] != normalized: + raise PublicationBatchError(f"{label} {field} must be exact canonical text") + actual[field] = normalized if actual != SOLE_RELEASE_REVIEWER: raise PublicationBatchError(f"{label} must be the configured sole release reviewer") if reviewer["human_confirmed"] is not True: @@ -2422,7 +2607,8 @@ def _validate_approval_keyring( keyring, "approval keyring", ( - "schema_version", "root_key_id", "epoch", "issued_at", "expires_at", "keys", + "schema_version", "root_key_id", "epoch", "issued_at", "expires_at", + "authorized_batch_manifest_raw_sha256", "keys", "keyring_payload_sha256", "root_signature_base64", ), ) @@ -2431,9 +2617,14 @@ def _validate_approval_keyring( unsigned = { field: trusted[field] for field in ( - "schema_version", "root_key_id", "epoch", "issued_at", "expires_at", "keys" + "schema_version", "root_key_id", "epoch", "issued_at", "expires_at", + "authorized_batch_manifest_raw_sha256", "keys", ) } + _require_sha( + trusted["authorized_batch_manifest_raw_sha256"], + "approval keyring authorized batch manifest raw SHA-256", + ) payload_sha256 = _require_sha( trusted["keyring_payload_sha256"], "approval keyring payload SHA-256", @@ -2444,10 +2635,6 @@ def _validate_approval_keyring( raise PublicationBatchError("approval root public key pin is not provisioned") if _sha256_bytes(_public_key_raw(root_public_key)) != TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256: raise PublicationBatchError("approval root public key does not match the frozen pin") - if not HEX64_RE.fullmatch(TRUSTED_APPROVAL_KEYRING_PAYLOAD_SHA256): - raise PublicationBatchError("approval keyring payload pin is not provisioned") - if payload_sha256 != TRUSTED_APPROVAL_KEYRING_PAYLOAD_SHA256: - raise PublicationBatchError("approval keyring is not the currently trusted revocation state") if trusted["root_key_id"] != TRUSTED_APPROVAL_ROOT_KEY_ID: raise PublicationBatchError("approval keyring uses the wrong root key") epoch = trusted["epoch"] @@ -2455,13 +2642,13 @@ def _validate_approval_keyring( not isinstance(epoch, int) or isinstance(epoch, bool) or epoch < 1 - or not isinstance(MIN_APPROVAL_KEYRING_EPOCH, int) - or isinstance(MIN_APPROVAL_KEYRING_EPOCH, bool) - or MIN_APPROVAL_KEYRING_EPOCH < 1 + or not isinstance(FROZEN_APPROVAL_KEYRING_EPOCH, int) + or isinstance(FROZEN_APPROVAL_KEYRING_EPOCH, bool) + or FROZEN_APPROVAL_KEYRING_EPOCH < 1 ): raise PublicationBatchError("approval keyring epoch policy is not provisioned") - if epoch < MIN_APPROVAL_KEYRING_EPOCH: - raise PublicationBatchError("approval keyring epoch is older than the trusted minimum") + if epoch != FROZEN_APPROVAL_KEYRING_EPOCH: + raise PublicationBatchError("approval keyring epoch does not match the frozen candidate") issued_at = _parse_time(trusted["issued_at"], "approval keyring issued_at") expires_at = _parse_time(trusted["expires_at"], "approval keyring expires_at") if expires_at <= issued_at or approved_at < issued_at or approved_at >= expires_at: @@ -2535,6 +2722,7 @@ def _validate_approval_keyring( def validate_approval_receipt( manifest: Mapping[str, Any], + manifest_raw_sha256: str, receipt: Any, keyring: Any, root_public_key: Ed25519PublicKey, @@ -2550,16 +2738,24 @@ def validate_approval_receipt( envelope["payload"], "approval payload", ( - "schema_version", "batch_manifest_payload_sha256", "decision", "approved_at", - "expires_at", "key_id", "approver", "asset_payload_sha256s", + "schema_version", "batch_manifest_raw_sha256", "batch_manifest_payload_sha256", + "decision", "approved_at", "expires_at", "key_id", + "approval_keyring_epoch", "approval_keyring_payload_sha256", "approver", + "asset_payload_sha256s", "prompt_module_payload_sha256s", "golden_disposition_inventory_sha256", - "evidence_sha256", + "review_summary_sha256", ), ) if payload["schema_version"] != APPROVAL_PAYLOAD_SCHEMA_VERSION or payload["decision"] != "approved": raise PublicationBatchError("approval receipt is not an approved RC-06 batch receipt") if payload["batch_manifest_payload_sha256"] != manifest["manifest_payload_sha256"]: raise PublicationBatchError("approval receipt is not bound to this batch") + normalized_manifest_raw_sha256 = _require_sha( + manifest_raw_sha256, + "publication batch raw SHA-256", + ) + if payload["batch_manifest_raw_sha256"] != normalized_manifest_raw_sha256: + raise PublicationBatchError("approval receipt is not bound to the exact manifest file") approved_at = _parse_time(payload["approved_at"], "approval approved_at") expires_at = _parse_time(payload["expires_at"], "approval expires_at") if expires_at <= approved_at or expires_at - approved_at > MAX_APPROVAL_WINDOW: @@ -2579,15 +2775,43 @@ def validate_approval_receipt( raise PublicationBatchError("approval receipt Prompt identities drift") if payload["golden_disposition_inventory_sha256"] != manifest["golden_dispositions"]["inventory_sha256"]: raise PublicationBatchError("approval receipt Golden disposition identity drift") - _require_sha(payload["evidence_sha256"], "approval evidence SHA-256") + review_summary_sha256 = _require_sha( + payload["review_summary_sha256"], + "approval review summary SHA-256", + ) + expected_review_summary_sha256 = _sha256_json( + build_approval_review_summary( + manifest, + manifest_raw_sha256=normalized_manifest_raw_sha256, + ) + ) + if review_summary_sha256 != expected_review_summary_sha256: + raise PublicationBatchError("approval receipt does not match the exact review summary") if not manifest["publication_control"]["golden_dispositions_complete"]: raise PublicationBatchError("approval cannot release a batch with unresolved Golden dispositions") + _validate_disposition_approval_window( + manifest, + approved_at=approved_at, + expires_at=expires_at, + ) trusted_keys = _validate_approval_keyring( keyring, root_public_key=root_public_key, approved_at=approved_at, ) + if payload["approval_keyring_epoch"] != keyring["epoch"]: + raise PublicationBatchError("approval receipt keyring epoch drift") + if _require_sha( + payload["approval_keyring_payload_sha256"], + "approval receipt keyring payload SHA-256", + ) != keyring["keyring_payload_sha256"]: + raise PublicationBatchError("approval receipt keyring payload identity drift") + if keyring["authorized_batch_manifest_raw_sha256"] != normalized_manifest_raw_sha256: + raise PublicationBatchError("approval keyring does not authorize the exact batch manifest") + keyring_expires_at = _parse_time(keyring["expires_at"], "approval keyring expires_at") + if expires_at > keyring_expires_at: + raise PublicationBatchError("approval validity exceeds the trusted keyring window") key_id = _require_string(payload["key_id"], "approval key_id", max_length=128) matches = [ item @@ -2617,6 +2841,7 @@ def validate_approval_receipt( def validate_apply_receipt( manifest: Mapping[str, Any], + manifest_raw_sha256: str, approval: Mapping[str, Any], approval_keyring: Mapping[str, Any], approval_root_public_key: Ed25519PublicKey, @@ -2626,6 +2851,7 @@ def validate_apply_receipt( ) -> dict[str, Any]: validate_approval_receipt( manifest, + manifest_raw_sha256, approval, approval_keyring, approval_root_public_key, @@ -2655,7 +2881,7 @@ def validate_apply_receipt( _require_string(row["environment_id"], "apply environment_id", max_length=256) applied_at = _parse_time(row["applied_at"], "apply applied_at") approval_payload = approval["payload"] - if applied_at < _parse_time(approval_payload["approved_at"], "approval approved_at") or applied_at > _parse_time( + if applied_at < _parse_time(approval_payload["approved_at"], "approval approved_at") or applied_at >= _parse_time( approval_payload["expires_at"], "approval expires_at" ): raise PublicationBatchError("apply receipt was generated outside the approval window") @@ -2871,7 +3097,7 @@ def main(argv: list[str] | None = None) -> int: approval_parser = subparsers.add_parser("validate-approval", help="validate a separate approval receipt") approval_parser.add_argument("--manifest", type=Path, required=True) - approval_parser.add_argument("--manifest-sha256") + approval_parser.add_argument("--manifest-sha256", required=True) approval_parser.add_argument("--approval", type=Path, required=True) approval_parser.add_argument("--approval-sha256", required=True) approval_parser.add_argument("--approval-keyring", type=Path, required=True) @@ -2880,7 +3106,7 @@ def main(argv: list[str] | None = None) -> int: apply_parser = subparsers.add_parser("validate-apply", help="validate a separate apply receipt") apply_parser.add_argument("--manifest", type=Path, required=True) - apply_parser.add_argument("--manifest-sha256") + apply_parser.add_argument("--manifest-sha256", required=True) apply_parser.add_argument("--approval", type=Path, required=True) apply_parser.add_argument("--approval-sha256", required=True) apply_parser.add_argument("--approval-keyring", type=Path, required=True) @@ -2906,7 +3132,7 @@ def main(argv: list[str] | None = None) -> int: "service_writes": 0, } elif args.command == "validate-approval": - manifest, _ = _load_manifest(args.manifest, args.manifest_sha256) + manifest, manifest_raw_sha = _load_manifest(args.manifest, args.manifest_sha256) approval, raw_sha = secure_read_json(args.approval, "approval receipt", args.approval_sha256) approval_keyring, _ = secure_read_json( args.approval_keyring, @@ -2920,6 +3146,7 @@ def main(argv: list[str] | None = None) -> int: ) validate_approval_receipt( manifest, + manifest_raw_sha, approval, approval_keyring, approval_root_public_key, @@ -2933,7 +3160,7 @@ def main(argv: list[str] | None = None) -> int: "service_writes": 0, } else: - manifest, _ = _load_manifest(args.manifest, args.manifest_sha256) + manifest, manifest_raw_sha = _load_manifest(args.manifest, args.manifest_sha256) approval, approval_raw = secure_read_json( args.approval, "approval receipt", args.approval_sha256 ) @@ -2954,6 +3181,7 @@ def main(argv: list[str] | None = None) -> int: ) validate_approval_receipt( manifest, + manifest_raw_sha, approval, approval_keyring, approval_root_public_key, @@ -2963,6 +3191,7 @@ def main(argv: list[str] | None = None) -> int: ) validate_apply_receipt( manifest, + manifest_raw_sha, approval, approval_keyring, approval_root_public_key, diff --git a/yuqei-infra/scripts/build-release-candidate-manifest.py b/yuqei-infra/scripts/build-release-candidate-manifest.py index f7453c2c..66114373 100644 --- a/yuqei-infra/scripts/build-release-candidate-manifest.py +++ b/yuqei-infra/scripts/build-release-candidate-manifest.py @@ -66,6 +66,11 @@ RELEASE_CONTROL_PATHS = ( "yuqei-infra/scripts/build-installable-release.py", "yuqei-infra/scripts/build-infrastructure-image-bundle.py", "yuqei-infra/scripts/build-third-party-materials.py", + "yuqei-infra/scripts/build-rc06-publication-batch.py", + "yuqei-infra/scripts/rc06-single-reviewer-signing.py", + "yuqei-infra/scripts/Launch-ReleaseSigner.ps1", + "yuqei-infra/scripts/Initialize-ReleaseSigner.cmd", + "yuqei-infra/scripts/Approve-ReleaseCandidate.cmd", "yuqei-infra/tests/test_production_compose_preflight.py", "yuqei-infra/tests/test_production_compose_merge_gate.py", "yuqei-infra/tests/test_release_package_executor.py", @@ -76,6 +81,7 @@ RELEASE_CONTROL_PATHS = ( "yuqei-infra/tests/test_rollback_rehearsal_planner.py", "yuqei-infra/tests/test_third_party_materials.py", "yuqei-infra/tests/test_release_candidate_manifest_tools.py", + "yuqei-infra/tests/test_rc06_publication_batch.py", ) CONTRACT_LOCK_PATH = REQUIREMENT_LOCK_PATHS[0] CONTRACT_DOCKERFILE_PATH = "yuqei-infra/docker/contract-api.Dockerfile" diff --git a/yuqei-infra/scripts/build-release-worktree-ownership.py b/yuqei-infra/scripts/build-release-worktree-ownership.py index 8c631382..5efb89ba 100644 --- a/yuqei-infra/scripts/build-release-worktree-ownership.py +++ b/yuqei-infra/scripts/build-release-worktree-ownership.py @@ -31,6 +31,7 @@ CLOSEOUT_PREFIX = "docs/qa/multiscene-closeout-20260910/" MAIN_PLAN = "project-docs/AI合同平台-多场景完整交付与开发收口方案-2026-09-10.md" CURRENT_RELEASE_CONTEXT_DOCS = { "project-docs/上线收口-执行约束-2026-09-10.md", + "project-docs/RC06-single-reviewer-one-click-signing-runbook-2026-09-27.md", } HISTORICAL_PROJECT_DOCS = { "project-docs/V4-website-shell-redesign-2026-08-28.md", @@ -94,6 +95,11 @@ RUNTIME_COMPONENTS = { "yuqei-infra/scripts/build-installable-release.py", "yuqei-infra/scripts/build-infrastructure-image-bundle.py", "yuqei-infra/scripts/build-third-party-materials.py", + "yuqei-infra/scripts/build-rc06-publication-batch.py", + "yuqei-infra/scripts/rc06-single-reviewer-signing.py", + "yuqei-infra/scripts/Launch-ReleaseSigner.ps1", + "yuqei-infra/scripts/Initialize-ReleaseSigner.cmd", + "yuqei-infra/scripts/Approve-ReleaseCandidate.cmd", "yuqei-infra/scripts/build-release-candidate-manifest.py", "yuqei-infra/scripts/build-release-worktree-ownership.py", "yuqei-infra/tests/test_installable_release_assembler_hardening.py", @@ -103,6 +109,7 @@ RUNTIME_COMPONENTS = { "yuqei-infra/tests/test_third_party_materials.py", "yuqei-infra/tests/test_release_package_executor.py", "yuqei-infra/tests/test_release_candidate_manifest_tools.py", + "yuqei-infra/tests/test_rc06_publication_batch.py", "yuqei-infra/tests/test_production_compose_preflight.py", "yuqei-infra/tests/test_production_compose_merge_gate.py", "yuqei-infra/tests/test_linux_release_build_tools.py", @@ -150,6 +157,11 @@ REQUIRED_WORKTREE_ANCHORS = ( "yuqei-infra/scripts/build-installable-release.py", "yuqei-infra/scripts/build-infrastructure-image-bundle.py", "yuqei-infra/scripts/build-third-party-materials.py", + "yuqei-infra/scripts/build-rc06-publication-batch.py", + "yuqei-infra/scripts/rc06-single-reviewer-signing.py", + "yuqei-infra/scripts/Launch-ReleaseSigner.ps1", + "yuqei-infra/scripts/Initialize-ReleaseSigner.cmd", + "yuqei-infra/scripts/Approve-ReleaseCandidate.cmd", "yuqei-infra/tests/test_production_compose_preflight.py", "yuqei-infra/tests/test_production_compose_merge_gate.py", "yuqei-infra/tests/test_release_package_executor.py", @@ -160,6 +172,7 @@ REQUIRED_WORKTREE_ANCHORS = ( "yuqei-infra/tests/test_rollback_rehearsal_planner.py", "yuqei-infra/tests/test_third_party_materials.py", "yuqei-infra/tests/test_release_candidate_manifest_tools.py", + "yuqei-infra/tests/test_rc06_publication_batch.py", ) diff --git a/yuqei-infra/scripts/rc06-single-reviewer-signing.py b/yuqei-infra/scripts/rc06-single-reviewer-signing.py new file mode 100644 index 00000000..a6a30ad0 --- /dev/null +++ b/yuqei-infra/scripts/rc06-single-reviewer-signing.py @@ -0,0 +1,1910 @@ +#!/usr/bin/env python3 +"""Initialize and use the sole RC-06 release-reviewer signing key. + +The two user-facing commands are intentionally parameter-free. ``initialize`` +creates one encrypted reviewer key under the current user's local application +data directory. ``sign`` discovers one prepared request in the signer inbox, +shows the exact batch summary, asks for one confirmation code and the private +key passphrase, signs, and immediately verifies the receipt. + +No command publishes assets, changes Git, calls a model, or opens production. +""" + +from __future__ import annotations + +import argparse +import base64 +import csv +import getpass +import importlib.util +import json +import os +import stat +import subprocess +import sys +import sysconfig +import tempfile +from contextlib import contextmanager +from datetime import UTC, datetime, timedelta +from pathlib import Path +from typing import Any, Iterator, Mapping + +def _bootstrap_isolated_site_packages() -> tuple[Path, ...]: + """Load only the interpreter's exact site-packages path under ``-S``.""" + + if not (sys.flags.isolated and sys.flags.no_site): + return () + base = Path(sys.base_prefix).resolve(strict=True) + allowed: list[Path] = [] + for name in ("purelib", "platlib"): + configured = Path(sysconfig.get_path(name)).resolve(strict=True) + if not _is_relative_to_bootstrap(configured, base): + raise RuntimeError("isolated Python package path escapes the selected runtime") + if configured not in allowed: + sys.path.append(os.fspath(configured)) + allowed.append(configured) + return tuple(allowed) + + +def _is_relative_to_bootstrap(path: Path, parent: Path) -> bool: + try: + path.relative_to(parent) + return True + except ValueError: + return False + + +ISOLATED_SITE_PACKAGES = _bootstrap_isolated_site_packages() + +import cryptography +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + Ed25519PublicKey, +) + + +REPO_ROOT = Path(__file__).resolve().parents[2] +BATCH_SCRIPT = Path(__file__).with_name("build-rc06-publication-batch.py") +SPEC = importlib.util.spec_from_file_location("yuqei_rc06_publication_batch", BATCH_SCRIPT) +if SPEC is None or SPEC.loader is None: # pragma: no cover - import platform failure + raise RuntimeError("cannot load the RC-06 publication verifier") +batch = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(batch) + + +KEY_BUNDLE_SCHEMA_VERSION = "yuqei.rc06-reviewer-key-bundle.v1" +KEY_REGISTRATION_SCHEMA_VERSION = "yuqei.rc06-reviewer-key-registration.v1" +REVIEW_REQUEST_SCHEMA_VERSION = "yuqei.rc06-release-review-request.v1" +SIGNING_RESULT_SCHEMA_VERSION = "yuqei.rc06-single-reviewer-signing-result.v1" +PRIVATE_KEY_FILENAME = "reviewer-approval-private.pem" # pragma: allowlist secret +PUBLIC_KEY_FILENAME = "reviewer-approval-public.pem" +KEY_BUNDLE_FILENAME = "reviewer-key-bundle.json" +REGISTRATION_FILENAME = "reviewer-registration.json" +REQUEST_FILENAME = "release-review-request.json" +MANIFEST_FILENAME = "rc06-publication-batch.json" +KEYRING_FILENAME = "rc06-approval-keyring.json" +ROOT_PUBLIC_KEY_FILENAME = "rc06-approval-root-public-key.raw" +APPROVAL_FILENAME = "rc06-batch-approval.json" +APPROVAL_SHA_FILENAME = "rc06-batch-approval.sha256" +SUMMARY_JSON_FILENAME = "rc06-review-summary.json" +SUMMARY_TEXT_FILENAME = "rc06-review-summary.txt" +SIGNING_RESULT_FILENAME = "signing-result.json" +BUNDLE_COMPLETE_FILENAME = ".complete.json" +BUNDLE_COMPLETE_SCHEMA_VERSION = "yuqei.secure-local-bundle-complete.v1" +DEFAULT_APPROVAL_WINDOW = timedelta(hours=24) +MAX_REQUEST_WINDOW = timedelta(days=7) +MAX_SECRET_BYTES = 64 * 1024 +MIN_PASSPHRASE_CHARACTERS = 20 +COMMON_WEAK_PASSPHRASES = { + "12345678901234567890", + "abcdefghijklmnopqrst", + "passwordpasswordpassword", + "qwertyuiopqwertyuiop", +} +COMMON_WEAK_PASSPHRASE_FRAGMENTS = ("password", "qwerty", "123456", "letmein") + + +class SignerError(RuntimeError): + """A fail-closed, user-safe signing error.""" + + +def _now_utc() -> datetime: + return datetime.now(UTC).replace(microsecond=0) + + +def _format_time(value: datetime) -> str: + if value.tzinfo is None or value.utcoffset() is None: + raise SignerError("time value must include a timezone") + return value.astimezone(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + +def _parse_time(value: Any, label: str) -> datetime: + try: + parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00")) + except ValueError as exc: + raise SignerError(f"{label} is not a valid timestamp") from exc + if parsed.tzinfo is None or parsed.utcoffset() is None: + raise SignerError(f"{label} must include a timezone") + return parsed.astimezone(UTC) + + +def _default_state_directory() -> Path: + if os.name == "nt": + base = os.environ.get("LOCALAPPDATA") + if not base: + raise SignerError("LOCALAPPDATA is unavailable; signer storage cannot be selected safely") + return Path(base) / "Yuqei" / "ReleaseSigner" + return Path.home() / ".local" / "share" / "yuqei" / "release-signer" + + +def _assert_trusted_runtime() -> None: + required_flags = ( + sys.flags.isolated, + sys.flags.ignore_environment, + sys.flags.no_user_site, + sys.flags.no_site, + sys.flags.safe_path, + sys.flags.dont_write_bytecode, + ) + if not all(required_flags): + raise SignerError("签署工具必须由随附的隔离启动器运行,请勿直接调用 Python") + if sys.version_info < (3, 11): + raise SignerError("签署工具需要 Python 3.11 或更高版本") + if not ISOLATED_SITE_PACKAGES: + raise SignerError("隔离 Python 依赖目录未建立") + cryptography_file = Path(cryptography.__file__).resolve(strict=True) + if not any(_is_relative_to(cryptography_file, root) for root in ISOLATED_SITE_PACKAGES): + raise SignerError("cryptography 未从所选隔离运行时加载") + try: + cryptography_major = int(cryptography.__version__.split(".", 1)[0]) + except (AttributeError, ValueError) as exc: + raise SignerError("无法确认 cryptography 版本") from exc + if cryptography_major < 44: + raise SignerError("签署运行时的 cryptography 版本过旧") + runtime_root = Path(sys.base_prefix).resolve(strict=True) + for entry in sys.path: + if not entry: + raise SignerError("隔离 Python 搜索路径包含当前目录") + resolved = Path(entry).resolve(strict=False) + if not _is_relative_to(resolved, runtime_root): + raise SignerError("隔离 Python 搜索路径包含未受信目录") + + +def _is_relative_to(path: Path, parent: Path) -> bool: + try: + path.relative_to(parent) + return True + except ValueError: + return False + + +def _assert_local_safe_path(path: Path, label: str, *, allow_existing: bool) -> Path: + if not path.is_absolute(): + raise SignerError(f"{label} must be an absolute path") + text = os.fspath(path) + if os.name == "nt" and text.startswith(("\\\\", "//")): + raise SignerError(f"{label} must not use a network path") + parent = path if path.exists() else path.parent + try: + batch._assert_no_symlink_components(parent, label) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + resolved = path.resolve(strict=False) + if _is_relative_to(resolved, REPO_ROOT.resolve(strict=True)): + raise SignerError(f"{label} must be outside the source repository") + for variable in ("OneDrive", "OneDriveConsumer", "OneDriveCommercial"): + configured = os.environ.get(variable) + if configured and _is_relative_to(resolved, Path(configured).resolve(strict=False)): + raise SignerError(f"{label} must not be stored in a synchronized OneDrive folder") + if not allow_existing and (path.exists() or path.is_symlink()): + raise SignerError(f"{label} already exists; refusing to overwrite") + return resolved + + +def _windows_current_sid() -> str: + system_root = Path(os.environ.get("SystemRoot", r"C:\Windows")) + whoami = system_root / "System32" / "whoami.exe" + if not whoami.is_file(): + raise SignerError("cannot locate the trusted Windows account identity tool") + completed = subprocess.run( + [os.fspath(whoami), "/user", "/fo", "csv", "/nh"], + check=False, + capture_output=True, + text=True, + timeout=15, + ) + if completed.returncode: + raise SignerError("cannot resolve the current Windows account SID") + try: + row = next(csv.reader([completed.stdout.strip()])) + except (StopIteration, csv.Error) as exc: + raise SignerError("cannot parse the current Windows account SID") from exc + if len(row) < 2 or not row[1].startswith("S-1-"): + raise SignerError("cannot parse the current Windows account SID") + return row[1] + + +def _harden_private_directory(path: Path) -> None: + if os.name == "nt": + sid = _windows_current_sid() + system_root = Path(os.environ.get("SystemRoot", r"C:\Windows")) + powershell = system_root / "System32" / "WindowsPowerShell" / "v1.0" / "powershell.exe" + if not powershell.is_file(): + raise SignerError("cannot locate the trusted Windows ACL tool") + acl_script = r""" +$ErrorActionPreference = 'Stop' +$target = [Environment]::GetEnvironmentVariable('YUQEI_SIGNER_ACL_TARGET', 'Process') +$userSidText = [Environment]::GetEnvironmentVariable('YUQEI_SIGNER_ACL_SID', 'Process') +if ([string]::IsNullOrWhiteSpace($target) -or [string]::IsNullOrWhiteSpace($userSidText)) { + throw 'missing ACL target' +} +$userSid = New-Object System.Security.Principal.SecurityIdentifier($userSidText) +$systemSid = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-18') +$acl = New-Object System.Security.AccessControl.DirectorySecurity +$acl.SetAccessRuleProtection($true, $false) +$inheritance = [System.Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit' +$propagation = [System.Security.AccessControl.PropagationFlags]::None +$allow = [System.Security.AccessControl.AccessControlType]::Allow +$rights = [System.Security.AccessControl.FileSystemRights]::FullControl +$acl.SetOwner($userSid) +$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($userSid, $rights, $inheritance, $propagation, $allow))) +$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($systemSid, $rights, $inheritance, $propagation, $allow))) +[System.IO.Directory]::SetAccessControl($target, $acl) +$verified = [System.IO.Directory]::GetAccessControl($target) +$allowedSids = @($userSidText, 'S-1-5-18') +if (-not $verified.AreAccessRulesProtected) { throw 'ACL inheritance remains enabled' } +if ($verified.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $userSidText) { + throw 'directory owner is not the current account' +} +foreach ($rule in $verified.Access) { + $ruleSid = $rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value + if ($rule.AccessControlType -ne $allow -or $allowedSids -notcontains $ruleSid) { + throw ('unauthorized ACL entry: ' + $ruleSid) + } +} +foreach ($requiredSid in $allowedSids) { + if (-not ($verified.Access | Where-Object { + $_.AccessControlType -eq $allow -and + $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $requiredSid -and + ($_.FileSystemRights -band $rights) -eq $rights + })) { throw ('required ACL entry is missing: ' + $requiredSid) } +} +Write-Output 'ACL_OK' +""" + encoded = base64.b64encode(acl_script.encode("utf-16-le")).decode("ascii") + environment = os.environ.copy() + environment["YUQEI_SIGNER_ACL_TARGET"] = os.fspath(path) + environment["YUQEI_SIGNER_ACL_SID"] = sid + environment["PSModulePath"] = os.pathsep.join( + [ + os.fspath(system_root / "System32" / "WindowsPowerShell" / "v1.0" / "Modules"), + os.fspath(Path(os.environ.get("ProgramFiles", r"C:\Program Files")) / "WindowsPowerShell" / "Modules"), + ] + ) + completed = subprocess.run( + [ + os.fspath(powershell), + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-EncodedCommand", + encoded, + ], + check=False, + capture_output=True, + text=True, + timeout=30, + env=environment, + ) + if completed.returncode or "ACL_OK" not in completed.stdout: + raise SignerError("cannot restrict the signer key directory to the current account") + else: + path.chmod(0o700) + current = path.stat() + if current.st_uid != os.getuid() or stat.S_IMODE(current.st_mode) != 0o700: + raise SignerError("signer key directory permissions are not private") + + +def _harden_private_file(path: Path) -> None: + if os.name == "nt": + sid = _windows_current_sid() + system_root = Path(os.environ.get("SystemRoot", r"C:\Windows")) + powershell = system_root / "System32" / "WindowsPowerShell" / "v1.0" / "powershell.exe" + if not powershell.is_file(): + raise SignerError("cannot locate the trusted Windows ACL tool") + acl_script = r""" +$ErrorActionPreference = 'Stop' +$target = [Environment]::GetEnvironmentVariable('YUQEI_SIGNER_ACL_TARGET', 'Process') +$userSidText = [Environment]::GetEnvironmentVariable('YUQEI_SIGNER_ACL_SID', 'Process') +if ([string]::IsNullOrWhiteSpace($target) -or [string]::IsNullOrWhiteSpace($userSidText)) { + throw 'missing ACL target' +} +$userSid = New-Object System.Security.Principal.SecurityIdentifier($userSidText) +$systemSid = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-18') +$acl = New-Object System.Security.AccessControl.FileSecurity +$acl.SetAccessRuleProtection($true, $false) +$inheritance = [System.Security.AccessControl.InheritanceFlags]::None +$propagation = [System.Security.AccessControl.PropagationFlags]::None +$allow = [System.Security.AccessControl.AccessControlType]::Allow +$rights = [System.Security.AccessControl.FileSystemRights]::FullControl +$acl.SetOwner($userSid) +$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($userSid, $rights, $inheritance, $propagation, $allow))) +$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($systemSid, $rights, $inheritance, $propagation, $allow))) +[System.IO.File]::SetAccessControl($target, $acl) +$verified = [System.IO.File]::GetAccessControl($target) +$allowedSids = @($userSidText, 'S-1-5-18') +if (-not $verified.AreAccessRulesProtected) { throw 'ACL inheritance remains enabled' } +if ($verified.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $userSidText) { + throw 'file owner is not the current account' +} +foreach ($rule in $verified.Access) { + $ruleSid = $rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value + if ($rule.AccessControlType -ne $allow -or $allowedSids -notcontains $ruleSid) { + throw ('unauthorized ACL entry: ' + $ruleSid) + } +} +foreach ($requiredSid in $allowedSids) { + if (-not ($verified.Access | Where-Object { + $_.AccessControlType -eq $allow -and + $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $requiredSid -and + ($_.FileSystemRights -band $rights) -eq $rights + })) { throw ('required ACL entry is missing: ' + $requiredSid) } +} +Write-Output 'ACL_OK' +""" + encoded = base64.b64encode(acl_script.encode("utf-16-le")).decode("ascii") + environment = os.environ.copy() + environment["YUQEI_SIGNER_ACL_TARGET"] = os.fspath(path) + environment["YUQEI_SIGNER_ACL_SID"] = sid + environment["PSModulePath"] = os.pathsep.join( + [ + os.fspath(system_root / "System32" / "WindowsPowerShell" / "v1.0" / "Modules"), + os.fspath(Path(os.environ.get("ProgramFiles", r"C:\Program Files")) / "WindowsPowerShell" / "Modules"), + ] + ) + completed = subprocess.run( + [ + os.fspath(powershell), + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-EncodedCommand", + encoded, + ], + check=False, + capture_output=True, + text=True, + timeout=30, + env=environment, + ) + if completed.returncode or "ACL_OK" not in completed.stdout: + raise SignerError("cannot restrict the signer private key to the current account") + else: + path.chmod(0o600) + current = path.stat() + if current.st_uid != os.getuid() or stat.S_IMODE(current.st_mode) != 0o600: + raise SignerError("signer private key permissions are not private") + + +def _ensure_private_directory(path: Path, label: str) -> Path: + path = _assert_local_safe_path(path, label, allow_existing=True) + try: + path.mkdir(mode=0o700, parents=True, exist_ok=True) + except OSError as exc: + raise SignerError(f"cannot create {label}: {exc}") from exc + try: + batch._assert_no_symlink_components(path, label) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + if not path.is_dir() or path.is_symlink(): + raise SignerError(f"{label} must be a real local directory") + _harden_private_directory(path) + return path + + +@contextmanager +def _state_operation_lock(state_directory: Path) -> Iterator[None]: + state_directory = _ensure_private_directory(state_directory, "signer state directory") + lock_path = state_directory / ".operation.lock" + flags = os.O_RDWR | os.O_CREAT | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0) + descriptor: int | None = None + locked = False + try: + descriptor = os.open(lock_path, flags, 0o600) + opened = os.fstat(descriptor) + named = os.lstat(lock_path) + if ( + not stat.S_ISREG(opened.st_mode) + or not os.path.samestat(opened, named) + or opened.st_nlink != 1 + ): + raise SignerError("signer operation lock is not a stable private file") + if os.name != "nt": + if opened.st_uid != os.getuid(): + raise SignerError("signer operation lock is not owned by the current account") + os.fchmod(descriptor, 0o600) + import fcntl + + try: + fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError as exc: + raise SignerError("另一个审核操作正在进行,请关闭重复窗口后重试") from exc + else: + _harden_private_file(lock_path) + import msvcrt + + if opened.st_size == 0: + os.write(descriptor, b"0") + os.fsync(descriptor) + os.lseek(descriptor, 0, os.SEEK_SET) + try: + msvcrt.locking(descriptor, msvcrt.LK_NBLCK, 1) + except OSError as exc: + raise SignerError("另一个审核操作正在进行,请关闭重复窗口后重试") from exc + locked = True + yield + except OSError as exc: + raise SignerError(f"cannot secure the signer operation lock: {exc}") from exc + finally: + if descriptor is not None: + if locked: + try: + if os.name == "nt": + import msvcrt + + os.lseek(descriptor, 0, os.SEEK_SET) + msvcrt.locking(descriptor, msvcrt.LK_UNLCK, 1) + else: + import fcntl + + fcntl.flock(descriptor, fcntl.LOCK_UN) + except OSError: + pass + os.close(descriptor) + + +def _write_exclusive(path: Path, raw: bytes, label: str, *, mode: int = 0o600) -> None: + if path.exists() or path.is_symlink(): + raise SignerError(f"{label} already exists; refusing to overwrite") + try: + batch._assert_no_symlink_components(path.parent, f"{label} parent") + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0) + flags |= getattr(os, "O_NOFOLLOW", 0) + descriptor: int | None = None + try: + descriptor = os.open(path, flags, mode) + offset = 0 + while offset < len(raw): + written = os.write(descriptor, raw[offset:]) + if written <= 0: + raise SignerError(f"cannot write complete {label}") + offset += written + os.fsync(descriptor) + opened = os.fstat(descriptor) + named = os.lstat(path) + if not stat.S_ISREG(opened.st_mode) or not os.path.samestat(opened, named): + raise SignerError(f"{label} is not a stable regular file") + except OSError as exc: + raise SignerError(f"cannot write {label}: {exc}") from exc + finally: + if descriptor is not None: + os.close(descriptor) + if os.name != "nt": + path.chmod(mode) + + +def _bundle_completion(files: Mapping[str, bytes]) -> bytes: + return _canonical_json( + { + "schema_version": BUNDLE_COMPLETE_SCHEMA_VERSION, + "files": [ + { + "name": name, + "size": len(raw), + "sha256": batch._sha256_bytes(raw), + } + for name, raw in sorted(files.items()) + ], + } + ) + + +def _verify_complete_bundle(directory: Path, expected_names: set[str], label: str) -> None: + try: + batch._assert_no_symlink_components(directory, label) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + if not directory.is_dir() or directory.is_symlink(): + raise SignerError(f"{label} is not a stable directory") + try: + actual_names = {child.name for child in directory.iterdir()} + except OSError as exc: + raise SignerError(f"cannot inspect {label}: {exc}") from exc + required_names = expected_names | {BUNDLE_COMPLETE_FILENAME} + if actual_names != required_names: + raise SignerError(f"{label} is incomplete or contains unexpected files") + try: + complete, _ = batch.secure_read_json( + directory / BUNDLE_COMPLETE_FILENAME, + f"{label} completion record", + ) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + if not isinstance(complete, dict) or set(complete) != {"schema_version", "files"}: + raise SignerError(f"{label} completion record has an invalid shape") + if complete["schema_version"] != BUNDLE_COMPLETE_SCHEMA_VERSION: + raise SignerError(f"{label} completion record schema is unsupported") + entries = complete["files"] + if not isinstance(entries, list) or len(entries) != len(expected_names): + raise SignerError(f"{label} completion record is incomplete") + seen: set[str] = set() + for entry in entries: + if not isinstance(entry, dict) or set(entry) != {"name", "size", "sha256"}: + raise SignerError(f"{label} completion file entry is invalid") + name = entry["name"] + if name not in expected_names or name in seen: + raise SignerError(f"{label} completion file inventory drift") + seen.add(name) + if not isinstance(entry["size"], int) or entry["size"] < 0: + raise SignerError(f"{label} completion file size is invalid") + try: + expected_sha = batch._require_sha(entry["sha256"], f"{label} {name} SHA-256") + raw = batch.secure_read_bytes( + directory / name, + f"{label} {name}", + maximum_bytes=max(MAX_SECRET_BYTES, entry["size"]), + ) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + if len(raw) != entry["size"] or batch._sha256_bytes(raw) != expected_sha: + raise SignerError(f"{label} file hash or size drift: {name}") + if seen != expected_names: + raise SignerError(f"{label} completion file inventory is incomplete") + + +def _assert_bundle_matches_files( + directory: Path, + expected_files: Mapping[str, bytes], + label: str, +) -> None: + _verify_complete_bundle(directory, set(expected_files), label) + for name, expected_raw in expected_files.items(): + try: + actual_raw = batch.secure_read_bytes( + directory / name, + f"{label} {name}", + maximum_bytes=max(MAX_SECRET_BYTES, len(expected_raw)), + ) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + if actual_raw != expected_raw: + raise SignerError(f"{label} conflicts with the exact prepared request: {name}") + + +def _assert_existing_request_matches_materials( + directory: Path, + expected_request: Mapping[str, Any], + expected_materials: Mapping[str, bytes], + label: str, +) -> dict[str, Any]: + expected_names = set(expected_materials) | {REQUEST_FILENAME} + _verify_complete_bundle(directory, expected_names, label) + for name, expected_raw in expected_materials.items(): + try: + actual_raw = batch.secure_read_bytes( + directory / name, + f"{label} {name}", + maximum_bytes=max(MAX_SECRET_BYTES, len(expected_raw)), + ) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + if actual_raw != expected_raw: + raise SignerError(f"{label} conflicts with the exact prepared material: {name}") + existing_request, _ = _load_review_request(directory / REQUEST_FILENAME) + existing_identity = { + key: value + for key, value in existing_request.items() + if key not in {"created_at", "expires_at"} + } + expected_identity = { + key: value + for key, value in expected_request.items() + if key not in {"created_at", "expires_at"} + } + if existing_identity != expected_identity: + raise SignerError(f"{label} request identity conflicts with the exact candidate") + return existing_request + + +def _write_bundle_atomically( + destination: Path, + files: Mapping[str, bytes], + *, + private_directory: bool, +) -> None: + destination = _assert_local_safe_path( + destination, + "output directory", + allow_existing=False, + ) + parent = _ensure_private_directory(destination.parent, "output parent") + temporary: Path | None = None + try: + temporary = Path(tempfile.mkdtemp(prefix=f".{destination.name}.", dir=parent)) + _harden_private_directory(temporary) + for name, raw in sorted(files.items()): + if Path(name).name != name: + raise SignerError("bundle output filename is unsafe") + _write_exclusive(temporary / name, raw, name) + if name == PRIVATE_KEY_FILENAME: + _harden_private_file(temporary / name) + _write_exclusive( + temporary / BUNDLE_COMPLETE_FILENAME, + _bundle_completion(files), + "bundle completion record", + ) + _verify_complete_bundle(temporary, set(files), "completed staging bundle") + _publish_directory_no_replace(temporary, destination) + temporary = None + _verify_complete_bundle(destination, set(files), "completed output bundle") + except OSError as exc: + raise SignerError(f"cannot complete output bundle: {exc}") from exc + finally: + if temporary is not None: + _remove_owned_staging_bundle(temporary, set(files) | {BUNDLE_COMPLETE_FILENAME}) + + +def _publish_directory_no_replace(source: Path, destination: Path) -> None: + """Publish one complete directory without replacing an existing name.""" + + if os.name == "nt": + try: + os.rename(source, destination) + except FileExistsError as exc: + raise SignerError("output directory already exists; refusing to overwrite") from exc + except OSError as exc: + # Windows reports an existing directory as either EEXIST or + # EACCES depending on the filesystem/provider. + if destination.exists() or destination.is_symlink(): + raise SignerError("output directory already exists; refusing to overwrite") from exc + raise + return + if sys.platform.startswith("linux"): + import ctypes + import errno + + library = ctypes.CDLL(None, use_errno=True) + renameat2 = getattr(library, "renameat2", None) + if renameat2 is None: + raise SignerError("this Linux runtime cannot publish a bundle without replacement") + renameat2.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint] + renameat2.restype = ctypes.c_int + result = renameat2( + -100, + os.fsencode(source), + -100, + os.fsencode(destination), + 1, + ) + if result != 0: + error = ctypes.get_errno() + if error == errno.EEXIST: + raise SignerError("output directory already exists; refusing to overwrite") + raise SignerError(f"cannot publish completed output bundle: {os.strerror(error)}") + return + raise SignerError("this platform cannot publish a bundle without replacement") + + +def _remove_owned_staging_bundle(directory: Path, expected_names: set[str]) -> None: + """Remove only this writer's known regular staging files; never recurse.""" + + try: + children = list(directory.iterdir()) + if {child.name for child in children} - expected_names: + return + for child in children: + current = child.lstat() + if not stat.S_ISREG(current.st_mode) or child.is_symlink(): + return + for child in children: + child.unlink() + directory.rmdir() + except OSError: + # An unexpected object is left for manual inspection instead of being + # followed or recursively deleted. + return + + +def _validate_passphrase(passphrase: str) -> bytes: + if not isinstance(passphrase, str) or len(passphrase) < MIN_PASSPHRASE_CHARACTERS: + raise SignerError( + f"signer passphrase must contain at least {MIN_PASSPHRASE_CHARACTERS} characters" + ) + if len(passphrase) > 1024 or "\0" in passphrase or "\n" in passphrase or "\r" in passphrase: + raise SignerError("signer passphrase contains unsupported characters") + normalized = passphrase.strip() + if len(normalized) < MIN_PASSPHRASE_CHARACTERS: + raise SignerError("signer passphrase must not rely on leading or trailing whitespace") + folded = normalized.casefold() + repeated_short_pattern = any( + len(folded) % width == 0 + and folded == folded[:width] * (len(folded) // width) + for width in range(1, min(8, len(folded) // 2) + 1) + ) + if ( + len(set(folded)) < 4 + or folded in COMMON_WEAK_PASSPHRASES + or folded.isdecimal() + or repeated_short_pattern + or any(fragment in folded for fragment in COMMON_WEAK_PASSPHRASE_FRAGMENTS) + ): + raise SignerError("signer passphrase is too predictable; use a long private phrase") + return passphrase.encode("utf-8") + + +def _encode_existing_passphrase(passphrase: str) -> bytes: + if ( + not isinstance(passphrase, str) + or not passphrase + or len(passphrase) > 1024 + or "\0" in passphrase + or "\n" in passphrase + or "\r" in passphrase + ): + raise SignerError("reviewer private key password is incorrect or unsupported") + return passphrase.encode("utf-8") + + +def _read_secret_file(path: Path, label: str) -> bytes: + try: + before = path.lstat() + except OSError as exc: + raise SignerError(f"cannot inspect {label}: {exc}") from exc + if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1: + raise SignerError(f"{label} must be one regular, non-linked file") + _harden_private_file(path) + try: + raw = batch.secure_read_bytes(path, label, maximum_bytes=MAX_SECRET_BYTES) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + current = path.stat() + if current.st_nlink != 1 or not os.path.samestat(before, current): + raise SignerError(f"{label} must not be hard linked") + if os.name != "nt": + if current.st_uid != os.getuid(): + raise SignerError(f"{label} must be owned by the current account") + if stat.S_IMODE(current.st_mode) & 0o077: + raise SignerError(f"{label} permissions are too broad") + return raw + + +def _canonical_json(value: Any) -> bytes: + return batch._canonical(value) + + +def _public_raw(public_key: Ed25519PublicKey) -> bytes: + return public_key.public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw, + ) + + +def _load_public_registration(key_directory: Path) -> tuple[dict[str, Any], Ed25519PublicKey]: + key_directory = _assert_local_safe_path( + key_directory, + "reviewer key directory", + allow_existing=True, + ) + if not key_directory.is_dir(): + raise SignerError("reviewer key directory is missing") + _verify_complete_bundle( + key_directory, + { + PRIVATE_KEY_FILENAME, + PUBLIC_KEY_FILENAME, + REGISTRATION_FILENAME, + KEY_BUNDLE_FILENAME, + }, + "reviewer key bundle", + ) + try: + registration, _ = batch.secure_read_json( + key_directory / REGISTRATION_FILENAME, + "reviewer key registration", + ) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + expected_keys = { + "schema_version", + "key_id", + "algorithm", + "reviewer", + "created_at", + "public_key_pem", + "public_key_raw_sha256", + "public_key_pem_sha256", + } + if not isinstance(registration, dict) or set(registration) != expected_keys: + raise SignerError("reviewer key registration has an invalid shape") + if registration["schema_version"] != KEY_REGISTRATION_SCHEMA_VERSION: + raise SignerError("reviewer key registration schema is unsupported") + if registration["algorithm"] != "Ed25519": + raise SignerError("reviewer key algorithm is unsupported") + if registration["reviewer"] != batch.SOLE_RELEASE_REVIEWER: + raise SignerError("reviewer key does not belong to the configured sole reviewer") + public_path = key_directory / PUBLIC_KEY_FILENAME + try: + public_pem = batch.secure_read_bytes( + public_path, + "reviewer public key", + maximum_bytes=MAX_SECRET_BYTES, + ) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + if batch._sha256_bytes(public_pem) != registration["public_key_pem_sha256"]: + raise SignerError("reviewer public key file does not match its registration") + if public_pem.decode("ascii") != registration["public_key_pem"]: + raise SignerError("reviewer public key bytes are not canonical") + try: + public_key = serialization.load_pem_public_key(public_pem) + except (TypeError, ValueError) as exc: + raise SignerError("reviewer public key is invalid") from exc + if not isinstance(public_key, Ed25519PublicKey): + raise SignerError("reviewer public key must be Ed25519") + public_raw = _public_raw(public_key) + fingerprint = batch._sha256_bytes(public_raw) + if fingerprint != registration["public_key_raw_sha256"]: + raise SignerError("reviewer public key fingerprint drift") + expected_key_id = f"chen-yuqi-rc06-{fingerprint[:16]}" + if registration["key_id"] != expected_key_id: + raise SignerError("reviewer key ID is not derived from its public key") + return registration, public_key + + +def _load_private_key( + key_directory: Path, + passphrase: str, +) -> tuple[dict[str, Any], Ed25519PrivateKey]: + key_directory = _assert_local_safe_path( + key_directory, + "reviewer key directory", + allow_existing=True, + ) + _harden_private_directory(key_directory) + registration, registered_public = _load_public_registration(key_directory) + private_raw = _read_secret_file( + key_directory / PRIVATE_KEY_FILENAME, + "reviewer encrypted private key", + ) + if not private_raw.startswith(b"-----BEGIN ENCRYPTED PRIVATE KEY-----"): + raise SignerError("reviewer private key must be encrypted PKCS#8") + try: + private_key = serialization.load_pem_private_key( + private_raw, + password=_encode_existing_passphrase(passphrase), + ) + except (TypeError, ValueError) as exc: + raise SignerError("reviewer private key password is incorrect or the key is invalid") from exc + if not isinstance(private_key, Ed25519PrivateKey): + raise SignerError("reviewer private key must be Ed25519") + if _public_raw(private_key.public_key()) != _public_raw(registered_public): + raise SignerError("reviewer private and public keys do not match") + return registration, private_key + + +def initialize_reviewer_key( + key_directory: Path, + *, + passphrase: str, + created_at: datetime | None = None, +) -> dict[str, Any]: + key_directory = _assert_local_safe_path( + key_directory, + "reviewer key directory", + allow_existing=True, + ) + if key_directory.exists() or key_directory.is_symlink(): + registration, _private_key = _load_private_key(key_directory, passphrase) + return { + "status": "already_initialized_and_verified", + "key_id": registration["key_id"], + "public_key_fingerprint": registration["public_key_raw_sha256"], + "registration_file": os.fspath(key_directory / REGISTRATION_FILENAME), + } + password = _validate_passphrase(passphrase) + created = (created_at or _now_utc()).astimezone(UTC).replace(microsecond=0) + private_key = Ed25519PrivateKey.generate() + public_key = private_key.public_key() + public_raw = _public_raw(public_key) + fingerprint = batch._sha256_bytes(public_raw) + key_id = f"chen-yuqi-rc06-{fingerprint[:16]}" + private_pem = private_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.BestAvailableEncryption(password), + ) + public_pem = public_key.public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + registration = { + "schema_version": KEY_REGISTRATION_SCHEMA_VERSION, + "key_id": key_id, + "algorithm": "Ed25519", + "reviewer": dict(batch.SOLE_RELEASE_REVIEWER), + "created_at": _format_time(created), + "public_key_pem": public_pem.decode("ascii"), + "public_key_raw_sha256": fingerprint, + "public_key_pem_sha256": batch._sha256_bytes(public_pem), + } + bundle = { + "schema_version": KEY_BUNDLE_SCHEMA_VERSION, + "key_id": key_id, + "algorithm": "Ed25519", + "reviewer": dict(batch.SOLE_RELEASE_REVIEWER), + "created_at": _format_time(created), + "encrypted_private_key": True, + "private_key_filename": PRIVATE_KEY_FILENAME, + "public_key_filename": PUBLIC_KEY_FILENAME, + "registration_filename": REGISTRATION_FILENAME, + "public_key_raw_sha256": fingerprint, + } + _write_bundle_atomically( + key_directory, + { + PRIVATE_KEY_FILENAME: private_pem, + PUBLIC_KEY_FILENAME: public_pem, + REGISTRATION_FILENAME: _canonical_json(registration), + KEY_BUNDLE_FILENAME: _canonical_json(bundle), + }, + private_directory=True, + ) + loaded, _ = _load_private_key(key_directory, passphrase) + if loaded != registration: + raise SignerError("reviewer key self-verification failed") + return { + "status": "initialized_not_approved", + "key_id": key_id, + "public_key_fingerprint": fingerprint, + "registration_file": os.fspath(key_directory / REGISTRATION_FILENAME), + } + + +def _safe_package_file(directory: Path, filename: Any, expected: str, label: str) -> Path: + if filename != expected: + raise SignerError(f"{label} filename must be {expected}") + path = directory / expected + try: + batch._assert_no_symlink_components(path, label) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + return path + + +def _load_review_request(request_file: Path) -> tuple[dict[str, Any], bytes]: + try: + raw = batch.secure_read_bytes(request_file, "release review request", maximum_bytes=1024 * 1024) + request, _ = batch.secure_read_json( + request_file, + "release review request", + batch._sha256_bytes(raw), + ) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + required = { + "schema_version", + "request_id", + "created_at", + "expires_at", + "candidate_commit", + "candidate_tag", + "manifest", + "approval_keyring", + "approval_root_public_key", + } + if not isinstance(request, dict) or set(request) != required: + raise SignerError("release review request has an invalid shape") + if request["schema_version"] != REVIEW_REQUEST_SCHEMA_VERSION: + raise SignerError("release review request schema is unsupported") + if not isinstance(request["request_id"], str) or not batch.STABLE_ID_RE.fullmatch(request["request_id"]): + raise SignerError("release review request ID is invalid") + return request, raw + + +def _load_request_materials( + request_file: Path, + *, + now: datetime, +) -> dict[str, Any]: + _verify_complete_bundle( + request_file.parent, + { + REQUEST_FILENAME, + MANIFEST_FILENAME, + KEYRING_FILENAME, + ROOT_PUBLIC_KEY_FILENAME, + }, + "release review request bundle", + ) + request, request_raw = _load_review_request(request_file) + created_at = _parse_time(request["created_at"], "request created_at") + expires_at = _parse_time(request["expires_at"], "request expires_at") + if expires_at <= created_at or expires_at - created_at > MAX_REQUEST_WINDOW: + raise SignerError("release review request validity is invalid") + normalized_now = now.astimezone(UTC) + if normalized_now < created_at or normalized_now >= expires_at: + raise SignerError("release review request is not currently valid") + directory = _assert_local_safe_path( + request_file.parent, + "release review request directory", + allow_existing=True, + ) + if not directory.is_dir(): + raise SignerError("release review request directory is missing") + + manifest_ref = request["manifest"] + keyring_ref = request["approval_keyring"] + root_ref = request["approval_root_public_key"] + for ref, label in ( + (manifest_ref, "manifest reference"), + (keyring_ref, "approval keyring reference"), + (root_ref, "approval root public key reference"), + ): + if not isinstance(ref, dict) or set(ref) != {"filename", "raw_sha256"}: + raise SignerError(f"{label} has an invalid shape") + batch._require_sha(ref["raw_sha256"], f"{label} raw SHA-256") + + manifest_path = _safe_package_file( + directory, + manifest_ref["filename"], + MANIFEST_FILENAME, + "publication batch manifest", + ) + keyring_path = _safe_package_file( + directory, + keyring_ref["filename"], + KEYRING_FILENAME, + "approval keyring", + ) + root_path = _safe_package_file( + directory, + root_ref["filename"], + ROOT_PUBLIC_KEY_FILENAME, + "approval root public key", + ) + try: + manifest, manifest_raw_sha = batch._load_manifest( + manifest_path, + manifest_ref["raw_sha256"], + ) + keyring, keyring_raw_sha = batch.secure_read_json( + keyring_path, + "approval keyring", + keyring_ref["raw_sha256"], + ) + root_raw = batch.secure_read_bytes(root_path, "approval root public key", maximum_bytes=32) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + if len(root_raw) != 32 or batch._sha256_bytes(root_raw) != root_ref["raw_sha256"]: + raise SignerError("approval root public key hash mismatch") + try: + root_public_key = Ed25519PublicKey.from_public_bytes(root_raw) + except ValueError as exc: + raise SignerError("approval root public key is invalid") from exc + if manifest["candidate"]["commit"] != request["candidate_commit"]: + raise SignerError("release review request candidate commit drift") + if manifest["candidate"]["tag"] != request["candidate_tag"]: + raise SignerError("release review request candidate tag drift") + expected_request_id = f"rc06-review-{manifest_raw_sha[:24]}" + if request["request_id"] != expected_request_id or request_file.parent.name != expected_request_id: + raise SignerError("release review request ID is not bound to the exact batch manifest") + if not manifest["publication_control"]["golden_dispositions_complete"]: + raise SignerError("all 31 Golden dispositions must be resolved before signing") + try: + trusted_keys = batch._validate_approval_keyring( + keyring, + root_public_key=root_public_key, + approved_at=normalized_now, + ) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + if keyring["authorized_batch_manifest_raw_sha256"] != manifest_raw_sha: + raise SignerError("approval keyring does not authorize the exact batch manifest") + return { + "request": request, + "request_raw_sha256": batch._sha256_bytes(request_raw), + "manifest": manifest, + "manifest_raw_sha256": manifest_raw_sha, + "keyring": keyring, + "keyring_raw_sha256": keyring_raw_sha, + "root_public_key": root_public_key, + "trusted_keys": trusted_keys, + "request_expires_at": expires_at, + } + + +def _approval_code(review_summary_sha256: str) -> str: + return f"{int(review_summary_sha256[:12], 16) % 1_000_000:06d}" + + +def _summary_text(summary: Mapping[str, Any], *, approval_code: str) -> str: + candidate = summary["candidate"] + scope = summary["batch"] + lines = [ + "AI合同平台 RC-06 单审核人批准摘要", + "", + f"审核人:{batch.SOLE_RELEASE_REVIEWER['name']}({batch.SOLE_RELEASE_REVIEWER['role']})", + f"候选提交:{candidate['commit']}", + f"候选标签:{candidate['tag']}", + f"Manifest 原始文件 SHA-256:{summary['manifest_raw_sha256']}", + f"Manifest Payload SHA-256:{summary['manifest_payload_sha256']}", + f"合同资产:{scope['asset_count']} 个", + f"Prompt 模块:{scope['prompt_module_count']} 个", + f"Golden 处置:{scope['golden_disposition_count']} 项", + f"RC-05 真实终态:{'通过' if candidate['rc05_complete_real_green'] else '未通过'}", + f"本次批准码:{approval_code}", + "", + "本操作只批准上述精确批次,不执行发布、迁移、公众开放或生产切流。", + "系统应用证据和运行时 Prompt 快照仍是独立门禁,当前状态继续 NO_GO。", + ] + return "\n".join(lines) + "\n" + + +def _matching_reviewer_key( + trusted_keys: list[dict[str, Any]], + registration: Mapping[str, Any], + public_key: Ed25519PublicKey, +) -> dict[str, Any]: + public_raw = _public_raw(public_key) + matches = [ + item + for item in trusted_keys + if item["key_id"] == registration["key_id"] and item["public_key_raw"] == public_raw + ] + if len(matches) != 1: + raise SignerError("reviewer key is not uniquely trusted by the frozen keyring") + return matches[0] + + +def sign_review_request( + request_file: Path, + key_directory: Path, + output_directory: Path, + *, + passphrase: str, + confirmation_code: str, + now: datetime | None = None, + expected_request_raw_sha256: str | None = None, + expected_review_summary_sha256: str | None = None, +) -> dict[str, Any]: + approved_at = (now or _now_utc()).astimezone(UTC).replace(microsecond=0) + materials = _load_request_materials(request_file, now=approved_at) + if ( + expected_request_raw_sha256 is not None + and materials["request_raw_sha256"] + != batch._require_sha(expected_request_raw_sha256, "displayed request raw SHA-256") + ): + raise SignerError("release review request changed after it was displayed") + registration, private_key = _load_private_key(key_directory, passphrase) + reviewer_key = _matching_reviewer_key( + materials["trusted_keys"], + registration, + private_key.public_key(), + ) + key_not_after = reviewer_key["not_after_parsed"].astimezone(UTC) + keyring_not_after = _parse_time( + materials["keyring"]["expires_at"], + "approval keyring expires_at", + ) + expiry_candidates = [ + approved_at + DEFAULT_APPROVAL_WINDOW, + key_not_after, + keyring_not_after, + materials["request_expires_at"], + ] + risk_not_after = batch._approval_risk_acceptance_deadline(materials["manifest"]) + if risk_not_after is not None: + expiry_candidates.append(risk_not_after) + expires_at = min(expiry_candidates) + if expires_at <= approved_at: + raise SignerError("reviewer key or request expires before an approval can be issued") + try: + review_summary = batch.build_approval_review_summary( + materials["manifest"], + manifest_raw_sha256=materials["manifest_raw_sha256"], + ) + review_summary_sha256 = batch._sha256_json(review_summary) + if ( + expected_review_summary_sha256 is not None + and review_summary_sha256 + != batch._require_sha( + expected_review_summary_sha256, + "displayed review summary SHA-256", + ) + ): + raise SignerError("release review summary changed after it was displayed") + expected_code = _approval_code(review_summary_sha256) + if confirmation_code.strip() != expected_code: + raise SignerError("approval code does not match the exact review summary") + payload = batch.build_approval_payload( + materials["manifest"], + manifest_raw_sha256=materials["manifest_raw_sha256"], + key_id=registration["key_id"], + approval_keyring_epoch=materials["keyring"]["epoch"], + approval_keyring_payload_sha256=materials["keyring"]["keyring_payload_sha256"], + approved_at=approved_at, + expires_at=expires_at, + ) + signature = private_key.sign(batch.APPROVAL_SIGNATURE_DOMAIN + batch._canonical(payload)) + approval = { + "schema_version": batch.APPROVAL_SCHEMA_VERSION, + "approval_id": ( + "rc06-batch-approval-" + + batch._sha256_bytes(batch._canonical(payload) + signature)[:24] + ), + "payload": payload, + "signature": base64.b64encode(signature).decode("ascii"), + } + batch.validate_approval_receipt( + materials["manifest"], + materials["manifest_raw_sha256"], + approval, + materials["keyring"], + materials["root_public_key"], + ) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + + approval_raw = _canonical_json(approval) + approval_raw_sha256 = batch._sha256_bytes(approval_raw) + summary_raw = _canonical_json(review_summary) + summary_text = _summary_text(review_summary, approval_code=expected_code).encode("utf-8") + result = { + "schema_version": SIGNING_RESULT_SCHEMA_VERSION, + "status": "signed_and_self_verified_not_published", + "request_id": materials["request"]["request_id"], + "request_raw_sha256": materials["request_raw_sha256"], + "approval_id": approval["approval_id"], + "key_id": registration["key_id"], + "approval_keyring_epoch": payload["approval_keyring_epoch"], + "approval_keyring_payload_sha256": payload["approval_keyring_payload_sha256"], + "candidate_commit": materials["manifest"]["candidate"]["commit"], + "candidate_tag": materials["manifest"]["candidate"]["tag"], + "manifest_raw_sha256": materials["manifest_raw_sha256"], + "manifest_payload_sha256": materials["manifest"]["manifest_payload_sha256"], + "review_summary_sha256": review_summary_sha256, + "approval_raw_sha256": approval_raw_sha256, + "approved_at": payload["approved_at"], + "expires_at": payload["expires_at"], + "release_ready": False, + "public_traffic_authorized": False, + } + _write_bundle_atomically( + output_directory, + { + APPROVAL_FILENAME: approval_raw, + APPROVAL_SHA_FILENAME: (approval_raw_sha256 + "\n").encode("ascii"), + SUMMARY_JSON_FILENAME: summary_raw, + SUMMARY_TEXT_FILENAME: summary_text, + SIGNING_RESULT_FILENAME: _canonical_json(result), + }, + private_directory=True, + ) + return result + + +def prepare_review_request( + *, + state_directory: Path, + manifest_file: Path, + manifest_sha256: str, + approval_keyring_file: Path, + approval_keyring_sha256: str, + approval_root_public_key_file: Path, + now: datetime | None = None, +) -> dict[str, Any]: + created_at = (now or _now_utc()).astimezone(UTC).replace(microsecond=0) + state_directory = _ensure_private_directory(state_directory, "signer state directory") + try: + manifest_raw = batch.secure_read_bytes(manifest_file, "publication batch manifest") + if batch._sha256_bytes(manifest_raw) != batch._require_sha( + manifest_sha256, + "publication batch manifest raw SHA-256", + ): + raise SignerError("publication batch manifest raw SHA-256 mismatch") + manifest = batch.validate_manifest( + json.loads( + manifest_raw, + object_pairs_hook=batch._reject_duplicate_json_pairs, + parse_constant=batch._reject_json_constant, + ) + ) + keyring_raw = batch.secure_read_bytes(approval_keyring_file, "approval keyring") + if batch._sha256_bytes(keyring_raw) != batch._require_sha( + approval_keyring_sha256, + "approval keyring raw SHA-256", + ): + raise SignerError("approval keyring raw SHA-256 mismatch") + keyring = json.loads( + keyring_raw, + object_pairs_hook=batch._reject_duplicate_json_pairs, + parse_constant=batch._reject_json_constant, + ) + root_raw = batch.secure_read_bytes( + approval_root_public_key_file, + "approval root public key", + maximum_bytes=32, + ) + if len(root_raw) != 32: + raise SignerError("approval root public key must contain exactly 32 bytes") + root_public_key = Ed25519PublicKey.from_public_bytes(root_raw) + except (batch.PublicationBatchError, UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc: + if isinstance(exc, SignerError): + raise + raise SignerError(str(exc)) from exc + if not manifest["publication_control"]["golden_dispositions_complete"]: + raise SignerError("all 31 Golden dispositions must be resolved before preparing a request") + expires_at = min( + created_at + MAX_REQUEST_WINDOW, + _parse_time(keyring["expires_at"], "approval keyring expires_at"), + ) + request_id = f"rc06-review-{batch._sha256_bytes(manifest_raw)[:24]}" + request = { + "schema_version": REVIEW_REQUEST_SCHEMA_VERSION, + "request_id": request_id, + "created_at": _format_time(created_at), + "expires_at": _format_time(expires_at), + "candidate_commit": manifest["candidate"]["commit"], + "candidate_tag": manifest["candidate"]["tag"], + "manifest": { + "filename": MANIFEST_FILENAME, + "raw_sha256": batch._sha256_bytes(manifest_raw), + }, + "approval_keyring": { + "filename": KEYRING_FILENAME, + "raw_sha256": batch._sha256_bytes(keyring_raw), + }, + "approval_root_public_key": { + "filename": ROOT_PUBLIC_KEY_FILENAME, + "raw_sha256": batch._sha256_bytes(root_raw), + }, + } + output_directory = state_directory / "inbox" / request_id + material_files = { + MANIFEST_FILENAME: manifest_raw, + KEYRING_FILENAME: keyring_raw, + ROOT_PUBLIC_KEY_FILENAME: root_raw, + } + request_files = {REQUEST_FILENAME: _canonical_json(request), **material_files} + processed_directory = state_directory / "processed" / request_id + if processed_directory.exists() or processed_directory.is_symlink(): + _assert_existing_request_matches_materials( + processed_directory, + request, + material_files, + "existing processed review request", + ) + existing = _load_existing_signing_result( + state_directory, + processed_directory / REQUEST_FILENAME, + _receipt_directory(state_directory, request), + ) + return { + **existing, + "request_file": os.fspath(processed_directory / REQUEST_FILENAME), + } + try: + batch._validate_approval_keyring( + keyring, + root_public_key=root_public_key, + approved_at=created_at, + ) + if keyring["authorized_batch_manifest_raw_sha256"] != batch._sha256_bytes(manifest_raw): + raise SignerError("approval keyring does not authorize the exact batch manifest") + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + if expires_at <= created_at: + raise SignerError("approval keyring expires before the review request can be issued") + if output_directory.exists() or output_directory.is_symlink(): + existing_request = _assert_existing_request_matches_materials( + output_directory, + request, + material_files, + "existing pending review request", + ) + existing_created_at = _parse_time( + existing_request["created_at"], + "existing request created_at", + ) + existing_expires_at = _parse_time( + existing_request["expires_at"], + "existing request expires_at", + ) + if created_at < existing_created_at: + raise SignerError("existing review request is not valid yet") + if created_at < existing_expires_at: + _load_request_materials(output_directory / REQUEST_FILENAME, now=created_at) + return { + "status": "review_request_already_prepared_not_signed", + "request_id": request_id, + "request_file": os.fspath(output_directory / REQUEST_FILENAME), + "manifest_raw_sha256": batch._sha256_bytes(manifest_raw), + "release_ready": False, + } + _archive_expired_request( + state_directory, + output_directory / REQUEST_FILENAME, + ) + _write_bundle_atomically(output_directory, request_files, private_directory=True) + return { + "status": "review_request_prepared_not_signed", + "request_id": request_id, + "request_file": os.fspath(output_directory / REQUEST_FILENAME), + "manifest_raw_sha256": batch._sha256_bytes(manifest_raw), + "release_ready": False, + } + + +def _discover_single_request(state_directory: Path) -> Path: + inbox = state_directory / "inbox" + if not inbox.exists(): + raise SignerError("当前没有待签署候选;未创建任何批准,请等待系统通知") + try: + batch._assert_no_symlink_components(inbox, "reviewer inbox") + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + requests = sorted(inbox.glob(f"*/{REQUEST_FILENAME}")) + if not requests: + raise SignerError("当前没有待签署候选;未创建任何批准,请等待系统通知") + if len(requests) != 1: + raise SignerError("检测到多个待签署候选,为避免签错已停止;请联系系统处理,不要手工删除") + return requests[0] + + +def _receipt_directory(state_directory: Path, request: Mapping[str, Any]) -> Path: + return ( + state_directory + / "receipts" + / str(request["candidate_commit"]) + / str(request["request_id"]) + ) + + +def _load_existing_signing_result( + state_directory: Path, + request_file: Path, + output_directory: Path, +) -> dict[str, Any]: + _verify_complete_bundle( + output_directory, + { + APPROVAL_FILENAME, + APPROVAL_SHA_FILENAME, + SUMMARY_JSON_FILENAME, + SUMMARY_TEXT_FILENAME, + SIGNING_RESULT_FILENAME, + }, + "existing signing receipt bundle", + ) + try: + approval_raw = batch.secure_read_bytes( + output_directory / APPROVAL_FILENAME, + "existing approval receipt", + ) + approval = json.loads( + approval_raw, + object_pairs_hook=batch._reject_duplicate_json_pairs, + parse_constant=batch._reject_json_constant, + ) + approved_at = _parse_time( + approval.get("payload", {}).get("approved_at"), + "existing approval approved_at", + ) + materials = _load_request_materials(request_file, now=approved_at) + batch.validate_approval_receipt( + materials["manifest"], + materials["manifest_raw_sha256"], + approval, + materials["keyring"], + materials["root_public_key"], + ) + expected_summary = batch.build_approval_review_summary( + materials["manifest"], + manifest_raw_sha256=materials["manifest_raw_sha256"], + ) + expected_summary_sha = batch._sha256_json(expected_summary) + summary_raw = batch.secure_read_bytes( + output_directory / SUMMARY_JSON_FILENAME, + "existing review summary", + ) + if ( + summary_raw != _canonical_json(expected_summary) + or expected_summary_sha != approval["payload"]["review_summary_sha256"] + ): + raise SignerError("existing review summary does not match the signed approval") + summary_text = batch.secure_read_bytes( + output_directory / SUMMARY_TEXT_FILENAME, + "existing review summary text", + ) + expected_code = _approval_code(expected_summary_sha) + if summary_text != _summary_text( + expected_summary, + approval_code=expected_code, + ).encode("utf-8"): + raise SignerError("existing review summary text does not match the signed approval") + result, _ = batch.secure_read_json( + output_directory / SIGNING_RESULT_FILENAME, + "existing signing result", + ) + approval_sha = batch._sha256_bytes(approval_raw) + expected = { + "schema_version": SIGNING_RESULT_SCHEMA_VERSION, + "status": "signed_and_self_verified_not_published", + "request_id": materials["request"]["request_id"], + "request_raw_sha256": materials["request_raw_sha256"], + "approval_id": approval["approval_id"], + "key_id": approval["payload"]["key_id"], + "approval_keyring_epoch": approval["payload"]["approval_keyring_epoch"], + "approval_keyring_payload_sha256": approval["payload"][ + "approval_keyring_payload_sha256" + ], + "candidate_commit": materials["manifest"]["candidate"]["commit"], + "candidate_tag": materials["manifest"]["candidate"]["tag"], + "manifest_raw_sha256": materials["manifest_raw_sha256"], + "manifest_payload_sha256": materials["manifest"]["manifest_payload_sha256"], + "review_summary_sha256": approval["payload"]["review_summary_sha256"], + "approval_raw_sha256": approval_sha, + "approved_at": approval["payload"]["approved_at"], + "expires_at": approval["payload"]["expires_at"], + "release_ready": False, + "public_traffic_authorized": False, + } + if result != expected: + raise SignerError("existing signing result does not match its verified approval") + stored_sha = batch.secure_read_bytes( + output_directory / APPROVAL_SHA_FILENAME, + "existing approval checksum", + ).decode("ascii").strip() + if stored_sha != approval_sha: + raise SignerError("existing approval checksum drift") + except (batch.PublicationBatchError, UnicodeDecodeError, json.JSONDecodeError) as exc: + if isinstance(exc, SignerError): + raise + raise SignerError(str(exc)) from exc + return {**result, "status": "already_signed_and_verified_not_published"} + + +def _archive_signed_request(state_directory: Path, request_file: Path) -> None: + request, _ = _load_review_request(request_file) + processed_parent = _ensure_private_directory(state_directory / "processed", "processed request directory") + destination = processed_parent / request["request_id"] + expected_names = { + REQUEST_FILENAME, + MANIFEST_FILENAME, + KEYRING_FILENAME, + ROOT_PUBLIC_KEY_FILENAME, + } + if destination.exists() or destination.is_symlink(): + _verify_complete_bundle(request_file.parent, expected_names, "duplicate pending request") + source_files: dict[str, bytes] = {} + for name in expected_names: + try: + source_files[name] = batch.secure_read_bytes( + request_file.parent / name, + f"duplicate pending request {name}", + ) + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + _assert_bundle_matches_files(destination, source_files, "processed review request") + _remove_owned_staging_bundle( + request_file.parent, + expected_names | {BUNDLE_COMPLETE_FILENAME}, + ) + if request_file.parent.exists(): + raise SignerError("duplicate pending request could not be safely retired") + return + _publish_directory_no_replace(request_file.parent, destination) + + +def _archive_expired_request(state_directory: Path, request_file: Path) -> None: + request, request_raw = _load_review_request(request_file) + expired_parent = _ensure_private_directory(state_directory / "expired", "expired request directory") + destination = expired_parent / ( + f"{request['request_id']}-{batch._sha256_bytes(request_raw)[:16]}" + ) + if destination.exists() or destination.is_symlink(): + raise SignerError("expired review request archive already exists; refusing to overwrite") + _publish_directory_no_replace(request_file.parent, destination) + + +def _retire_expired_pending_requests( + state_directory: Path, + *, + now: datetime, +) -> list[str]: + """Authenticate and archive every expired inbox request before discovery.""" + + inbox = state_directory / "inbox" + if not inbox.exists(): + return [] + try: + batch._assert_no_symlink_components(inbox, "reviewer inbox") + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + normalized_now = now.astimezone(UTC) + retired: list[str] = [] + for request_file in sorted(inbox.glob(f"*/{REQUEST_FILENAME}")): + _verify_complete_bundle( + request_file.parent, + { + REQUEST_FILENAME, + MANIFEST_FILENAME, + KEYRING_FILENAME, + ROOT_PUBLIC_KEY_FILENAME, + }, + "release review request bundle", + ) + request, _ = _load_review_request(request_file) + created_at = _parse_time(request["created_at"], "request created_at") + expires_at = _parse_time(request["expires_at"], "request expires_at") + if expires_at <= created_at or expires_at - created_at > MAX_REQUEST_WINDOW: + raise SignerError("release review request validity is invalid") + if normalized_now < expires_at: + continue + # Validate the complete root-authorized package at the instant it became + # valid. Current wall time is intentionally not used for this historical + # authenticity check because the request is already expired. + _load_request_materials(request_file, now=created_at) + _archive_expired_request(state_directory, request_file) + retired.append(request["request_id"]) + return retired + + +def _reconcile_signed_requests(state_directory: Path) -> list[dict[str, Any]]: + inbox = state_directory / "inbox" + if not inbox.exists(): + return [] + try: + batch._assert_no_symlink_components(inbox, "reviewer inbox") + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + reconciled: list[dict[str, Any]] = [] + for request_file in sorted(inbox.glob(f"*/{REQUEST_FILENAME}")): + _verify_complete_bundle( + request_file.parent, + { + REQUEST_FILENAME, + MANIFEST_FILENAME, + KEYRING_FILENAME, + ROOT_PUBLIC_KEY_FILENAME, + }, + "release review request bundle", + ) + request, _ = _load_review_request(request_file) + output_directory = _receipt_directory(state_directory, request) + if not output_directory.exists() and not output_directory.is_symlink(): + continue + result = _load_existing_signing_result(state_directory, request_file, output_directory) + _archive_signed_request(state_directory, request_file) + reconciled.append(result) + return reconciled + + +def _latest_processed_signing_result(state_directory: Path) -> dict[str, Any] | None: + processed = state_directory / "processed" + if not processed.exists(): + return None + try: + batch._assert_no_symlink_components(processed, "processed request directory") + except batch.PublicationBatchError as exc: + raise SignerError(str(exc)) from exc + results: list[dict[str, Any]] = [] + for request_file in sorted(processed.glob(f"*/{REQUEST_FILENAME}")): + _verify_complete_bundle( + request_file.parent, + { + REQUEST_FILENAME, + MANIFEST_FILENAME, + KEYRING_FILENAME, + ROOT_PUBLIC_KEY_FILENAME, + }, + "processed release review request bundle", + ) + request, _ = _load_review_request(request_file) + output_directory = _receipt_directory(state_directory, request) + if not output_directory.is_dir(): + raise SignerError("processed review request is missing its verified receipt") + results.append( + _load_existing_signing_result(state_directory, request_file, output_directory) + ) + if not results: + return None + results.sort(key=lambda item: _parse_time(item["approved_at"], "existing approval approved_at")) + return results[-1] + + +def _require_interactive() -> None: + if not sys.stdin.isatty() or not sys.stdout.isatty(): + raise SignerError("reviewer initialization and signing require an interactive terminal") + + +def _prompt_new_passphrase() -> str: + first = getpass.getpass("请输入新的审核密钥口令(至少20个字符,建议使用仅你知道的长句):") + second = getpass.getpass("请再次输入审核密钥口令:") + if first != second: + raise SignerError("两次输入的审核密钥口令不一致") + _validate_passphrase(first) + return first + + +def _run_initialize(state_directory: Path) -> dict[str, Any]: + _require_interactive() + state_directory = _ensure_private_directory(state_directory, "signer state directory") + print("这是陈钰麒(总经理)的唯一人工发布审核密钥。私钥不会上传。") + key_directory = state_directory / "keys" + if key_directory.exists() or key_directory.is_symlink(): + passphrase = getpass.getpass("检测到已有审核密钥,请输入原口令进行验证:") + else: + passphrase = _prompt_new_passphrase() + return initialize_reviewer_key( + key_directory, + passphrase=passphrase, + ) + + +def _run_sign(state_directory: Path) -> dict[str, Any]: + _require_interactive() + state_directory = _ensure_private_directory(state_directory, "signer state directory") + display_time = _now_utc() + reconciled = _reconcile_signed_requests(state_directory) + _retire_expired_pending_requests(state_directory, now=display_time) + try: + request_file = _discover_single_request(state_directory) + except SignerError as exc: + if str(exc).startswith("当前没有待签署候选"): + existing = reconciled[-1] if reconciled else _latest_processed_signing_result( + state_directory + ) + if existing is not None: + return existing + raise + materials = _load_request_materials(request_file, now=display_time) + summary = batch.build_approval_review_summary( + materials["manifest"], + manifest_raw_sha256=materials["manifest_raw_sha256"], + ) + code = _approval_code(batch._sha256_json(summary)) + print(_summary_text(summary, approval_code=code)) + typed_code = input("请输入上方6位批准码;如不同意请直接关闭窗口:").strip() + passphrase = getpass.getpass("请输入审核密钥口令:") + output_directory = _receipt_directory(state_directory, materials["request"]) + result = sign_review_request( + request_file, + state_directory / "keys", + output_directory, + passphrase=passphrase, + confirmation_code=typed_code, + expected_request_raw_sha256=materials["request_raw_sha256"], + expected_review_summary_sha256=batch._sha256_json(summary), + ) + _archive_signed_request(state_directory, request_file) + return result + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + commands.add_parser("runtime-check", help="verify the isolated offline signer runtime") + initialize = commands.add_parser("initialize", help="initialize the sole reviewer key once") + initialize.add_argument("--state-directory", type=Path) + sign = commands.add_parser("sign", help="review and sign the only pending exact batch") + sign.add_argument("--state-directory", type=Path) + prepare = commands.add_parser( + "prepare-request", + help="system-only: stage one exact request in the reviewer inbox", + ) + prepare.add_argument("--state-directory", type=Path) + prepare.add_argument("--manifest", type=Path, required=True) + prepare.add_argument("--manifest-sha256", required=True) + prepare.add_argument("--approval-keyring", type=Path, required=True) + prepare.add_argument("--approval-keyring-sha256", required=True) + prepare.add_argument("--approval-root-public-key", type=Path, required=True) + return parser + + +def _user_error_message(error: str) -> str: + lowered = error.casefold() + if error.startswith("当前没有待签署候选") or error.startswith("检测到多个待签署候选"): + return error + if "password is incorrect" in lowered: + return "审核密钥口令不正确;未创建批准,请确认口令后重试" + if "approval code" in lowered: + return "批准码与当前候选不一致;未创建批准,请重新核对页面上的6位批准码" + if "pin is not provisioned" in lowered or "frozen pin" in lowered: + return "候选的系统信任配置尚未完成;未创建批准,请等待系统通知" + if "currently valid" in lowered or "expires" in lowered or "expired" in lowered: + return "候选或审核凭据已过有效期;未创建批准,请联系系统重新准备候选" + if error.startswith("另一个审核操作正在进行"): + return error + return f"操作未完成,系统已安全停止且未创建新的批准。原因:{error}" + + +def _user_success_message(result: Mapping[str, Any]) -> str | None: + status = result.get("status") + if status == "initialized_not_approved": + return "审核密钥初始化成功。现在无需继续操作;本步骤没有批准或发布任何候选。" + if status == "already_initialized_and_verified": + return "审核密钥已存在且验证成功,无需重复初始化。" + if status == "signed_and_self_verified_not_published": + return "候选已签署并完成自动验签。该操作尚未发布、迁移或开放公众访问。" + if status == "already_signed_and_verified_not_published": + return "该候选此前已签署且重新验签成功,无需重复操作;当前仍未发布。" + return None + + +def main(argv: list[str] | None = None) -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + args = _parser().parse_args(argv) + try: + _assert_trusted_runtime() + if args.command == "runtime-check": + result = { + "status": "isolated_runtime_verified", + "python_version": ".".join(str(part) for part in sys.version_info[:3]), + "cryptography_version": cryptography.__version__, + } + print(json.dumps(result, ensure_ascii=False, separators=(",", ":"))) + return 0 + state_directory = _assert_local_safe_path( + args.state_directory or _default_state_directory(), + "signer state directory", + allow_existing=True, + ) + with _state_operation_lock(state_directory): + if args.command == "initialize": + result = _run_initialize(state_directory) + elif args.command == "sign": + result = _run_sign(state_directory) + else: + result = prepare_review_request( + state_directory=state_directory, + manifest_file=args.manifest, + manifest_sha256=args.manifest_sha256, + approval_keyring_file=args.approval_keyring, + approval_keyring_sha256=args.approval_keyring_sha256, + approval_root_public_key_file=args.approval_root_public_key, + ) + except (SignerError, batch.PublicationBatchError, KeyboardInterrupt, EOFError) as exc: + message = "operation cancelled" if isinstance(exc, (KeyboardInterrupt, EOFError)) else str(exc) + user_message = ( + "操作已取消;未创建新的批准。" + if isinstance(exc, (KeyboardInterrupt, EOFError)) + else _user_error_message(message) + ) + print(user_message, file=sys.stderr) + print( + json.dumps( + {"status": "failed_closed", "error": message, "user_message": user_message}, + ensure_ascii=False, + separators=(",", ":"), + ), + file=sys.stderr, + ) + return 2 + user_message = _user_success_message(result) + if user_message is not None: + print(user_message) + print(json.dumps(result, ensure_ascii=False, separators=(",", ":"))) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/yuqei-infra/tests/test_rc06_publication_batch.py b/yuqei-infra/tests/test_rc06_publication_batch.py index 1e85a365..5824df28 100644 --- a/yuqei-infra/tests/test_rc06_publication_batch.py +++ b/yuqei-infra/tests/test_rc06_publication_batch.py @@ -4,7 +4,11 @@ import copy import base64 import importlib.util import json +import os +import subprocess import sys +import threading +from datetime import UTC, datetime, timedelta from pathlib import Path import pytest @@ -14,6 +18,7 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey REPO_ROOT = Path(__file__).resolve().parents[2] SCRIPT = REPO_ROOT / "yuqei-infra" / "scripts" / "build-rc06-publication-batch.py" +SIGNER_SCRIPT = REPO_ROOT / "yuqei-infra" / "scripts" / "rc06-single-reviewer-signing.py" def _load_script(): @@ -28,6 +33,18 @@ def _load_script(): batch = _load_script() +def _load_signer_script(): + spec = importlib.util.spec_from_file_location("rc06_single_reviewer_signing", SIGNER_SCRIPT) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +signer = _load_signer_script() + + @pytest.fixture(scope="module") def inventory(): return batch.load_runtime_inventory(REPO_ROOT) @@ -487,6 +504,39 @@ def _manifest(inventory: dict, *, resolved: bool = False) -> dict: ) +def _with_scoped_risk_acceptance( + manifest: dict, + *, + decided_at: str = "2026-09-20T10:00:00+08:00", + expires_at: str = "2026-10-20T10:00:00+08:00", +) -> dict: + result = copy.deepcopy(manifest) + item = result["golden_dispositions"]["items"][0] + item["status"] = "scoped_internal_risk_acceptance" + item["decided_at"] = decided_at + item["risk_acceptance"] = { + "owner": batch.SOLE_RELEASE_REVIEWER["name"], + "scope": "Named internal pilot workspaces only", + "expires_at": expires_at, + "controls": ["No public registration", "Manual incident stop"], + "use_limits": ["Synthetic or de-identified contracts only"], + } + result["golden_dispositions"]["counts_by_status"] = { + status_value: sum( + row["status"] == status_value + for row in result["golden_dispositions"]["items"] + ) + for status_value in sorted(batch.DISPOSITION_STATUSES) + } + result["golden_dispositions"]["inventory_sha256"] = batch._sha256_json( + result["golden_dispositions"]["items"] + ) + result["manifest_payload_sha256"] = batch._sha256_json( + {key: value for key, value in result.items() if key != "manifest_payload_sha256"} + ) + return result + + def test_real_source_inventory_is_exact_and_keeps_prompt_boundaries(inventory: dict) -> None: assert len(inventory["assets"]) == 60 assert len({item["template_id"] for item in inventory["assets"]}) == 60 @@ -578,6 +628,41 @@ def test_golden_dispositions_require_sole_reviewer_batch_and_complete_risk_field batch.validate_disposition(missing_controls, case) +def test_approval_window_must_be_covered_by_current_dispositions(inventory: dict) -> None: + approved_at = batch._parse_time("2026-09-21T09:00:00+08:00", "fixture approved_at") + expires_at = batch._parse_time("2026-09-22T09:00:00+08:00", "fixture expires_at") + manifest = _with_scoped_risk_acceptance( + _manifest(inventory, resolved=True), + expires_at="2026-09-21T18:00:00+08:00", + ) + with pytest.raises(batch.PublicationBatchError, match="approval validity exceeds.*risk acceptance"): + batch.build_approval_payload( + manifest, + manifest_raw_sha256=batch._sha256_bytes(batch._canonical(manifest)), + key_id="rc06-release-owner-2026q3", + approval_keyring_epoch=1, + approval_keyring_payload_sha256="c" * 64, + approved_at=approved_at, + expires_at=expires_at, + ) + + future_decision = _with_scoped_risk_acceptance( + _manifest(inventory, resolved=True), + decided_at="2026-09-21T10:00:00+08:00", + expires_at="2026-09-23T10:00:00+08:00", + ) + with pytest.raises(batch.PublicationBatchError, match="decided after the approval time"): + batch.build_approval_payload( + future_decision, + manifest_raw_sha256=batch._sha256_bytes(batch._canonical(future_decision)), + key_id="rc06-release-owner-2026q3", + approval_keyring_epoch=1, + approval_keyring_payload_sha256="c" * 64, + approved_at=approved_at, + expires_at=expires_at, + ) + + def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repetition( inventory: dict, monkeypatch: pytest.MonkeyPatch, @@ -607,6 +692,7 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe "epoch": 1, "issued_at": "2026-09-01T00:00:00+08:00", "expires_at": "2026-10-31T23:59:59+08:00", + "authorized_batch_manifest_raw_sha256": batch._sha256_bytes(batch._canonical(manifest)), "keys": [ { "key_id": "rc06-release-owner-2026q3", @@ -634,39 +720,22 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe "TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256", batch._sha256_bytes(root_public_raw), ) - monkeypatch.setattr( - batch, - "TRUSTED_APPROVAL_KEYRING_PAYLOAD_SHA256", - keyring_payload_sha256, - ) - monkeypatch.setattr(batch, "MIN_APPROVAL_KEYRING_EPOCH", 1) + monkeypatch.setattr(batch, "FROZEN_APPROVAL_KEYRING_EPOCH", 1) monkeypatch.setattr( batch, "TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256", batch._sha256_bytes(apply_service_public_raw), ) - approval_payload = { - "schema_version": batch.APPROVAL_PAYLOAD_SCHEMA_VERSION, - "batch_manifest_payload_sha256": manifest["manifest_payload_sha256"], - "decision": "approved", - "approved_at": "2026-09-21T09:00:00+08:00", - "expires_at": "2026-09-28T09:00:00+08:00", - "key_id": "rc06-release-owner-2026q3", - "approver": { - **batch.SOLE_RELEASE_REVIEWER, - "human_confirmed": True, - }, - "asset_payload_sha256s": { - item["template_id"]: item["asset_payload_sha256"] - for item in manifest["asset_batch"]["items"] - }, - "prompt_module_payload_sha256s": { - item["key"]: item["module_payload_sha256"] - for item in manifest["prompt_module_batch"]["items"] - }, - "golden_disposition_inventory_sha256": manifest["golden_dispositions"]["inventory_sha256"], - "evidence_sha256": "8" * 64, - } + manifest_raw_sha = batch._sha256_bytes(batch._canonical(manifest)) + approval_payload = batch.build_approval_payload( + manifest, + manifest_raw_sha256=manifest_raw_sha, + key_id="rc06-release-owner-2026q3", + approval_keyring_epoch=keyring["epoch"], + approval_keyring_payload_sha256=keyring["keyring_payload_sha256"], + approved_at=batch._parse_time("2026-09-21T09:00:00+08:00", "fixture approved_at"), + expires_at=batch._parse_time("2026-09-28T09:00:00+08:00", "fixture expires_at"), + ) signature = private_key.sign( batch.APPROVAL_SIGNATURE_DOMAIN + batch._canonical(approval_payload) ) @@ -681,11 +750,192 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe } assert batch.validate_approval_receipt( manifest, + manifest_raw_sha, approval, keyring, root_public_key, ) == approval + rotated_unsigned = copy.deepcopy(keyring_unsigned) + rotated_unsigned["epoch"] = 2 + rotated_keyring = { + **rotated_unsigned, + "keyring_payload_sha256": batch._sha256_json(rotated_unsigned), + "root_signature_base64": base64.b64encode( + root_private_key.sign( + batch.APPROVAL_KEYRING_SIGNATURE_DOMAIN + batch._canonical(rotated_unsigned) + ) + ).decode("ascii"), + } + with pytest.raises(batch.PublicationBatchError, match="does not match the frozen candidate"): + batch.validate_approval_receipt( + manifest, + manifest_raw_sha, + approval, + rotated_keyring, + root_public_key, + ) + monkeypatch.setattr(batch, "FROZEN_APPROVAL_KEYRING_EPOCH", 2) + with pytest.raises(batch.PublicationBatchError, match="does not match the frozen candidate"): + batch.validate_approval_receipt( + manifest, + manifest_raw_sha, + approval, + keyring, + root_public_key, + ) + monkeypatch.setattr(batch, "FROZEN_APPROVAL_KEYRING_EPOCH", 1) + + risk_manifest = _with_scoped_risk_acceptance( + manifest, + expires_at="2026-09-22T09:00:00+08:00", + ) + risk_manifest_raw_sha = batch._sha256_bytes(batch._canonical(risk_manifest)) + risk_payload = { + **copy.deepcopy(approval_payload), + "batch_manifest_raw_sha256": risk_manifest_raw_sha, + "batch_manifest_payload_sha256": risk_manifest["manifest_payload_sha256"], + "golden_disposition_inventory_sha256": risk_manifest["golden_dispositions"][ + "inventory_sha256" + ], + "review_summary_sha256": batch._sha256_json( + batch.build_approval_review_summary( + risk_manifest, + manifest_raw_sha256=risk_manifest_raw_sha, + ) + ), + } + risk_signature = private_key.sign( + batch.APPROVAL_SIGNATURE_DOMAIN + batch._canonical(risk_payload) + ) + risk_approval = { + "schema_version": batch.APPROVAL_SCHEMA_VERSION, + "approval_id": ( + "rc06-batch-approval-" + + batch._sha256_bytes(batch._canonical(risk_payload) + risk_signature)[:24] + ), + "payload": risk_payload, + "signature": base64.b64encode(risk_signature).decode("ascii"), + } + risk_keyring_unsigned = copy.deepcopy(keyring_unsigned) + risk_keyring_unsigned["authorized_batch_manifest_raw_sha256"] = risk_manifest_raw_sha + risk_keyring = { + **risk_keyring_unsigned, + "keyring_payload_sha256": batch._sha256_json(risk_keyring_unsigned), + "root_signature_base64": base64.b64encode( + root_private_key.sign( + batch.APPROVAL_KEYRING_SIGNATURE_DOMAIN + + batch._canonical(risk_keyring_unsigned) + ) + ).decode("ascii"), + } + with pytest.raises(batch.PublicationBatchError, match="approval validity exceeds.*risk acceptance"): + batch.validate_approval_receipt( + risk_manifest, + risk_manifest_raw_sha, + risk_approval, + risk_keyring, + root_public_key, + ) + + with pytest.raises(batch.PublicationBatchError, match="exact manifest file"): + batch.validate_approval_receipt( + manifest, + manifest["manifest_payload_sha256"], + approval, + keyring, + root_public_key, + ) + + wrong_summary = copy.deepcopy(approval) + wrong_summary["payload"]["review_summary_sha256"] = "8" * 64 + wrong_summary_signature = private_key.sign( + batch.APPROVAL_SIGNATURE_DOMAIN + batch._canonical(wrong_summary["payload"]) + ) + wrong_summary["signature"] = base64.b64encode(wrong_summary_signature).decode("ascii") + wrong_summary["approval_id"] = ( + "rc06-batch-approval-" + + batch._sha256_bytes( + batch._canonical(wrong_summary["payload"]) + wrong_summary_signature + )[:24] + ) + with pytest.raises(batch.PublicationBatchError, match="exact review summary"): + batch.validate_approval_receipt( + manifest, + manifest_raw_sha, + wrong_summary, + keyring, + root_public_key, + ) + + other_identity_unsigned = copy.deepcopy(keyring_unsigned) + other_identity_unsigned["keys"][0]["name"] = "其他审核人" + other_identity_payload_sha256 = batch._sha256_json(other_identity_unsigned) + other_identity_keyring = { + **other_identity_unsigned, + "keyring_payload_sha256": other_identity_payload_sha256, + "root_signature_base64": base64.b64encode( + root_private_key.sign( + batch.APPROVAL_KEYRING_SIGNATURE_DOMAIN + + batch._canonical(other_identity_unsigned) + ) + ).decode("ascii"), + } + with pytest.raises(batch.PublicationBatchError, match="sole release reviewer"): + batch.validate_approval_receipt( + manifest, + manifest_raw_sha, + approval, + other_identity_keyring, + root_public_key, + ) + short_keyring_unsigned = copy.deepcopy(keyring_unsigned) + short_keyring_unsigned["expires_at"] = "2026-09-25T23:59:59+08:00" + short_keyring_payload_sha256 = batch._sha256_json(short_keyring_unsigned) + short_keyring = { + **short_keyring_unsigned, + "keyring_payload_sha256": short_keyring_payload_sha256, + "root_signature_base64": base64.b64encode( + root_private_key.sign( + batch.APPROVAL_KEYRING_SIGNATURE_DOMAIN + + batch._canonical(short_keyring_unsigned) + ) + ).decode("ascii"), + } + with pytest.raises(batch.PublicationBatchError, match="keyring payload identity drift"): + batch.validate_approval_receipt( + manifest, + manifest_raw_sha, + approval, + short_keyring, + root_public_key, + ) + short_bound_payload = { + **approval_payload, + "approval_keyring_payload_sha256": short_keyring_payload_sha256, + } + short_bound_signature = private_key.sign( + batch.APPROVAL_SIGNATURE_DOMAIN + batch._canonical(short_bound_payload) + ) + short_bound_approval = { + "schema_version": batch.APPROVAL_SCHEMA_VERSION, + "approval_id": ( + "rc06-batch-approval-" + + batch._sha256_bytes( + batch._canonical(short_bound_payload) + short_bound_signature + )[:24] + ), + "payload": short_bound_payload, + "signature": base64.b64encode(short_bound_signature).decode("ascii"), + } + with pytest.raises(batch.PublicationBatchError, match="exceeds the trusted keyring window"): + batch.validate_approval_receipt( + manifest, + manifest_raw_sha, + short_bound_approval, + short_keyring, + root_public_key, + ) untrusted_keyring = copy.deepcopy(keyring) root_signature = bytearray(base64.b64decode(untrusted_keyring["root_signature_base64"])) root_signature[0] ^= 1 @@ -695,6 +945,7 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe with pytest.raises(batch.PublicationBatchError, match="root signature is invalid"): batch.validate_approval_receipt( manifest, + manifest_raw_sha, approval, untrusted_keyring, root_public_key, @@ -746,6 +997,7 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe } assert batch.validate_apply_receipt( manifest, + manifest_raw_sha, approval, keyring, root_public_key, @@ -754,11 +1006,34 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe apply_receipt, ) == apply_receipt + boundary_apply = copy.deepcopy(apply_receipt) + boundary_apply["payload"]["applied_at"] = approval_payload["expires_at"] + boundary_signature = apply_service_private_key.sign( + batch.APPLY_SIGNATURE_DOMAIN + batch._canonical(boundary_apply["payload"]) + ) + boundary_apply["signature"] = base64.b64encode(boundary_signature).decode("ascii") + boundary_apply["receipt_id"] = ( + "rc06-apply-receipt-" + + batch._sha256_bytes(batch._canonical(boundary_apply["payload"]) + boundary_signature)[:24] + ) + with pytest.raises(batch.PublicationBatchError, match="outside the approval window"): + batch.validate_apply_receipt( + manifest, + manifest_raw_sha, + approval, + keyring, + root_public_key, + apply_service_public_key, + approval_raw_sha, + boundary_apply, + ) + another_reviewer = copy.deepcopy(approval) another_reviewer["payload"]["approver"]["approver_id"] = "another-reviewer" with pytest.raises(batch.PublicationBatchError, match="sole release reviewer"): batch.validate_approval_receipt( manifest, + manifest_raw_sha, another_reviewer, keyring, root_public_key, @@ -769,6 +1044,7 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe with pytest.raises(batch.PublicationBatchError, match="74-item online evidence drift"): batch.validate_apply_receipt( manifest, + manifest_raw_sha, approval, keyring, root_public_key, @@ -785,6 +1061,7 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe with pytest.raises(batch.PublicationBatchError, match="service signature verification failed"): batch.validate_apply_receipt( manifest, + manifest_raw_sha, approval, keyring, root_public_key, @@ -797,6 +1074,734 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe assert manifest["publication_control"]["release_ready"] is False +def _prepare_signer_trust( + tmp_path: Path, + inventory: dict, + monkeypatch: pytest.MonkeyPatch, + *, + resolved: bool = True, +) -> dict: + state_directory = tmp_path / "release-signer" + key_directory = state_directory / "keys" + passphrase = "correct horse battery staple" + initialized = signer.initialize_reviewer_key( + key_directory, + passphrase=passphrase, + created_at=datetime(2026, 9, 27, 1, 0, tzinfo=UTC), + ) + registration, reviewer_public_key = signer._load_public_registration(key_directory) + root_private_key = Ed25519PrivateKey.generate() + root_public_key = root_private_key.public_key() + root_raw = root_public_key.public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw, + ) + manifest = _manifest(inventory, resolved=resolved) + manifest_raw = signer.batch._canonical(manifest) + keyring_unsigned = { + "schema_version": signer.batch.APPROVAL_KEYRING_SCHEMA_VERSION, + "root_key_id": signer.batch.TRUSTED_APPROVAL_ROOT_KEY_ID, + "epoch": 1, + "issued_at": "2026-09-27T00:00:00Z", + "expires_at": "2026-10-31T23:59:59Z", + "authorized_batch_manifest_raw_sha256": signer.batch._sha256_bytes(manifest_raw), + "keys": [ + { + "key_id": registration["key_id"], + "algorithm": "Ed25519", + "public_key_pem": registration["public_key_pem"], + **signer.batch.SOLE_RELEASE_REVIEWER, + "not_before": "2026-09-27T00:00:00Z", + "not_after": "2026-10-31T23:59:59Z", + "revoked": False, + } + ], + } + keyring_payload_sha256 = signer.batch._sha256_json(keyring_unsigned) + keyring = { + **keyring_unsigned, + "keyring_payload_sha256": keyring_payload_sha256, + "root_signature_base64": base64.b64encode( + root_private_key.sign( + signer.batch.APPROVAL_KEYRING_SIGNATURE_DOMAIN + + signer.batch._canonical(keyring_unsigned) + ) + ).decode("ascii"), + } + monkeypatch.setattr( + signer.batch, + "TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256", + signer.batch._sha256_bytes(root_raw), + ) + monkeypatch.setattr(signer.batch, "FROZEN_APPROVAL_KEYRING_EPOCH", 1) + keyring_raw = signer.batch._canonical(keyring) + material_directory = tmp_path / "request-inputs" + material_directory.mkdir() + manifest_file = material_directory / "manifest.json" + keyring_file = material_directory / "keyring.json" + root_file = material_directory / "root.raw" + manifest_file.write_bytes(manifest_raw) + keyring_file.write_bytes(keyring_raw) + root_file.write_bytes(root_raw) + return { + "state_directory": state_directory, + "key_directory": key_directory, + "passphrase": passphrase, + "initialized": initialized, + "registration": registration, + "reviewer_public_key": reviewer_public_key, + "manifest": manifest, + "manifest_raw": manifest_raw, + "manifest_file": manifest_file, + "keyring": keyring, + "keyring_raw": keyring_raw, + "keyring_file": keyring_file, + "root_raw": root_raw, + "root_file": root_file, + "root_private_key": root_private_key, + } + + +def test_single_reviewer_signer_initializes_only_an_encrypted_private_key(tmp_path: Path) -> None: + key_directory = tmp_path / "release-signer" / "keys" + result = signer.initialize_reviewer_key( + key_directory, + passphrase="correct horse battery staple", + created_at=datetime(2026, 9, 27, 1, 0, tzinfo=UTC), + ) + private_raw = (key_directory / signer.PRIVATE_KEY_FILENAME).read_bytes() + assert result["status"] == "initialized_not_approved" + assert private_raw.startswith(b"-----BEGIN ENCRYPTED PRIVATE KEY-----") + assert b"BEGIN PRIVATE KEY" not in private_raw # pragma: allowlist secret + registration, private_key = signer._load_private_key( + key_directory, + "correct horse battery staple", + ) + assert isinstance(private_key, Ed25519PrivateKey) + assert registration["reviewer"] == signer.batch.SOLE_RELEASE_REVIEWER + assert registration["key_id"] == result["key_id"] + assert "private" not in json.dumps(result, ensure_ascii=False).casefold() + with pytest.raises(signer.SignerError, match="password is incorrect"): + signer._load_private_key(key_directory, "this password is wrong") + repeated = signer.initialize_reviewer_key( + key_directory, + passphrase="correct horse battery staple", + ) + assert repeated["status"] == "already_initialized_and_verified" + assert repeated["key_id"] == result["key_id"] + + +def test_single_reviewer_signer_rejects_weak_password_and_repository_storage(tmp_path: Path) -> None: + weak_values = ( + "short", + " " * 20, + "a" * 20, + "1234567890" * 4, + "abcd" * 5, + "password1234567890abc", + "qwerty phrase that is long", + ) + for index, value in enumerate(weak_values): + with pytest.raises(signer.SignerError): + signer.initialize_reviewer_key( + tmp_path / f"weak-reviewer-key-{index}", + passphrase=value, + ) + with pytest.raises(signer.SignerError, match="outside the source repository"): + signer.initialize_reviewer_key( + REPO_ROOT / ".runtime" / "unsafe-reviewer-key", + passphrase="correct horse battery staple", + ) + + +def test_single_reviewer_signer_prepares_signs_and_self_verifies_exact_batch( + tmp_path: Path, + inventory: dict, + monkeypatch: pytest.MonkeyPatch, +) -> None: + materials = _prepare_signer_trust(tmp_path, inventory, monkeypatch) + now = datetime(2026, 9, 27, 2, 0, tzinfo=UTC) + prepared = signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=materials["manifest_file"], + manifest_sha256=signer.batch._sha256_bytes(materials["manifest_raw"]), + approval_keyring_file=materials["keyring_file"], + approval_keyring_sha256=signer.batch._sha256_bytes(materials["keyring_raw"]), + approval_root_public_key_file=materials["root_file"], + now=now, + ) + request_file = Path(prepared["request_file"]) + loaded = signer._load_request_materials(request_file, now=now) + summary = signer.batch.build_approval_review_summary( + loaded["manifest"], + manifest_raw_sha256=loaded["manifest_raw_sha256"], + ) + code = signer._approval_code(signer.batch._sha256_json(summary)) + output = tmp_path / "signed-receipt" + result = signer.sign_review_request( + request_file, + materials["key_directory"], + output, + passphrase=materials["passphrase"], + confirmation_code=code, + now=now, + ) + assert result["status"] == "signed_and_self_verified_not_published" + assert result["release_ready"] is False + assert result["public_traffic_authorized"] is False + assert result["manifest_raw_sha256"] == signer.batch._sha256_bytes(materials["manifest_raw"]) + approval_raw = (output / signer.APPROVAL_FILENAME).read_bytes() + assert result["approval_raw_sha256"] == signer.batch._sha256_bytes(approval_raw) + approval = json.loads(approval_raw) + assert approval["payload"]["batch_manifest_raw_sha256"] == result["manifest_raw_sha256"] + assert approval["payload"]["review_summary_sha256"] == result["review_summary_sha256"] + assert approval["payload"]["approver"] == { + **signer.batch.SOLE_RELEASE_REVIEWER, + "human_confirmed": True, + } + assert (output / signer.SUMMARY_TEXT_FILENAME).read_text(encoding="utf-8").find( + materials["manifest"]["candidate"]["commit"] + ) >= 0 + + +def test_single_reviewer_signer_fails_closed_on_raw_hash_password_confirmation_and_pins( + tmp_path: Path, + inventory: dict, + monkeypatch: pytest.MonkeyPatch, +) -> None: + materials = _prepare_signer_trust(tmp_path, inventory, monkeypatch) + now = datetime(2026, 9, 27, 2, 0, tzinfo=UTC) + with pytest.raises(signer.SignerError, match="raw SHA-256 mismatch"): + signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=materials["manifest_file"], + manifest_sha256=materials["manifest"]["manifest_payload_sha256"], + approval_keyring_file=materials["keyring_file"], + approval_keyring_sha256=signer.batch._sha256_bytes(materials["keyring_raw"]), + approval_root_public_key_file=materials["root_file"], + now=now, + ) + prepared = signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=materials["manifest_file"], + manifest_sha256=signer.batch._sha256_bytes(materials["manifest_raw"]), + approval_keyring_file=materials["keyring_file"], + approval_keyring_sha256=signer.batch._sha256_bytes(materials["keyring_raw"]), + approval_root_public_key_file=materials["root_file"], + now=now, + ) + request_file = Path(prepared["request_file"]) + loaded = signer._load_request_materials(request_file, now=now) + summary = signer.batch.build_approval_review_summary( + loaded["manifest"], + manifest_raw_sha256=loaded["manifest_raw_sha256"], + ) + correct_code = signer._approval_code(signer.batch._sha256_json(summary)) + for output, password, code, error in ( + (tmp_path / "wrong-password", "this password is wrong", correct_code, "password is incorrect"), + (tmp_path / "wrong-code", materials["passphrase"], "000000", "approval code"), + ): + with pytest.raises(signer.SignerError, match=error): + signer.sign_review_request( + request_file, + materials["key_directory"], + output, + passphrase=password, + confirmation_code=code, + now=now, + ) + assert not output.exists() + monkeypatch.setattr(signer.batch, "TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256", "") + with pytest.raises(signer.SignerError, match="pin is not provisioned"): + signer.sign_review_request( + request_file, + materials["key_directory"], + tmp_path / "missing-pin", + passphrase=materials["passphrase"], + confirmation_code=correct_code, + now=now, + ) + assert not (tmp_path / "missing-pin").exists() + + +def test_single_reviewer_signer_rejects_unresolved_dispositions( + tmp_path: Path, + inventory: dict, + monkeypatch: pytest.MonkeyPatch, +) -> None: + materials = _prepare_signer_trust( + tmp_path, + inventory, + monkeypatch, + resolved=False, + ) + with pytest.raises(signer.SignerError, match="31 Golden dispositions"): + signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=materials["manifest_file"], + manifest_sha256=signer.batch._sha256_bytes(materials["manifest_raw"]), + approval_keyring_file=materials["keyring_file"], + approval_keyring_sha256=signer.batch._sha256_bytes(materials["keyring_raw"]), + approval_root_public_key_file=materials["root_file"], + now=datetime(2026, 9, 27, 2, 0, tzinfo=UTC), + ) + + +def test_root_signed_keyring_authorizes_only_the_exact_manifest_without_a_cyclic_pin( + tmp_path: Path, + inventory: dict, + monkeypatch: pytest.MonkeyPatch, +) -> None: + materials = _prepare_signer_trust(tmp_path, inventory, monkeypatch) + assert not hasattr(signer.batch, "TRUSTED_APPROVAL_KEYRING_PAYLOAD_SHA256") + unsigned = { + key: value + for key, value in materials["keyring"].items() + if key not in {"keyring_payload_sha256", "root_signature_base64"} + } + unsigned["authorized_batch_manifest_raw_sha256"] = "f" * 64 + payload_sha = signer.batch._sha256_json(unsigned) + wrong_keyring = { + **unsigned, + "keyring_payload_sha256": payload_sha, + "root_signature_base64": base64.b64encode( + materials["root_private_key"].sign( + signer.batch.APPROVAL_KEYRING_SIGNATURE_DOMAIN + + signer.batch._canonical(unsigned) + ) + ).decode("ascii"), + } + wrong_keyring_raw = signer.batch._canonical(wrong_keyring) + wrong_keyring_file = tmp_path / "wrong-keyring.json" + wrong_keyring_file.write_bytes(wrong_keyring_raw) + with pytest.raises(signer.SignerError, match="does not authorize the exact batch manifest"): + signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=materials["manifest_file"], + manifest_sha256=signer.batch._sha256_bytes(materials["manifest_raw"]), + approval_keyring_file=wrong_keyring_file, + approval_keyring_sha256=signer.batch._sha256_bytes(wrong_keyring_raw), + approval_root_public_key_file=materials["root_file"], + now=datetime(2026, 9, 27, 2, 0, tzinfo=UTC), + ) + + +def test_prepare_request_is_idempotent_and_rotates_only_an_expired_window( + tmp_path: Path, + inventory: dict, + monkeypatch: pytest.MonkeyPatch, +) -> None: + materials = _prepare_signer_trust(tmp_path, inventory, monkeypatch) + now = datetime(2026, 9, 27, 2, 0, tzinfo=UTC) + + def prepare(at: datetime) -> dict: + return signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=materials["manifest_file"], + manifest_sha256=signer.batch._sha256_bytes(materials["manifest_raw"]), + approval_keyring_file=materials["keyring_file"], + approval_keyring_sha256=signer.batch._sha256_bytes(materials["keyring_raw"]), + approval_root_public_key_file=materials["root_file"], + now=at, + ) + + first = prepare(now) + first_raw = Path(first["request_file"]).read_bytes() + repeated = prepare(now + timedelta(seconds=1)) + assert repeated["status"] == "review_request_already_prepared_not_signed" + assert Path(repeated["request_file"]).read_bytes() == first_raw + + renewed = prepare(now + timedelta(days=8)) + assert renewed["status"] == "review_request_prepared_not_signed" + assert renewed["request_id"] == first["request_id"] + assert Path(renewed["request_file"]).read_bytes() != first_raw + expired = list((materials["state_directory"] / "expired").glob("*/release-review-request.json")) + assert len(expired) == 1 + + +def test_interactive_signing_retires_an_unrelated_expired_candidate_before_discovery( + tmp_path: Path, + inventory: dict, + monkeypatch: pytest.MonkeyPatch, +) -> None: + materials = _prepare_signer_trust(tmp_path, inventory, monkeypatch) + first_time = datetime(2026, 9, 27, 2, 0, tzinfo=UTC) + first = signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=materials["manifest_file"], + manifest_sha256=signer.batch._sha256_bytes(materials["manifest_raw"]), + approval_keyring_file=materials["keyring_file"], + approval_keyring_sha256=signer.batch._sha256_bytes(materials["keyring_raw"]), + approval_root_public_key_file=materials["root_file"], + now=first_time, + ) + + second_manifest = copy.deepcopy(materials["manifest"]) + second_manifest["candidate"]["commit"] = "b" * 40 + second_manifest["candidate"]["tag"] = "v2-fixture-rotated" + second_manifest["manifest_payload_sha256"] = signer.batch._sha256_json( + { + key: value + for key, value in second_manifest.items() + if key != "manifest_payload_sha256" + } + ) + second_manifest = signer.batch.validate_manifest(second_manifest) + second_manifest_raw = signer.batch._canonical(second_manifest) + second_keyring_unsigned = { + key: copy.deepcopy(value) + for key, value in materials["keyring"].items() + if key not in {"keyring_payload_sha256", "root_signature_base64"} + } + second_keyring_unsigned["authorized_batch_manifest_raw_sha256"] = ( + signer.batch._sha256_bytes(second_manifest_raw) + ) + second_keyring = { + **second_keyring_unsigned, + "keyring_payload_sha256": signer.batch._sha256_json(second_keyring_unsigned), + "root_signature_base64": base64.b64encode( + materials["root_private_key"].sign( + signer.batch.APPROVAL_KEYRING_SIGNATURE_DOMAIN + + signer.batch._canonical(second_keyring_unsigned) + ) + ).decode("ascii"), + } + second_keyring_raw = signer.batch._canonical(second_keyring) + second_inputs = tmp_path / "request-inputs-b" + second_inputs.mkdir() + second_manifest_file = second_inputs / "manifest.json" + second_keyring_file = second_inputs / "keyring.json" + second_manifest_file.write_bytes(second_manifest_raw) + second_keyring_file.write_bytes(second_keyring_raw) + second_time = first_time + timedelta(days=8) + second = signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=second_manifest_file, + manifest_sha256=signer.batch._sha256_bytes(second_manifest_raw), + approval_keyring_file=second_keyring_file, + approval_keyring_sha256=signer.batch._sha256_bytes(second_keyring_raw), + approval_root_public_key_file=materials["root_file"], + now=second_time, + ) + assert first["request_id"] != second["request_id"] + assert len(list((materials["state_directory"] / "inbox").glob("*/release-review-request.json"))) == 2 + + second_request_file = Path(second["request_file"]) + loaded = signer._load_request_materials(second_request_file, now=second_time) + summary = signer.batch.build_approval_review_summary( + loaded["manifest"], manifest_raw_sha256=loaded["manifest_raw_sha256"] + ) + code = signer._approval_code(signer.batch._sha256_json(summary)) + monkeypatch.setattr(signer, "_require_interactive", lambda: None) + monkeypatch.setattr(signer, "_now_utc", lambda: second_time) + monkeypatch.setattr("builtins.input", lambda _prompt: code) + monkeypatch.setattr(signer.getpass, "getpass", lambda _prompt: materials["passphrase"]) + signed = signer._run_sign(materials["state_directory"]) + + assert signed["request_id"] == second["request_id"] + assert signed["status"] == "signed_and_self_verified_not_published" + assert list( + (materials["state_directory"] / "expired").glob( + f"{first['request_id']}-*/release-review-request.json" + ) + ) + assert ( + materials["state_directory"] + / "processed" + / second["request_id"] + / signer.REQUEST_FILENAME + ).is_file() + + +def test_interactive_signing_rechecks_time_after_confirmation_and_leaves_no_receipt_when_expired( + tmp_path: Path, + inventory: dict, + monkeypatch: pytest.MonkeyPatch, +) -> None: + materials = _prepare_signer_trust(tmp_path, inventory, monkeypatch) + display_time = datetime(2026, 9, 27, 2, 0, tzinfo=UTC) + prepared = signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=materials["manifest_file"], + manifest_sha256=signer.batch._sha256_bytes(materials["manifest_raw"]), + approval_keyring_file=materials["keyring_file"], + approval_keyring_sha256=signer.batch._sha256_bytes(materials["keyring_raw"]), + approval_root_public_key_file=materials["root_file"], + now=display_time, + ) + loaded = signer._load_request_materials(Path(prepared["request_file"]), now=display_time) + summary = signer.batch.build_approval_review_summary( + loaded["manifest"], + manifest_raw_sha256=loaded["manifest_raw_sha256"], + ) + code = signer._approval_code(signer.batch._sha256_json(summary)) + clock = iter((display_time, display_time + timedelta(days=8))) + monkeypatch.setattr(signer, "_require_interactive", lambda: None) + monkeypatch.setattr(signer, "_now_utc", lambda: next(clock)) + monkeypatch.setattr("builtins.input", lambda _prompt: code) + monkeypatch.setattr(signer.getpass, "getpass", lambda _prompt: materials["passphrase"]) + with pytest.raises(signer.SignerError, match="not currently valid"): + signer._run_sign(materials["state_directory"]) + assert not (materials["state_directory"] / "receipts").exists() + + +def test_interactive_signing_archives_request_and_repeat_click_only_reverifies( + tmp_path: Path, + inventory: dict, + monkeypatch: pytest.MonkeyPatch, +) -> None: + materials = _prepare_signer_trust(tmp_path, inventory, monkeypatch) + now = datetime(2026, 9, 27, 2, 0, tzinfo=UTC) + prepared = signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=materials["manifest_file"], + manifest_sha256=signer.batch._sha256_bytes(materials["manifest_raw"]), + approval_keyring_file=materials["keyring_file"], + approval_keyring_sha256=signer.batch._sha256_bytes(materials["keyring_raw"]), + approval_root_public_key_file=materials["root_file"], + now=now, + ) + request_file = Path(prepared["request_file"]) + loaded = signer._load_request_materials(request_file, now=now) + summary = signer.batch.build_approval_review_summary( + loaded["manifest"], + manifest_raw_sha256=loaded["manifest_raw_sha256"], + ) + code = signer._approval_code(signer.batch._sha256_json(summary)) + monkeypatch.setattr(signer, "_require_interactive", lambda: None) + monkeypatch.setattr(signer, "_now_utc", lambda: now) + monkeypatch.setattr("builtins.input", lambda _prompt: code) + monkeypatch.setattr(signer.getpass, "getpass", lambda _prompt: materials["passphrase"]) + signed = signer._run_sign(materials["state_directory"]) + assert signed["status"] == "signed_and_self_verified_not_published" + assert not request_file.exists() + processed_request = ( + materials["state_directory"] / "processed" / prepared["request_id"] / signer.REQUEST_FILENAME + ) + assert processed_request.is_file() + + def unexpected_prompt(_prompt: str) -> str: + raise AssertionError("repeat signing must not ask for a code or password") + + monkeypatch.setattr("builtins.input", unexpected_prompt) + monkeypatch.setattr(signer.getpass, "getpass", unexpected_prompt) + repeated = signer._run_sign(materials["state_directory"]) + assert repeated["status"] == "already_signed_and_verified_not_published" + prepared_again = signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=materials["manifest_file"], + manifest_sha256=signer.batch._sha256_bytes(materials["manifest_raw"]), + approval_keyring_file=materials["keyring_file"], + approval_keyring_sha256=signer.batch._sha256_bytes(materials["keyring_raw"]), + approval_root_public_key_file=materials["root_file"], + now=now + timedelta(days=40), + ) + assert prepared_again["status"] == "already_signed_and_verified_not_published" + + +def test_existing_receipt_reverification_rejects_rewritten_review_summary( + tmp_path: Path, + inventory: dict, + monkeypatch: pytest.MonkeyPatch, +) -> None: + materials = _prepare_signer_trust(tmp_path, inventory, monkeypatch) + now = datetime(2026, 9, 27, 2, 0, tzinfo=UTC) + prepared = signer.prepare_review_request( + state_directory=materials["state_directory"], + manifest_file=materials["manifest_file"], + manifest_sha256=signer.batch._sha256_bytes(materials["manifest_raw"]), + approval_keyring_file=materials["keyring_file"], + approval_keyring_sha256=signer.batch._sha256_bytes(materials["keyring_raw"]), + approval_root_public_key_file=materials["root_file"], + now=now, + ) + request_file = Path(prepared["request_file"]) + loaded = signer._load_request_materials(request_file, now=now) + summary = signer.batch.build_approval_review_summary( + loaded["manifest"], manifest_raw_sha256=loaded["manifest_raw_sha256"] + ) + output = signer._receipt_directory(materials["state_directory"], loaded["request"]) + signer.sign_review_request( + request_file, + materials["key_directory"], + output, + passphrase=materials["passphrase"], + confirmation_code=signer._approval_code(signer.batch._sha256_json(summary)), + now=now, + ) + (output / signer.SUMMARY_JSON_FILENAME).write_bytes(b"{}") + (output / signer.SUMMARY_TEXT_FILENAME).write_text("FAKE SUMMARY\n", encoding="utf-8") + output_files = { + name: (output / name).read_bytes() + for name in { + signer.APPROVAL_FILENAME, + signer.APPROVAL_SHA_FILENAME, + signer.SUMMARY_JSON_FILENAME, + signer.SUMMARY_TEXT_FILENAME, + signer.SIGNING_RESULT_FILENAME, + } + } + (output / signer.BUNDLE_COMPLETE_FILENAME).write_bytes( + signer._bundle_completion(output_files) + ) + with pytest.raises(signer.SignerError, match="review summary"): + signer._load_existing_signing_result( + materials["state_directory"], request_file, output + ) + + +def test_bundle_publication_is_complete_no_replace_and_concurrent_safe(tmp_path: Path) -> None: + destination = tmp_path / "bundle" + barrier = threading.Barrier(2) + outcomes: list[tuple[str, object]] = [] + + def writer(value: bytes) -> None: + barrier.wait() + try: + signer._write_bundle_atomically( + destination, + {"value.bin": value}, + private_directory=True, + ) + outcomes.append(("ok", value)) + except Exception as exc: # noqa: BLE001 - the assertion inspects the loser + outcomes.append(("error", exc)) + + threads = [threading.Thread(target=writer, args=(value,)) for value in (b"first", b"second")] + for thread in threads: + thread.start() + for thread in threads: + thread.join(timeout=60) + assert all(not thread.is_alive() for thread in threads) + assert [kind for kind, _ in outcomes].count("ok") == 1 + errors = [value for kind, value in outcomes if kind == "error"] + assert len(errors) == 1 and isinstance(errors[0], signer.SignerError) + signer._verify_complete_bundle(destination, {"value.bin"}, "concurrent fixture") + assert (destination / "value.bin").read_bytes() in {b"first", b"second"} + assert not list(tmp_path.glob(".bundle.*")) + + +@pytest.mark.skipif(os.name != "nt", reason="Windows ACL regression") +def test_private_key_file_acl_removes_an_explicit_everyone_allow( + tmp_path: Path, +) -> None: + key_directory = tmp_path / "release-signer" / "keys" + signer.initialize_reviewer_key( + key_directory, + passphrase="correct horse battery staple", + ) + private_file = key_directory / signer.PRIVATE_KEY_FILENAME + icacls = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32" / "icacls.exe" + granted = subprocess.run( + [os.fspath(icacls), os.fspath(private_file), "/grant", "*S-1-1-0:(R)"], + check=False, + capture_output=True, + text=True, + timeout=30, + ) + assert granted.returncode == 0 + signer._load_private_key(key_directory, "correct horse battery staple") + powershell = ( + Path(os.environ.get("SystemRoot", r"C:\Windows")) + / "System32" + / "WindowsPowerShell" + / "v1.0" + / "powershell.exe" + ) + environment = os.environ.copy() + environment["YUQEI_ACL_TEST_TARGET"] = os.fspath(private_file) + current_sid = signer._windows_current_sid() + inspected = subprocess.run( + [ + os.fspath(powershell), + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-Command", + "$a=[IO.File]::GetAccessControl($env:YUQEI_ACL_TEST_TARGET);" + "'OWNER=' + $a.GetOwner([Security.Principal.SecurityIdentifier]).Value;" + "$a.Access|%{$_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value}", + ], + check=False, + capture_output=True, + text=True, + timeout=30, + env=environment, + ) + assert inspected.returncode == 0 + acl_lines = set(inspected.stdout.split()) + assert f"OWNER={current_sid}" in acl_lines + assert acl_lines - {f"OWNER={current_sid}"} == {current_sid, "S-1-5-18"} + + environment["YUQEI_ACL_TEST_TARGET"] = os.fspath(key_directory) + directory_owner = subprocess.run( + [ + os.fspath(powershell), + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-Command", + "$a=[IO.Directory]::GetAccessControl($env:YUQEI_ACL_TEST_TARGET);" + "$a.GetOwner([Security.Principal.SecurityIdentifier]).Value", + ], + check=False, + capture_output=True, + text=True, + timeout=30, + env=environment, + ) + assert directory_owner.returncode == 0 + assert directory_owner.stdout.strip() == current_sid + + +@pytest.mark.skipif(os.name != "nt", reason="Windows one-click launcher regression") +def test_windows_launcher_uses_isolated_runtime_and_ignores_pythonpath( + tmp_path: Path, +) -> None: + marker = tmp_path / "sitecustomize-loaded.txt" + malicious = tmp_path / "malicious" + malicious.mkdir() + (malicious / "sitecustomize.py").write_text( + f"from pathlib import Path\nPath({str(marker)!r}).write_text('loaded')\n", + encoding="utf-8", + ) + powershell = ( + Path(os.environ.get("SystemRoot", r"C:\Windows")) + / "System32" + / "WindowsPowerShell" + / "v1.0" + / "powershell.exe" + ) + launcher = SIGNER_SCRIPT.with_name("Launch-ReleaseSigner.ps1") + environment = os.environ.copy() + environment["PYTHONPATH"] = os.fspath(malicious) + completed = subprocess.run( + [ + os.fspath(powershell), + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + os.fspath(launcher), + "runtime-check", + ], + check=False, + capture_output=True, + text=True, + timeout=60, + env=environment, + ) + assert completed.returncode == 0, completed.stderr or completed.stdout + assert json.loads(completed.stdout.splitlines()[-1])["status"] == "isolated_runtime_verified" + assert not marker.exists() + for name in ("Initialize-ReleaseSigner.cmd", "Approve-ReleaseCandidate.cmd"): + raw = SIGNER_SCRIPT.with_name(name).read_text(encoding="utf-8") + assert "Launch-ReleaseSigner.ps1" in raw + assert "where python" not in raw.casefold() + + def test_secure_reader_rejects_parent_traversal_and_symlink(tmp_path: Path) -> None: target = tmp_path / "target.json" target.write_text(json.dumps({"ok": True}), encoding="utf-8") diff --git a/yuqei-infra/tests/test_release_candidate_manifest_tools.py b/yuqei-infra/tests/test_release_candidate_manifest_tools.py index d16c8104..5e91eb27 100644 --- a/yuqei-infra/tests/test_release_candidate_manifest_tools.py +++ b/yuqei-infra/tests/test_release_candidate_manifest_tools.py @@ -300,7 +300,15 @@ RUN python -c "import platform; name, version = platform.libc_ver(); minimum=(2, ("project-docs/report-TP03-run.json", "generated_evidence_retain_exclude"), ("project-docs/V4-website-shell-redesign-2026-08-28.md", "generated_evidence_retain_exclude"), ("project-docs/上线收口-执行约束-2026-09-10.md", "release_evidence_compact_include"), + ( + "project-docs/RC06-single-reviewer-one-click-signing-runbook-2026-09-27.md", + "release_evidence_compact_include", + ), ("project-docs/unreviewed-plan.md", "needs_manual_review"), + ("yuqei-infra/scripts/rc06-single-reviewer-signing.py", "release_source_include"), + ("yuqei-infra/scripts/Launch-ReleaseSigner.ps1", "release_source_include"), + ("yuqei-infra/scripts/Initialize-ReleaseSigner.cmd", "release_source_include"), + ("yuqei-infra/scripts/Approve-ReleaseCandidate.cmd", "release_source_include"), ("yuqei-infra/config/.env.example", "release_source_include"), ( "yuqei-infra/release-trust/third-party-materials-baseline.json",