Răsfoiți Sursa

fix(desktop): sign bundled Windows runtime before validation

winewill 3 săptămâni în urmă
părinte
comite
b37687c141

+ 2 - 2
.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.md
-2026-09-14-desktop-primary-runtime.md: 17d489e6c788c786cefcdddf5f1983ac48522bfe
-2026-09-14-desktop-primary-runtime.zh.md: f8be37801452ebf4f4623a1ad0342de770e21ad5
+2026-09-14-desktop-primary-runtime.md: 334b9a1aaa21021c8be4f3df42e2ada9e986d0f9
+2026-09-14-desktop-primary-runtime.zh.md: 1353aab6f1d8e91d6bc700dc16c0a17983ee7707

+ 4 - 0
.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.md

@@ -18,6 +18,8 @@ Node downloads and hash-verifies the complete locked wheel set and unpacks these
 
 macOS grants `com.apple.security.cs.allow-jit` only to the standalone Node executable. Hardened-runtime signing without that entitlement prevents V8 from allocating its code region. Interpreter and library smoke checks run after signing as well as after staging cleanup; a valid signature alone does not establish executable behavior.
 
+Windows signed packaging separates materialization from execution with a supervised primary-runtime signing stage. PE inspection excludes foreign-platform Node addons and refuses directory links. Valid vendor signatures remain intact; only unsigned files receive the configured EV signature. Invalid existing signatures fail before hardware access, and each new signature is checked for validity, timestamp and certificate identity before the next file. The existing per-user interlock, serialized signer and redacted journal own hardware calls; no failure permits a retry or later stage. Runtime execution receives no signing credentials and follows complete verification. Development and unsigned preparation retain native smoke without automatic hardware access.
+
 Desktop ZIP extraction pins `extract-zip` to `yauzl` 3.4.0 through a scoped dependency override. The 2.x reader can leave large deflate entries unfinished on Node 26 ([upstream issue](https://github.com/thejoshwolfe/yauzl/issues/176)); retaining the existing extractor preserves its path validation and wheel-entry checks. The development launcher uses top-level await so unfinished preparation cannot exit successfully. A large compressed wheel regression checks the complete extracted bytes.
 
 ## Alternatives considered
@@ -28,6 +30,8 @@ Desktop ZIP extraction pins `extract-zip` to `yauzl` 3.4.0 through a scoped depe
 
 **Independent updates and version-named directories.** Runtime releases are coupled to Desktop, and the requested installation location is stable.
 
+**Signing only the interpreter or bypassing native smoke.** Windows code integrity also evaluates DLLs and Python extensions. A signed launcher cannot make an unsigned extension load, and skipping execution would hide unusable installed dependencies. Preserving valid upstream signatures avoids unnecessary hardware operations and retains upstream attribution.
+
 ## Consequences
 
 The application carries additional native files and replaces the complete managed payload on upgrade. Running interpreters can prevent replacement on Windows. Native build smoke, install/reuse/recovery tests and a keyless tool-error session cover distinct installation and model-output paths; macOS signing uses the existing native-runtime signer. Interpreter archives and Python wheels are hash-pinned, and licenses remain with their distributions.

+ 4 - 0
.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.zh.md

@@ -18,6 +18,8 @@ Node 下载并校验完整锁定 wheel 集的哈希,将这些仅含库的压
 
 macOS 仅向独立 Node 可执行文件授予 `com.apple.security.cs.allow-jit`。缺少此权限的强化运行时签名会阻止 V8 分配代码区域。解释器和库的 smoke 检查在签名后以及暂存清理后执行;签名有效本身不能证明程序可运行。
 
+Windows 签名打包通过受监督的第一方运行时签名阶段,将文件准备与执行分开。PE 检查排除其他平台的 Node 插件,并拒绝目录链接。有效的上游签名保持不变;仅未签名文件使用配置的 EV 证书签名。已有签名无效时,在访问硬件前失败;每个新签名通过有效性、时间戳和证书身份检查后,才处理下一个文件。硬件调用复用现有的用户级互锁、串行签名器和脱敏日志;任何失败都不允许重试或继续后续阶段。运行时执行不接收签名凭据,并在完整验签后进行。开发和未签名准备保留本机 smoke,不自动访问硬件。
+
 Desktop ZIP 解压通过定向依赖覆盖为 `extract-zip` 固定 `yauzl` 3.4.0。2.x 读取器在 Node 26 上可能无法完成较大 deflate 条目的读取([上游问题](https://github.com/thejoshwolfe/yauzl/issues/176));保留现有解压器可保留其路径校验和 wheel 条目检查。开发启动器使用顶层 await,避免准备未完成却成功退出。大压缩 wheel 回归测试检查完整的解压字节。
 
 ## Alternatives considered
@@ -28,6 +30,8 @@ Desktop ZIP 解压通过定向依赖覆盖为 `extract-zip` 固定 `yauzl` 3.4.0
 
 **独立更新和版本目录。** Runtime 发布与 Desktop 绑定,且请求的安装位置固定。
 
+**仅签名解释器或跳过本机 smoke。** Windows 代码完整性也检查 DLL 和 Python 扩展。启动程序有签名不能让未签名扩展被加载,跳过执行会隐藏安装后不可用的依赖。保留有效上游签名既减少硬件操作,也保留上游归属信息。
+
 ## Consequences
 
 应用携带额外的原生文件,并在升级时替换完整受管产物。Windows 上运行中的解释器可能阻止替换。本机构建 smoke、安装与复用及恢复测试、无密钥工具错误会话分别覆盖安装和模型输出路径;macOS 签名复用现有原生 Runtime 签名器。解释器压缩包和 Python wheel 固定哈希,许可证随各分发包保留。

+ 2 - 2
apps/desktop/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/desktop/README.md
-README.md: 41fb52394940d8468a7eee681399446425d91a07
-README.zh.md: 53188b1207a304e95c02ad34769eab0b3b7df1df
+README.md: 97ae244877b8d58b037b0a97c7620f6a19f5b9de
+README.zh.md: dcb53cab532025efc35b4eb48c438f89cfe498b9

+ 2 - 2
apps/desktop/README.md

@@ -12,7 +12,7 @@ The macOS PNG uses an inset rounded background for legacy ICNS packaging, with r
 
 ### Bundled workspace dependencies
 
-The current Windows Python payload contains unsigned native extensions. Smart App Control blocked `_decimal`, `pyexpat`, `_lzma` and `_uuid` during local validation; XML and LZMA operations fail on that host. Successful numpy/pandas smoke checks do not establish compatibility for every extension.
+Signed Windows packaging preserves valid vendor signatures and signs unsigned PE executables, DLLs, Python extensions and Node addons in the primary runtime before executing its smoke checks. Each new signature must match the configured certificate and carry a timestamp; invalid existing signatures, signing errors and verification errors stop the run without retries. Checks include decimal, XML, LZMA, UUID, numpy and pandas. Development, preparation-only and unsigned builds do not use the hardware token and can be blocked by Windows code-integrity policy; no build mode disables that policy. A passing smoke does not establish compatibility for every extension or enterprise policy.
 
 Desktop carries independent Python, Node.js and pnpm distributions, with numpy and pandas in Python's `site-packages`. The `load_workspace_dependencies` tool installs this payload offline on first use under `$DSH_HOME/dsh-runtimes/dsh-primary-runtime` (normally `~/.dsh/dsh-runtimes/dsh-primary-runtime`) and returns absolute interpreter, pnpm script and library paths. Execute the pnpm script with the returned Node executable. The returned Node library directory is reserved for bundled libraries, not pnpm's global installation directory.
 
@@ -202,7 +202,7 @@ pnpm run package:desktop:win:x64
 
 Insert and unlock the token before packaging. The electron-builder hook passes each artifact to the CRLF `scripts/windows-sign.cmd`, which invokes the configured SignTool once with `/f`, SafeNet `/kc "[{{PIN}}]=container"`, `/csp "eToken Base Cryptographic Provider"`, a SHA-256 file digest, and a DigiCert SHA-256 RFC 3161 timestamp. The hook never substitutes electron-builder's bundled SignTool and never retries a failed signing request. Windows release packaging fails instead of emitting unsigned artifacts when the SignTool, certificate, container, PIN, token, or signature is unavailable.
 
-The PIN cannot contain `]`, a quote, or a line break because those characters delimit the SafeNet `/kc` value or its CMD argument. The CMD disables delayed expansion so a PIN containing `!` reaches SafeNet unchanged. Packaging withholds every `DSH_DESKTOP_WINDOWS_*` field from build and runtime-preparation subprocesses, gives electron-builder only the four configured inputs, gives the signing CMD only the validated signing fields in an otherwise scrubbed environment, clears those fields before SignTool starts, and redacts SignTool diagnostics. SafeNet still requires the PIN in the SignTool process command line. The local `.env.windows` stores the PIN in plaintext and needs restricted file access; CI uses a temporary file and deletes it after the job. Do not commit or share its contents or print credentials in logs. Configuration checks consume no token PIN attempts; signing still stops the batch on its first failure.
+The PIN cannot contain `]`, a quote, or a line break because those characters delimit the SafeNet `/kc` value or its CMD argument. The CMD disables delayed expansion so a PIN containing `!` reaches SafeNet unchanged. Packaging withholds every `DSH_DESKTOP_WINDOWS_*` field from build and runtime-preparation subprocesses, gives the dedicated primary-runtime signing stage and electron-builder only the four configured inputs, gives the signing CMD only the validated signing fields in an otherwise scrubbed environment, clears those fields before SignTool starts, and redacts SignTool diagnostics. SafeNet still requires the PIN in the SignTool process command line. The local `.env.windows` stores the PIN in plaintext and needs restricted file access; CI uses a temporary file and deletes it after the job. Do not commit or share its contents or print credentials in logs. Configuration checks consume no token PIN attempts; signing still stops the batch on its first failure.
 
 Create a runnable application directory instead of an installer by using the matching `:dir` command, such as:
 

+ 2 - 2
apps/desktop/README.zh.md

@@ -12,7 +12,7 @@ macOS PNG 使用带留白的圆角底板,供传统 ICNS 打包使用,包含
 
 ### 内置工作区依赖
 
-当前 Windows Python 产物包含未签名的原生扩展。本机验证中,Smart App Control 阻止了 `_decimal`、`pyexpat`、`_lzma` 和 `_uuid`;该主机上的 XML 和 LZMA 操作失败。numpy/pandas 冒烟检查通过,不代表所有扩展都兼容。
+Windows 签名打包保留有效的上游签名,并在执行冒烟检查前,为第一方运行时中未签名的 PE 可执行文件、DLL、Python 扩展和 Node 插件补签。每个新签名必须匹配配置的证书且带时间戳;已有签名无效、签名错误或验签错误都会停止本轮执行,不自动重试。检查覆盖 decimal、XML、LZMA、UUID、numpy 和 pandas。开发、仅准备和未签名构建不使用硬件令牌,可能被 Windows 代码完整性策略阻止;任何构建模式都不会关闭该策略。冒烟检查通过不代表所有扩展或企业策略都兼容。
 
 Desktop 携带独立的 Python、Node.js 和 pnpm 分发包,并在 Python 的 `site-packages` 中预装 numpy 和 pandas。`load_workspace_dependencies` 工具首次使用时,将该产物离线安装到 `$DSH_HOME/dsh-runtimes/dsh-primary-runtime`(通常为 `~/.dsh/dsh-runtimes/dsh-primary-runtime`),并返回解释器、pnpm 脚本和库目录的绝对路径。pnpm 脚本通过返回的 Node 可执行文件运行。返回的 Node 库目录为随包交付的库预留,不是 pnpm 的全局安装目录。
 
@@ -202,7 +202,7 @@ pnpm run package:desktop:win:x64
 
 打包前插入并解锁 Token。electron-builder 钩子把每个产物交给采用 CRLF 的 `scripts/windows-sign.cmd`;该 CMD 只调用一次已配置的 SignTool,并指定 `/f`、SafeNet `/kc "[{{PIN}}]=容器"`、`/csp "eToken Base Cryptographic Provider"`、SHA-256 文件摘要和 DigiCert SHA-256 RFC 3161 时间戳。钩子不会改用 electron-builder 内置的 SignTool,也不会重试失败的签名请求。SignTool、证书、容器、PIN、Token 或签名不可用时,Windows 发布打包会失败,不会生成未签名产物。
 
-PIN 不能包含 `]`、引号或换行,因为这些字符用于分隔 SafeNet `/kc` 值或对应的 CMD 参数。CMD 会禁用延迟展开,因此包含 `!` 的 PIN 可以原样到达 SafeNet。打包流程不会把任何 `DSH_DESKTOP_WINDOWS_*` 字段传给构建与 运行时准备子进程;它只向 electron-builder 提供四个配置输入,在其他字段已经清理的环境中只向签名 CMD 提供经过校验的签名字段,在 SignTool 启动前清除这些字段,并遮盖 SignTool 诊断。SafeNet 仍要求 PIN 出现在 SignTool 进程命令行中。本地 `.env.windows` 明文保存 PIN,应限制文件访问权限;CI 使用临时文件并在任务结束后删除。不要提交或分享文件内容,也不要把凭据写入日志。配置检查不会消耗 Token 的 PIN 尝试次数;签名仍在首次失败后停止整批任务。
+PIN 不能包含 `]`、引号或换行,因为这些字符用于分隔 SafeNet `/kc` 值或对应的 CMD 参数。CMD 会禁用延迟展开,因此包含 `!` 的 PIN 可以原样到达 SafeNet。打包流程不会把任何 `DSH_DESKTOP_WINDOWS_*` 字段传给构建与 运行时准备子进程;它只向独立的第一方运行时签名阶段与 electron-builder 提供四个配置输入,在其他字段已经清理的环境中只向签名 CMD 提供经过校验的签名字段,在 SignTool 启动前清除这些字段,并遮盖 SignTool 诊断。SafeNet 仍要求 PIN 出现在 SignTool 进程命令行中。本地 `.env.windows` 明文保存 PIN,应限制文件访问权限;CI 使用临时文件并在任务结束后删除。不要提交或分享文件内容,也不要把凭据写入日志。配置检查不会消耗 Token 的 PIN 尝试次数;签名仍在首次失败后停止整批任务。
 
 使用对应的 `:dir` 命令可以生成可直接运行的应用目录,而不是安装包,例如:
 

+ 1 - 0
apps/desktop/package.json

@@ -14,6 +14,7 @@
     "test:updates:local": "pnpm run build && node scripts/test-local-updater.mjs",
     "prepare:runtime": "tsx scripts/prepare-runtime.ts",
     "prepare:primary-runtime": "tsx scripts/prepare-primary-runtime.ts",
+    "sign:primary-runtime": "tsx scripts/sign-primary-runtime.ts",
     "prepare:packages": "tsx scripts/prepare-package-set.ts",
     "prepare:dsh": "tsx scripts/prepare-dsh.ts",
     "prepare:package": "tsx scripts/package-target.ts --prepare-only",

+ 3 - 1
apps/desktop/scripts/package-target.ts

@@ -339,7 +339,9 @@ async function packageTarget(
     '--pack-destination',
     buildPaths.packedLandlock,
   ], buildEnv, REPOSITORY_ROOT)
-  await execute(['run', 'prepare:runtime'], targetEnv)
+  const signPrimaryRuntime = target.platform === 'win32' && !invocation.unsigned && !invocation.prepareOnly
+  await execute(['run', 'prepare:runtime', ...(signPrimaryRuntime ? ['--defer-primary-runtime-smoke'] : [])], targetEnv)
+  if (signPrimaryRuntime) await execute(['run', 'sign:primary-runtime'], electronBuilderEnv)
   await execute(['run', 'prepare:packages'], targetEnv)
   await execute(['run', 'prepare:dsh'], targetEnv)
   if (invocation.prepareOnly) return

+ 7 - 5
apps/desktop/scripts/prepare-primary-runtime.ts

@@ -11,6 +11,7 @@ import extractZip from 'extract-zip'
 import { x as extractTar } from 'tar'
 import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../../desktop-host/src/primary-runtime.ts'
 import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
+import { scrubWindowsSigningEnvironment } from './windows-sign.mjs'
 import lock from './primary-runtime-lock.json' with { type: 'json' }
 
 /**
@@ -59,9 +60,10 @@ export async function unpackPrimaryRuntimeWheel(archive: string, destination: st
 
 /**
  * Materialize the selected Desktop target's primary runtime in its build resources.
- * @returns Resolves after dependency installation and native-target execution checks.
+ * @param options - Signed Windows packaging defers execution until its supervised signing stage.
+ * @returns Resolves after materialization and, unless deferred, native-target execution checks.
  */
-export async function preparePrimaryRuntime(): Promise<void> {
+export async function preparePrimaryRuntime(options: { deferSmoke?: boolean } = {}): Promise<void> {
   const target = resolveDesktopBuildTarget()
   const paths = resolveDesktopTargetBuildPaths()
   const artifact = lock.targets[target]
@@ -111,7 +113,7 @@ export async function preparePrimaryRuntime(): Promise<void> {
   } finally {
     rmSync(staging, { recursive: true, force: true })
   }
-  smokePrimaryRuntime(join(paths.runtime, 'primary-runtime'))
+  if (!options.deferSmoke) smokePrimaryRuntime(join(paths.runtime, 'primary-runtime'))
 }
 
 /**
@@ -122,8 +124,8 @@ export function smokePrimaryRuntime(root: string): void {
   const manifest = JSON.parse(readFileSync(join(root, 'runtime.json'), 'utf8')) as PrimaryRuntimeManifest
   if (manifest.platform !== process.platform || manifest.arch !== process.arch) return
   const entries = workspaceDependencyPaths(root, manifest)
-  const options = { stdio: 'inherit', timeout: 120_000 } as const
-  execFileSync(entries.python, ['-I', '-c', 'import numpy, pandas; assert numpy.arange(4).sum() == 6; assert pandas.DataFrame({"n": [1, 2]}).n.sum() == 3'], options)
+  const options = { stdio: 'inherit', timeout: 120_000, env: scrubWindowsSigningEnvironment(process.env) } as const
+  execFileSync(entries.python, ['-I', '-c', 'import decimal, xml.parsers.expat, lzma, uuid, numpy, pandas; assert numpy.arange(4).sum() == 6; assert pandas.DataFrame({"n": [1, 2]}).n.sum() == 3'], options)
   execFileSync(entries.node, ['-e', `if (process.versions.node !== ${JSON.stringify(manifest.components.node)}) process.exit(1)`], options)
   execFileSync(entries.node, [entries.pnpm, '--version'], options)
 }

+ 3 - 1
apps/desktop/scripts/prepare-runtime.ts

@@ -4,6 +4,7 @@ import { execFileSync } from 'node:child_process'
 import { chmodSync, cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
 import { createRequire } from 'node:module'
 import { dirname, join } from 'node:path'
+import { parseArgs } from 'node:util'
 import { downloadArtifact } from '@electron/get'
 import extractZip from 'extract-zip'
 import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
@@ -25,6 +26,7 @@ function preparePnpm(): string {
 }
 
 async function main(): Promise<void> {
+  const { values } = parseArgs({ options: { 'defer-primary-runtime-smoke': { type: 'boolean', default: false } } })
   const target = resolveDesktopBuildTarget()
   const platform = target.startsWith('mac-') ? 'darwin' : 'win32'
   const arch = target.endsWith('arm64') ? 'arm64' : 'x64'
@@ -47,7 +49,7 @@ async function main(): Promise<void> {
     node: nodeVersion,
     pnpm: pnpmVersion,
   }, undefined, 2)}\n`)
-  await preparePrimaryRuntime()
+  await preparePrimaryRuntime({ deferSmoke: values['defer-primary-runtime-smoke'] })
 }
 
 await main()

+ 127 - 0
apps/desktop/scripts/sign-primary-runtime.ts

@@ -0,0 +1,127 @@
+/** Sign Windows runtime code before executing it, retaining vendor signatures and fail-stop hardware protection. */
+import { execFile } from 'node:child_process'
+import { X509Certificate } from 'node:crypto'
+import { lstat, open, readdir, readFile } from 'node:fs/promises'
+import { extname, join, resolve } from 'node:path'
+import { promisify } from 'node:util'
+import { createWindowsTokenSigner, scrubWindowsSigningEnvironment } from './windows-sign.mjs'
+import { failPackagingRun, recordPackagingEvent } from './packaging-run.mjs'
+import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
+import { smokePrimaryRuntime } from './prepare-primary-runtime.ts'
+
+interface RuntimeSignature {
+  status: string
+  timestamped: boolean
+  thumbprint: string | null
+}
+
+/**
+ * Enumerate Windows code without following links or treating foreign .node files as PE binaries.
+ * @param root - Owned, materialized runtime directory.
+ * @returns Sorted real PE files; rejects links and malformed Windows executable files.
+ */
+export async function windowsRuntimeCode(root: string): Promise<string[]> {
+  const rootStat = await lstat(root)
+  if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error('primary runtime: expected a real directory')
+  const files: string[] = []
+  for (const entry of await readdir(root, { withFileTypes: true })) {
+    const path = join(root, entry.name)
+    if (entry.isSymbolicLink()) throw new Error(`primary runtime: directory links are not signable: ${path}`)
+    if (entry.isDirectory()) { files.push(...await windowsRuntimeCode(path)); continue }
+    if (!entry.isFile() || !['.exe', '.dll', '.pyd', '.node'].includes(extname(path).toLowerCase())) continue
+    const file = await open(path, 'r')
+    let portableExecutable = false
+    let windowsCandidate = false
+    try {
+      const header = Buffer.alloc(64)
+      const { bytesRead } = await file.read(header, 0, header.length, 0)
+      windowsCandidate = bytesRead >= 2 && header.readUInt16LE(0) === 0x5a4d
+      if (bytesRead === 64 && windowsCandidate) {
+        const signature = Buffer.alloc(4)
+        const offset = header.readUInt32LE(0x3c)
+        const read = await file.read(signature, 0, 4, offset)
+        portableExecutable = offset >= 64 && read.bytesRead === 4 && signature.readUInt32LE(0) === 0x4550
+      }
+    } finally { await file.close() }
+    if (portableExecutable) files.push(path)
+    else if (windowsCandidate || extname(path).toLowerCase() !== '.node') throw new Error(`primary runtime: invalid PE file: ${path}`)
+  }
+  return files.sort()
+}
+
+async function inspectSignature(path: string): Promise<RuntimeSignature> {
+  const { stdout, stderr } = await promisify(execFile)('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
+    '$ErrorActionPreference="Stop"; $s=Get-AuthenticodeSignature -LiteralPath $env:DSH_RUNTIME_VERIFY_FILE; [pscustomobject]@{status=[string]$s.Status;timestamped=($null -ne $s.TimeStamperCertificate);thumbprint=$s.SignerCertificate.Thumbprint}|ConvertTo-Json -Compress'], {
+    env: { ...scrubWindowsSigningEnvironment(process.env), DSH_RUNTIME_VERIFY_FILE: path },
+    encoding: 'utf8', windowsHide: true, timeout: 60_000, maxBuffer: 64 * 1024,
+  })
+  const value: unknown = JSON.parse(stdout)
+  if (stderr || typeof value !== 'object' || value === null || !('status' in value) || typeof value.status !== 'string'
+    || !('timestamped' in value) || typeof value.timestamped !== 'boolean' || !('thumbprint' in value)
+    || !(value.thumbprint === null || typeof value.thumbprint === 'string' && /^[A-F\d]{40}$/iu.test(value.thumbprint))) {
+    throw new Error(`primary runtime: invalid signature inspection: ${path}`)
+  }
+  return { status: value.status, timestamped: value.timestamped, thumbprint: value.thumbprint }
+}
+
+interface RuntimeSigningOptions {
+  thumbprint: string
+  sign: ReturnType<typeof createWindowsTokenSigner>
+  inspect?: (path: string) => Promise<RuntimeSignature>
+  record: (event: object) => void
+  smoke: (root: string) => void
+}
+
+/**
+ * Preserve valid signatures and sign only unsigned PE files before runtime execution.
+ * @param root - Owned final runtime directory.
+ * @param options - Supervised signer, certificate identity, audit sink and runtime check.
+ * @returns Resolves only after sequential signatures, verification and execution; no retries.
+ */
+export async function signWindowsPrimaryRuntime(root: string, options: RuntimeSigningOptions): Promise<void> {
+  const inspect = options.inspect ?? inspectSignature
+  const files = await windowsRuntimeCode(root)
+  if (files.length === 0) throw new Error('primary runtime: no Windows code found')
+  const unsigned: string[] = []
+  for (const path of files) {
+    const signature = await inspect(path)
+    options.record({ type: 'primary-runtime-signature', path, ...signature })
+    if (signature.status === 'NotSigned') unsigned.push(path)
+    else if (signature.status !== 'Valid') throw new Error(`primary runtime: refusing ${signature.status} signature: ${path}`)
+  }
+  options.record({ type: 'primary-runtime-signing-plan', files: files.length, unsigned: unsigned.length })
+  for (const path of unsigned) {
+    await options.sign({ path, hash: 'sha256', isNest: false })
+    const signature = await inspect(path)
+    if (signature.status !== 'Valid' || !signature.timestamped || signature.thumbprint?.toUpperCase() !== options.thumbprint.toUpperCase()) {
+      throw new Error(`primary runtime: signing verification failed: ${path}`)
+    }
+    options.record({ type: 'primary-runtime-signature-verified', path, ...signature })
+  }
+  options.smoke(root)
+  options.record({ type: 'primary-runtime-smoke-success' })
+}
+
+async function main(): Promise<void> {
+  if (process.platform !== 'win32' || resolveDesktopBuildTarget() !== 'win-x64') throw new Error('primary runtime signing requires Windows x64')
+  const runDirectory = process.env.DSH_DESKTOP_PACKAGING_RUN_DIR
+  if (!runDirectory) throw new Error('primary runtime signing requires a supervised packaging run')
+  await readFile(join(runDirectory, 'run.json'))
+  const certificateFile = process.env.DSH_DESKTOP_WINDOWS_CER_FILE
+  if (!certificateFile) throw new Error('primary runtime signing requires the configured certificate')
+  const thumbprint = new X509Certificate(await readFile(certificateFile)).fingerprint.replaceAll(':', '')
+  try {
+    await signWindowsPrimaryRuntime(join(resolveDesktopTargetBuildPaths().runtime, 'primary-runtime'), {
+      thumbprint,
+      sign: createWindowsTokenSigner({ certificateFile, signTool: process.env.DSH_DESKTOP_WINDOWS_SIGNTOOL,
+        keyContainer: process.env.DSH_DESKTOP_WINDOWS_KEY_CONTAINER, tokenPin: process.env.DSH_DESKTOP_WINDOWS_TOKEN_PIN }),
+      record: (event) => { recordPackagingEvent(runDirectory, event) },
+      smoke: smokePrimaryRuntime,
+    })
+  } catch (error) {
+    failPackagingRun(runDirectory, 'primary-runtime-signing-or-smoke-failed')
+    throw error
+  }
+}
+
+if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.filename) await main()

+ 122 - 0
apps/desktop/tests/primary-runtime-signing.spec.ts

@@ -0,0 +1,122 @@
+import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, expect, it, vi } from 'vitest'
+import { signWindowsPrimaryRuntime, windowsRuntimeCode } from '../scripts/sign-primary-runtime.ts'
+
+const roots: string[] = []
+const thumbprint = 'A'.repeat(40)
+const valid = { status: 'Valid', timestamped: true, thumbprint }
+const unsigned = { status: 'NotSigned', timestamped: false, thumbprint: null }
+
+async function fixture(names = ['a.exe', 'b.pyd', 'vendor.dll']): Promise<string> {
+  const root = await mkdtemp(join(tmpdir(), 'primary-signing-'))
+  roots.push(root)
+  const pe = Buffer.alloc(128)
+  pe.writeUInt16LE(0x5a4d, 0)
+  pe.writeUInt32LE(64, 0x3c)
+  pe.writeUInt32LE(0x4550, 64)
+  for (const name of names) await writeFile(join(root, name), pe)
+  return root
+}
+
+afterEach(async () => { for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }) })
+
+it('selects real PE code, including .node, without signing foreign native modules or data', async () => {
+  const root = await fixture(['runtime.node', 'python.exe'])
+  await mkdir(join(root, 'nested'))
+  await writeFile(join(root, 'nested', 'foreign.node'), Buffer.from([0x7f, 0x45, 0x4c, 0x46]))
+  await writeFile(join(root, 'readme.txt'), 'text')
+  expect(await windowsRuntimeCode(root)).toEqual([join(root, 'python.exe'), join(root, 'runtime.node')].sort())
+})
+
+it('refuses malformed executables and root or nested directory links', async () => {
+  const root = await fixture([])
+  const target = await fixture([])
+  const link = join(root, 'linked')
+  await symlink(target, link, process.platform === 'win32' ? 'junction' : 'dir')
+  await expect(windowsRuntimeCode(root)).rejects.toThrow('links are not signable')
+  await expect(windowsRuntimeCode(link)).rejects.toThrow('real directory')
+  await writeFile(join(target, 'broken.exe'), 'invalid')
+  await expect(windowsRuntimeCode(target)).rejects.toThrow('invalid PE file')
+  await rm(join(target, 'broken.exe'))
+  await writeFile(join(target, 'broken.node'), 'MZ')
+  await expect(windowsRuntimeCode(target)).rejects.toThrow('invalid PE file')
+})
+
+it('retains vendor signatures and verifies each new signature before execution', async () => {
+  const root = await fixture()
+  const signed = new Set<string>()
+  const sequence: string[] = []
+  const sign = vi.fn(async ({ path }: { path: string }) => { sequence.push(`sign:${path}`); signed.add(path) })
+  const inspect = vi.fn(async (path: string) => {
+    sequence.push(`inspect:${path}`)
+    return path.endsWith('vendor.dll') ? { ...valid, thumbprint: 'B'.repeat(40) } : signed.has(path) ? valid : unsigned
+  })
+  const smoke = vi.fn(() => { sequence.push('smoke') })
+  await signWindowsPrimaryRuntime(root, { sign, inspect, smoke, thumbprint, record: () => {} })
+  expect(sign.mock.calls.map(([input]) => input.path)).toEqual([join(root, 'a.exe'), join(root, 'b.pyd')])
+  expect(sequence).toEqual([
+    ...['a.exe', 'b.pyd', 'vendor.dll'].map(name => `inspect:${join(root, name)}`),
+    `sign:${join(root, 'a.exe')}`, `inspect:${join(root, 'a.exe')}`,
+    `sign:${join(root, 'b.pyd')}`, `inspect:${join(root, 'b.pyd')}`, 'smoke',
+  ])
+  expect(smoke).toHaveBeenCalledWith(root)
+})
+
+it.each(['HashMismatch', 'NotTrusted', 'UnknownError'])('rejects existing %s signatures before any hardware call', async (status) => {
+  const root = await fixture()
+  const sign = vi.fn(async () => {})
+  const smoke = vi.fn()
+  await expect(signWindowsPrimaryRuntime(root, { thumbprint, sign, smoke, record: () => {},
+    inspect: async path => path.endsWith('vendor.dll') ? { ...valid, status } : unsigned })).rejects.toThrow(status)
+  expect(sign).not.toHaveBeenCalled()
+  expect(smoke).not.toHaveBeenCalled()
+})
+
+it('stops immediately on signer failure without retrying, signing another file or executing it', async () => {
+  const root = await fixture()
+  const sign = vi.fn(async () => { throw new Error('token refused') })
+  const smoke = vi.fn()
+  await expect(signWindowsPrimaryRuntime(root, { thumbprint, sign, smoke, record: () => {}, inspect: async () => unsigned }))
+    .rejects.toThrow('token refused')
+  expect(sign).toHaveBeenCalledOnce()
+  expect(smoke).not.toHaveBeenCalled()
+})
+
+it.each([
+  { ...valid, timestamped: false },
+  { ...valid, thumbprint: 'B'.repeat(40) },
+  { ...valid, status: 'HashMismatch' },
+])('refuses an unverified new signature before the next file: %j', async (invalid) => {
+  const root = await fixture()
+  let signed = false
+  const sign = vi.fn(async () => { signed = true })
+  const smoke = vi.fn()
+  await expect(signWindowsPrimaryRuntime(root, {
+    thumbprint, sign, smoke, record: () => {}, inspect: async () => signed ? invalid : unsigned,
+  }))
+    .rejects.toThrow('signing verification failed')
+  expect(sign).toHaveBeenCalledOnce()
+  expect(smoke).not.toHaveBeenCalled()
+})
+
+it('stops before hardware when the audit sink fails and never reports a failed smoke as success', async () => {
+  const root = await fixture()
+  const sign = vi.fn(async () => {})
+  const smoke = vi.fn()
+  await expect(signWindowsPrimaryRuntime(root, { thumbprint, sign, smoke, inspect: async () => unsigned,
+    record: () => { throw new Error('audit unavailable') } })).rejects.toThrow('audit unavailable')
+  expect(sign).not.toHaveBeenCalled()
+  const record = vi.fn()
+  await expect(signWindowsPrimaryRuntime(root, { thumbprint, sign, inspect: async () => valid, record,
+    smoke: () => { throw new Error('runtime blocked') } })).rejects.toThrow('runtime blocked')
+  expect(record).not.toHaveBeenCalledWith({ type: 'primary-runtime-smoke-success' })
+})
+
+it('refuses an empty runtime without declaring successful validation', async () => {
+  const root = await fixture([])
+  const smoke = vi.fn()
+  await expect(signWindowsPrimaryRuntime(root, { thumbprint, sign: vi.fn(), smoke, record: () => {} })).rejects.toThrow('no Windows code')
+  expect(smoke).not.toHaveBeenCalled()
+})