windows-runtime-signature.mjs 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
  1. /** Inspect runtime signatures and preserve byte-identical copies made by electron-builder. */
  2. import { execFile } from 'node:child_process'
  3. import { readFile, realpath } from 'node:fs/promises'
  4. import { isAbsolute, join, relative, resolve, sep } from 'node:path'
  5. import { promisify } from 'node:util'
  6. import { scrubWindowsSigningEnvironment } from './windows-sign.mjs'
  7. import { recordPackagingEvent } from './packaging-run.mjs'
  8. /**
  9. * Read Windows trust, timestamp and signer identity using the engine's bundled modules, without accessing the private key.
  10. * @param {string} path File to inspect.
  11. * @returns {Promise<{status: string, timestamped: boolean, thumbprint: string | null}>} Authenticode verification result.
  12. */
  13. export async function inspectWindowsRuntimeSignature(path) {
  14. // Node can inherit PowerShell 7's module search path while launching Windows PowerShell 5.
  15. const { stdout, stderr } = await promisify(execFile)('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
  16. '$ErrorActionPreference="Stop"; [Console]::OutputEncoding=[System.Text.UTF8Encoding]::new(); Import-Module "$PSHOME/Modules/Microsoft.PowerShell.Security/Microsoft.PowerShell.Security.psd1" -ErrorAction Stop; Import-Module "$PSHOME/Modules/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1" -ErrorAction 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'], {
  17. env: { ...scrubWindowsSigningEnvironment(process.env), DSH_RUNTIME_VERIFY_FILE: path },
  18. encoding: 'utf8', windowsHide: true, timeout: 60_000, maxBuffer: 64 * 1024,
  19. })
  20. const value = JSON.parse(stdout)
  21. if (stderr || typeof value !== 'object' || value === null || typeof value.status !== 'string'
  22. || typeof value.timestamped !== 'boolean'
  23. || !(value.thumbprint === null || typeof value.thumbprint === 'string' && /^[A-F\d]{40}$/iu.test(value.thumbprint))) {
  24. throw new Error(`primary runtime: invalid signature inspection: ${path}`)
  25. }
  26. return { status: value.status, timestamped: value.timestamped, thumbprint: value.thumbprint }
  27. }
  28. /**
  29. * Preserve a copied primary-runtime executable only after signature and exact-byte verification.
  30. * @param {string} path Signing-hook target.
  31. * @param {{sourceRoot: string, destinationRoot: string, runDirectory: string, inspect?: typeof inspectWindowsRuntimeSignature}} options Prepared and copied runtime roots with retained audit directory.
  32. * @returns {Promise<boolean>} True for a verified runtime copy; false for targets outside that directory.
  33. */
  34. export async function preserveWindowsRuntimeSignature(path, options) {
  35. const suffix = relative(options.destinationRoot, path)
  36. if (!suffix || suffix === '..' || suffix.startsWith(`..${sep}`) || isAbsolute(suffix)) return false
  37. const source = join(options.sourceRoot, suffix)
  38. for (const file of [source, path]) {
  39. if (await realpath(file) !== resolve(file)) throw new Error(`primary runtime: linked copy is not signable: ${file}`)
  40. }
  41. const [prepared, copied] = await Promise.all([readFile(source), readFile(path)])
  42. if (!prepared.equals(copied)) throw new Error(`primary runtime: copied executable changed: ${path}`)
  43. const signature = await (options.inspect ?? inspectWindowsRuntimeSignature)(path)
  44. if (signature.status !== 'Valid') throw new Error(`primary runtime: copied signature is ${signature.status}: ${path}`)
  45. recordPackagingEvent(options.runDirectory, { type: 'primary-runtime-copy-verified', path, ...signature })
  46. return true
  47. }