mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 16:02:29 +00:00
142 lines
5.3 KiB
JavaScript
142 lines
5.3 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, expect, it, vi } from 'vitest'
|
|
import { writeBundledCliRuntimeFixture } from './bundled-cli-runtime-fixture.mjs'
|
|
const pins = vi.hoisted(() => ({ hash: '' }))
|
|
vi.mock('../../src/shared/orcad-bun-runtime.ts', async (importOriginal) => {
|
|
const original = await importOriginal()
|
|
return {
|
|
...original,
|
|
ORCAD_BUN_RELEASE_ASSETS: {
|
|
'win32-x64': {
|
|
get executableSha256() {
|
|
return pins.hash
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
import {
|
|
assertAuthenticodeOnlyChange,
|
|
finalizeSignedCliRuntime
|
|
} from './finalize-signed-cli-runtime.mjs'
|
|
import { verifyCliRuntimeDirectory } from '../bundled-cli-runtime.cjs'
|
|
const roots = []
|
|
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })))
|
|
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex')
|
|
function images(magic = 0x20b) {
|
|
const original = Buffer.alloc(515)
|
|
original.write('MZ')
|
|
original.writeUInt32LE(64, 60)
|
|
original.write('PE\0\0', 64)
|
|
original.writeUInt16LE(240, 84)
|
|
original.writeUInt16LE(magic, 88)
|
|
const directory = 88 + (magic === 0x20b ? 112 : 96)
|
|
original.writeUInt32LE(16, directory - 4)
|
|
original[510] = 42
|
|
const signed = Buffer.concat([original, Buffer.alloc(5), Buffer.alloc(16, 7)])
|
|
signed.writeUInt32LE(16, 520)
|
|
signed.writeUInt16LE(0x200, 524)
|
|
signed.writeUInt16LE(2, 526)
|
|
signed.writeUInt32LE(123, 88 + 64)
|
|
signed.writeUInt32LE(520, directory + 32)
|
|
signed.writeUInt32LE(16, directory + 36)
|
|
return { original, signed, security: directory + 32 }
|
|
}
|
|
|
|
it.each([0x20b, 0x10b])(
|
|
'allows only certificate append/checksum edits for PE magic %s',
|
|
(magic) => {
|
|
const { original, signed } = images(magic)
|
|
expect(() => assertAuthenticodeOnlyChange(original, signed)).not.toThrow()
|
|
}
|
|
)
|
|
it.each([
|
|
'code',
|
|
'padding',
|
|
'tail',
|
|
'directory',
|
|
'vendor-signature',
|
|
'certificate-length',
|
|
'certificate-type'
|
|
])('rejects unexpected signing change: %s', (kind) => {
|
|
let { original, signed, security } = images()
|
|
if (kind === 'certificate-length') {
|
|
signed.writeUInt32LE(1000, 520)
|
|
}
|
|
if (kind === 'certificate-type') {
|
|
signed.writeUInt16LE(1, 526)
|
|
}
|
|
if (kind === 'code') {
|
|
signed[510]++
|
|
}
|
|
if (kind === 'padding') {
|
|
signed[517] = 1
|
|
}
|
|
if (kind === 'tail') {
|
|
signed = Buffer.concat([signed, Buffer.from('extra')])
|
|
}
|
|
if (kind === 'directory') {
|
|
signed.writeUInt32LE(512, security)
|
|
}
|
|
if (kind === 'vendor-signature') {
|
|
original.writeUInt32LE(512, security)
|
|
}
|
|
expect(() => assertAuthenticodeOnlyChange(original, signed)).toThrow()
|
|
})
|
|
it('rejects truncated or non-PE images', () => {
|
|
expect(() => assertAuthenticodeOnlyChange(Buffer.from('bad'), Buffer.from('bad'))).toThrow()
|
|
})
|
|
|
|
async function stagedFixture() {
|
|
const root = mkdtempSync(join(tmpdir(), 'signed-runtime-'))
|
|
roots.push(root)
|
|
const app = join(root, 'app')
|
|
const stage = join(root, 'signing-stage')
|
|
const relative = join('resources', 'cli-runtime')
|
|
const directory = join(app, relative)
|
|
await writeBundledCliRuntimeFixture(directory, 'win32', 'x64')
|
|
const { original, signed } = images()
|
|
pins.hash = hash(original)
|
|
mkdirSync(join(stage, relative), { recursive: true })
|
|
writeFileSync(join(stage, relative, 'bun-runtime.exe'), original)
|
|
writeFileSync(join(directory, 'bun-runtime.exe'), signed)
|
|
const manifestPath = join(directory, 'runtime.json')
|
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
writeFileSync(manifestPath, JSON.stringify({ ...manifest, sha256: pins.hash }))
|
|
return { app, stage, directory, manifestPath, signed }
|
|
}
|
|
it('finalizes the signed full-file identity, preserving runtime verification and repeatability', async () => {
|
|
const f = await stagedFixture()
|
|
expect(() => verifyCliRuntimeDirectory(f.directory, 'win32', 'x64')).toThrow('checksum')
|
|
finalizeSignedCliRuntime(f.app, f.stage)
|
|
expect(JSON.parse(readFileSync(f.manifestPath, 'utf8'))).toMatchObject({
|
|
sha256: hash(f.signed),
|
|
unsignedSha256: pins.hash
|
|
})
|
|
expect(() => verifyCliRuntimeDirectory(f.directory, 'win32', 'x64')).not.toThrow()
|
|
expect(() => finalizeSignedCliRuntime(f.app, f.stage)).not.toThrow()
|
|
f.signed[510]++
|
|
writeFileSync(join(f.directory, 'bun-runtime.exe'), f.signed)
|
|
expect(() => verifyCliRuntimeDirectory(f.directory, 'win32', 'x64')).toThrow('checksum')
|
|
})
|
|
it('refuses to bless payload changes or unpinned original bytes', async () => {
|
|
const f = await stagedFixture()
|
|
f.signed[510]++
|
|
writeFileSync(join(f.directory, 'bun-runtime.exe'), f.signed)
|
|
expect(() => finalizeSignedCliRuntime(f.app, f.stage)).toThrow('executable bytes')
|
|
pins.hash = '0'.repeat(64)
|
|
expect(() => finalizeSignedCliRuntime(f.app, f.stage)).toThrow('pinned release')
|
|
})
|
|
|
|
it('refuses a changed original manifest without rewriting it', async () => {
|
|
const f = await stagedFixture()
|
|
const manifest = JSON.parse(readFileSync(f.manifestPath, 'utf8'))
|
|
const changed = JSON.stringify({ ...manifest, sha256: 'f'.repeat(64) })
|
|
writeFileSync(f.manifestPath, changed)
|
|
expect(() => finalizeSignedCliRuntime(f.app, f.stage)).toThrow('pinned release')
|
|
expect(readFileSync(f.manifestPath, 'utf8')).toBe(changed)
|
|
})
|