Files
hermes-agent/apps/desktop/scripts/payload-sign-cache.test.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

164 lines
7.4 KiB
JavaScript

import { createHash } from 'node:crypto'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { execFileSync } from 'node:child_process'
import { expect, test, vi } from 'vitest'
import { batchSignBinaries } from './batch-sign-binaries.mjs'
import { createPayloadSignCache, peContentHash, verifySignedPayloads } from './payload-sign-cache.mjs'
const hash = bytes => createHash('sha256').update(bytes).digest('hex')
// Minimal PE input for byte-binding tests, not a signature-validity fixture.
function pe(marker = 1) {
const bytes = Buffer.alloc(1024)
bytes.write('MZ')
bytes.writeUInt32LE(128, 0x3c)
bytes.writeUInt32LE(0x4550, 128)
bytes.writeUInt16LE(240, 148)
bytes.writeUInt16LE(0x20b, 152)
bytes.writeUInt32LE(16, 260)
bytes[600] = marker
return bytes
}
function signatureBytes(input) {
const result = Buffer.concat([input, Buffer.alloc(32, 42)])
result.writeUInt32LE(12345, 216)
result.writeUInt32LE(input.length, 296)
result.writeUInt32LE(32, 300)
return result
}
function fixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'payload-sign-cache-test-'))
const tool = path.join(root, 'tool')
fs.writeFileSync(tool, 'test tool bytes')
const env = { AZURE_SIGN_ENDPOINT: 'https://test.invalid', AZURE_SIGN_ACCOUNT: 'account',
AZURE_SIGN_PROFILE: 'profile', AZURE_SIGN_PUBLISHER: 'CN=Test', TEMP: root }
const verify = vi.fn(async files => new Set(files))
const opts = { root: path.join(root, 'cache'), env, signtool: tool, dlib: tool,
timestampUrl: 'http://timestamp.test.invalid', verify }
return { root, tool, env, verify, opts, cache: createPayloadSignCache(opts),
cleanup: () => fs.rmSync(root, { recursive: true, force: true }) }
}
test.runIf(process.platform === 'win32')('native verification binds cached bytes to the publisher and timestamp', async () => {
const f = fixture()
try {
const publisher = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
'(Get-AuthenticodeSignature -LiteralPath $env:NATIVE_SIGN_TEST_INPUT).SignerCertificate.Subject'
], { encoding: 'utf8', windowsHide: true, env: { ...process.env, NATIVE_SIGN_TEST_INPUT: process.execPath } }).trim()
expect(publisher).not.toBe('')
const files = ['original.exe', 'renamed.dll'].map(name => path.join(f.root, name))
for (const file of files) fs.copyFileSync(process.execPath, file)
expect((await verifySignedPayloads(files, publisher)).size).toBe(2)
expect((await verifySignedPayloads(files, 'CN=Wrong publisher')).size).toBe(0)
const cache = createPayloadSignCache({ ...f.opts, verify: verifySignedPayloads,
env: { ...f.env, AZURE_SIGN_PUBLISHER: publisher } })
const cold = await cache.prepare(files)
expect(cold.files).toHaveLength(1)
await cache.publish(cold)
const warm = await cache.prepare(files)
expect(warm.restored).toBe(2)
expect(warm.files).toEqual([])
const corrupted = fs.readFileSync(files[1])
corrupted[0x200] ^= 1
fs.writeFileSync(files[1], corrupted)
expect(await verifySignedPayloads(files, publisher)).toEqual(new Set([files[0]]))
} finally { f.cleanup() }
}, 30000)
test('input bytes select entries across paths, while policy and executable content stay binding', async () => {
const f = fixture()
try {
const first = path.join(f.root, 'one.exe')
const duplicate = path.join(f.root, 'different.dll')
const input = pe()
fs.writeFileSync(first, input)
fs.writeFileSync(duplicate, input)
const plan = await f.cache.prepare([first, duplicate])
expect(plan.files).toEqual([first])
expect(plan.duplicates).toBe(1)
fs.writeFileSync(first, signatureBytes(input))
await f.cache.publish(plan)
expect(fs.readFileSync(duplicate)).toEqual(fs.readFileSync(first))
fs.writeFileSync(duplicate, input)
const warm = await createPayloadSignCache(f.opts).prepare([duplicate])
expect(warm.files).toEqual([])
expect(warm.restored).toBe(1)
expect(fs.readFileSync(duplicate)).toEqual(signatureBytes(input))
fs.writeFileSync(duplicate, input)
const changedPolicy = createPayloadSignCache({ ...f.opts, env: { ...f.env, AZURE_SIGN_PROFILE: 'other' } })
expect((await changedPolicy.prepare([duplicate])).restored).toBe(0)
fs.writeFileSync(duplicate, pe(2))
expect((await f.cache.prepare([duplicate])).restored).toBe(0)
const policyDir = path.join(f.opts.root, fs.readdirSync(f.opts.root)[0])
const entry = path.join(policyDir, hash(input))
const candidate = path.join(entry, 'signed.exe')
// A different signed program with a matching receipt must not replace input.
const substituted = signatureBytes(pe(2))
fs.writeFileSync(candidate, substituted)
fs.writeFileSync(path.join(entry, 'receipt.json'), JSON.stringify({ signedHash: hash(substituted) }))
fs.writeFileSync(duplicate, input)
expect((await f.cache.prepare([duplicate])).restored).toBe(0)
expect(fs.readFileSync(duplicate)).toEqual(input)
expect(fs.existsSync(entry)).toBe(false)
for (const mode of ['corrupt', 'untrusted']) {
const pending = await f.cache.prepare([duplicate])
fs.writeFileSync(duplicate, signatureBytes(input))
await f.cache.publish(pending)
fs.writeFileSync(duplicate, input)
if (mode === 'corrupt') fs.appendFileSync(candidate, 'corruption')
else f.verify.mockResolvedValueOnce(new Set())
expect((await f.cache.prepare([duplicate])).restored).toBe(0)
expect(fs.readFileSync(duplicate)).toEqual(input)
}
} finally { f.cleanup() }
})
test('batch cache publishes only after successful signing and timestamping, and warm hits skip both', async () => {
const f = fixture()
try {
const file = path.join(f.root, 'input.exe')
const input = pe()
const options = { env: f.env, signtool: f.tool, dlib: f.tool, cache: f.cache,
timestampAttempts: 1, timestampRetryDelayMs: 0 }
for (const failAt of ['sign', 'timestamp']) {
fs.writeFileSync(file, input)
await expect(batchSignBinaries([file], { ...options, exec: async (_, args) => {
if (args[0] === failAt) throw new Error(failAt)
fs.writeFileSync(file, signatureBytes(input))
} })).rejects.toThrow(failAt)
expect(fs.existsSync(f.opts.root)).toBe(false)
}
fs.writeFileSync(file, input)
f.verify.mockImplementation(async () => new Set())
await expect(batchSignBinaries([file], { ...options, exec: async (_, args) => {
if (args[0] === 'sign') fs.writeFileSync(file, signatureBytes(input))
} })).rejects.toThrow('failed verification')
expect(fs.existsSync(f.opts.root)).toBe(false)
f.verify.mockImplementation(async files => new Set(files))
fs.writeFileSync(file, input)
const calls = []
const cold = await batchSignBinaries([file], { ...options, exec: async (_, args) => {
calls.push(args[0])
if (args[0] === 'sign') fs.writeFileSync(file, signatureBytes(input))
} })
expect(cold.signed).toBe(1)
expect(calls).toEqual(['sign', 'timestamp'])
fs.writeFileSync(file, input)
const warm = await batchSignBinaries([file], { ...options, exec: () => { throw new Error('not a hit') } })
expect(warm.signed).toBe(0)
expect(fs.readFileSync(file)).toEqual(signatureBytes(input))
const malformed = path.join(f.root, 'not-pe.exe')
fs.writeFileSync(malformed, 'not a PE')
expect(peContentHash(malformed)).toBe(null)
expect((await f.cache.prepare([malformed])).files).toEqual([malformed])
} finally { f.cleanup() }
})