sign-primary-runtime.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. /** Sign Windows runtime code before executing it, retaining vendor signatures and fail-stop hardware protection. */
  2. import { X509Certificate } from 'node:crypto'
  3. import { lstat, open, readdir, readFile } from 'node:fs/promises'
  4. import { extname, join, resolve } from 'node:path'
  5. import { createWindowsTokenSigner } from './windows-sign.mjs'
  6. import { inspectWindowsRuntimeSignature, type WindowsRuntimeSignature } from './windows-runtime-signature.mjs'
  7. import { failPackagingRun, recordPackagingEvent } from './packaging-run.mjs'
  8. import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
  9. import { smokePrimaryRuntime } from './prepare-primary-runtime.ts'
  10. /**
  11. * Enumerate Windows code without following links or treating foreign .node files as PE binaries.
  12. * @param root - Owned, materialized runtime directory.
  13. * @returns Sorted real PE files; rejects links and malformed Windows executable files.
  14. */
  15. export async function windowsRuntimeCode(root: string): Promise<string[]> {
  16. const rootStat = await lstat(root)
  17. if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error('primary runtime: expected a real directory')
  18. const files: string[] = []
  19. for (const entry of await readdir(root, { withFileTypes: true })) {
  20. const path = join(root, entry.name)
  21. if (entry.isSymbolicLink()) throw new Error(`primary runtime: directory links are not signable: ${path}`)
  22. if (entry.isDirectory()) { files.push(...await windowsRuntimeCode(path)); continue }
  23. if (!entry.isFile() || !['.exe', '.dll', '.pyd', '.node'].includes(extname(path).toLowerCase())) continue
  24. const file = await open(path, 'r')
  25. let portableExecutable = false
  26. let windowsCandidate = false
  27. try {
  28. const header = Buffer.alloc(64)
  29. const { bytesRead } = await file.read(header, 0, header.length, 0)
  30. windowsCandidate = bytesRead >= 2 && header.readUInt16LE(0) === 0x5a4d
  31. if (bytesRead === 64 && windowsCandidate) {
  32. const signature = Buffer.alloc(4)
  33. const offset = header.readUInt32LE(0x3c)
  34. const read = await file.read(signature, 0, 4, offset)
  35. portableExecutable = offset >= 64 && read.bytesRead === 4 && signature.readUInt32LE(0) === 0x4550
  36. }
  37. } finally { await file.close() }
  38. if (portableExecutable) files.push(path)
  39. else if (windowsCandidate || extname(path).toLowerCase() !== '.node') throw new Error(`primary runtime: invalid PE file: ${path}`)
  40. }
  41. return files.sort()
  42. }
  43. interface RuntimeSigningOptions {
  44. thumbprint: string
  45. sign: ReturnType<typeof createWindowsTokenSigner>
  46. inspect?: (path: string) => Promise<WindowsRuntimeSignature>
  47. record: (event: object) => void
  48. smoke: (root: string) => void
  49. }
  50. /**
  51. * Preserve valid signatures and sign only unsigned PE files before runtime execution.
  52. * @param root - Owned final runtime directory.
  53. * @param options - Supervised signer, certificate identity, audit sink and runtime check.
  54. * @returns Resolves only after sequential signatures, verification and execution; no retries.
  55. */
  56. export async function signWindowsPrimaryRuntime(root: string, options: RuntimeSigningOptions): Promise<void> {
  57. const inspect = options.inspect ?? inspectWindowsRuntimeSignature
  58. const files = await windowsRuntimeCode(root)
  59. if (files.length === 0) throw new Error('primary runtime: no Windows code found')
  60. const unsigned: string[] = []
  61. for (const path of files) {
  62. const signature = await inspect(path)
  63. options.record({ type: 'primary-runtime-signature', path, ...signature })
  64. if (signature.status === 'NotSigned') unsigned.push(path)
  65. else if (signature.status !== 'Valid') throw new Error(`primary runtime: refusing ${signature.status} signature: ${path}`)
  66. }
  67. options.record({ type: 'primary-runtime-signing-plan', files: files.length, unsigned: unsigned.length })
  68. for (const path of unsigned) {
  69. await options.sign({ path, hash: 'sha256', isNest: false })
  70. const signature = await inspect(path)
  71. if (signature.status !== 'Valid' || !signature.timestamped || signature.thumbprint?.toUpperCase() !== options.thumbprint.toUpperCase()) {
  72. throw new Error(`primary runtime: signing verification failed: ${path}`)
  73. }
  74. options.record({ type: 'primary-runtime-signature-verified', path, ...signature })
  75. }
  76. options.smoke(root)
  77. options.record({ type: 'primary-runtime-smoke-success' })
  78. }
  79. async function main(): Promise<void> {
  80. if (process.platform !== 'win32' || resolveDesktopBuildTarget() !== 'win-x64') throw new Error('primary runtime signing requires Windows x64')
  81. const runDirectory = process.env.DSH_DESKTOP_PACKAGING_RUN_DIR
  82. if (!runDirectory) throw new Error('primary runtime signing requires a supervised packaging run')
  83. await readFile(join(runDirectory, 'run.json'))
  84. const certificateFile = process.env.DSH_DESKTOP_WINDOWS_CER_FILE
  85. if (!certificateFile) throw new Error('primary runtime signing requires the configured certificate')
  86. const thumbprint = new X509Certificate(await readFile(certificateFile)).fingerprint.replaceAll(':', '')
  87. try {
  88. await signWindowsPrimaryRuntime(join(resolveDesktopTargetBuildPaths().runtime, 'primary-runtime'), {
  89. thumbprint,
  90. sign: createWindowsTokenSigner({ certificateFile, signTool: process.env.DSH_DESKTOP_WINDOWS_SIGNTOOL,
  91. keyContainer: process.env.DSH_DESKTOP_WINDOWS_KEY_CONTAINER, tokenPin: process.env.DSH_DESKTOP_WINDOWS_TOKEN_PIN }),
  92. record: (event) => { recordPackagingEvent(runDirectory, event) },
  93. smoke: smokePrimaryRuntime,
  94. })
  95. } catch (error) {
  96. failPackagingRun(runDirectory, 'primary-runtime-signing-or-smoke-failed')
  97. throw error
  98. }
  99. }
  100. if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.filename) await main()