Files
hermes-agent/apps/desktop/scripts/payload-sign-cache.mjs
ethernet 6edee205bd perf(signing): reuse verified payload signatures by input hash
The Windows release signed 1,106 payload binaries for each of the bundled
and Store variants. Both passes repeated remote signing and timestamping.

Cache signed payload bytes by exact input SHA256 and signing policy.
Paths and release versions do not affect entry identity. Verify content
binding, publisher and timestamp before restoring a hit. Sign duplicate
inputs once and publish cache entries only after successful verification.
Keep product EXEs and package envelopes on the fresh signing path.

Persist the cache across release runs and test its native verification
in the Windows release lane. Targeted signing tests: 35 passed.
Real Azure signing of three mixed binaries took 9.6s cold and 1.8s warm.
Warm probes restored identical signed bytes with no signtool calls.
Full release performance and cache transfer overhead remain unverified.
2026-09-07 10:33:23 -04:00

137 lines
5.8 KiB
JavaScript

import { createHash } from 'node:crypto'
import { execFile } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { promisify } from 'node:util'
import { readSecurityDirectory } from './sanitize-pe-signatures.mjs'
const exec = promisify(execFile)
const sha256 = bytes => createHash('sha256').update(bytes).digest('hex')
// Only checksum, security-directory entry and a trailing certificate can change.
// Keep every other byte, including overlays. Unknown layouts are not cacheable.
export function peContentHash(file) {
const entry = readSecurityDirectory(file)
if (!entry) return null
const bytes = fs.readFileSync(file)
const pe = bytes.readUInt32LE(0x3c)
const checksum = pe + 24 + 64
if (checksum + 4 > entry.offsetInFile) return null
let end = bytes.length
if (entry.certSize || entry.certOffset) {
if (!entry.certSize || entry.certOffset < entry.offsetInFile + 8 ||
entry.certOffset % 8 || entry.certOffset + entry.certSize !== bytes.length) return null
end = entry.certOffset
}
const content = Buffer.from(bytes.subarray(0, end))
content.fill(0, checksum, checksum + 4)
content.fill(0, entry.offsetInFile, entry.offsetInFile + 8)
const hash = createHash('sha256').update(content)
// Signing aligns the certificate table to eight bytes.
hash.update(Buffer.alloc((8 - content.length % 8) % 8))
return hash.digest('hex')
}
export async function verifySignedPayloads(files, publisher) {
if (!files.length) return new Set()
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-signatures-'))
try {
const manifest = path.join(temp, 'files.json')
fs.writeFileSync(manifest, JSON.stringify(files.map(file => ({ path: file, publisher }))))
const { stdout } = await exec('powershell.exe', [
'-NoProfile', '-NonInteractive', '-File',
path.join(import.meta.dirname, 'verify-signed-payloads.ps1'), manifest
], { windowsHide: true, timeout: 600000, maxBuffer: 4 * 1024 * 1024 })
const results = JSON.parse(stdout.replace(/^\uFEFF/, ''))
if (!Array.isArray(results) || results.length !== files.length ||
results.some((r, i) => r.path !== files[i] || typeof r.valid !== 'boolean')) {
throw new Error('Invalid Authenticode verification response')
}
return new Set(results.filter(r => r.valid).map(r => r.path))
} finally {
fs.rmSync(temp, { recursive: true, force: true })
}
}
export function createPayloadSignCache({ root, env, signtool, dlib, timestampUrl, verify = verifySignedPayloads }) {
const publisher = env.AZURE_SIGN_PUBLISHER
if (!root || !publisher) return null
const policy = sha256(JSON.stringify({
schema: 1, endpoint: env.AZURE_SIGN_ENDPOINT, account: env.AZURE_SIGN_ACCOUNT,
profile: env.AZURE_SIGN_PROFILE, publisher, timestampUrl,
digest: 'SHA256', timestampDigest: 'SHA256',
signtool: sha256(fs.readFileSync(signtool)), dlib: sha256(fs.readFileSync(dlib))
}))
const directory = path.join(root, policy)
const entryPath = key => path.join(directory, key)
return {
async prepare(files) {
const groups = new Map()
const uncached = []
for (const file of files) {
const content = peContentHash(file)
if (!content) { uncached.push(file); continue }
const key = sha256(fs.readFileSync(file))
if (groups.has(key)) groups.get(key).files.push(file)
else groups.set(key, { key, content, files: [file] })
}
const candidates = []
for (const group of groups.values()) {
const entry = entryPath(group.key)
if (!fs.existsSync(entry)) continue
try {
const receipt = JSON.parse(fs.readFileSync(path.join(entry, 'receipt.json'), 'utf8'))
const binary = path.join(entry, 'signed.exe')
if (receipt.signedHash !== sha256(fs.readFileSync(binary)) || peContentHash(binary) !== group.content) {
throw new Error('Cache content mismatch')
}
candidates.push({ group, binary })
} catch {
fs.rmSync(entry, { recursive: true, force: true })
}
}
const valid = await verify(candidates.map(c => c.binary), publisher)
let restored = 0
for (const { group, binary } of candidates) {
if (!valid.has(binary)) {
fs.rmSync(entryPath(group.key), { recursive: true, force: true })
continue
}
for (const file of group.files) fs.copyFileSync(binary, file)
restored += group.files.length
groups.delete(group.key)
}
const pending = [...groups.values()]
return {
files: [...uncached, ...pending.map(g => g.files[0])],
pending, restored,
duplicates: pending.reduce((n, g) => n + g.files.length - 1, 0)
}
},
async publish(plan) {
const files = plan.pending.map(g => g.files[0])
const valid = await verify(files, publisher)
for (const group of plan.pending) {
const file = group.files[0]
if (!valid.has(file) || peContentHash(file) !== group.content) {
throw new Error(`Signed payload failed verification: ${file}`)
}
const signed = fs.readFileSync(file)
fs.mkdirSync(directory, { recursive: true })
const temp = fs.mkdtempSync(path.join(directory, '.tmp-'))
try {
fs.writeFileSync(path.join(temp, 'signed.exe'), signed)
fs.writeFileSync(path.join(temp, 'receipt.json'), JSON.stringify({ signedHash: sha256(signed) }))
const dest = entryPath(group.key)
// A complete entry appears in one rename, never a half-written pair.
if (!fs.existsSync(dest)) fs.renameSync(temp, dest)
} finally {
fs.rmSync(temp, { recursive: true, force: true })
}
for (const duplicate of group.files.slice(1)) fs.copyFileSync(file, duplicate)
}
}
}
}