mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
Compiled installs always take the non-Bun branch, which called an unimported join and threw before any runtime-tested assertion could be witnessed. The child command selection is now a pure, platform-aware function; a missing sibling launcher fails with its expected path.
746 lines
28 KiB
TypeScript
746 lines
28 KiB
TypeScript
import {
|
|
generateKeyPairSync,
|
|
createPrivateKey,
|
|
createPublicKey,
|
|
randomBytes,
|
|
sign,
|
|
verify,
|
|
} from 'node:crypto';
|
|
import { existsSync, lstatSync, realpathSync } from 'node:fs';
|
|
import { basename, posix, win32 } from 'node:path';
|
|
import {
|
|
AssertionWitnessBinding,
|
|
AssertionWitnessReceipt,
|
|
Command,
|
|
CsoError,
|
|
MAX_OUTPUT,
|
|
VerificationObservation,
|
|
canonical,
|
|
object,
|
|
oneOf,
|
|
sha256,
|
|
string,
|
|
validateCommand,
|
|
validateVerificationObservation,
|
|
} from './contracts';
|
|
import { runProcess } from './process';
|
|
|
|
export interface WitnessTestExecution {
|
|
command: Command;
|
|
code: number;
|
|
output: string;
|
|
minimumPassingTests: number;
|
|
}
|
|
export interface WitnessedVerificationResult {
|
|
observation: VerificationObservation;
|
|
witness: AssertionWitnessReceipt;
|
|
}
|
|
export interface AssertionWitnessHandle {
|
|
readonly binding: AssertionWitnessBinding;
|
|
attest(
|
|
observation: VerificationObservation,
|
|
executions: WitnessTestExecution[],
|
|
): Promise<AssertionWitnessReceipt>;
|
|
validate(receipt: unknown, observation: VerificationObservation, now?: number): AssertionWitnessReceipt;
|
|
}
|
|
|
|
const HASH = /^[a-f0-9]{64}$/;
|
|
const PUBLIC_KEY = /^[a-f0-9]{88}$/;
|
|
const SIGNATURE = /^[a-f0-9]{128}$/;
|
|
const PROTOCOL = 'gstack-cso-assertion-witness-v1' as const;
|
|
const MAX_RECEIPT_AGE = 300_000;
|
|
const exact = (value: Record<string, any>, allowed: readonly string[], name: string) => {
|
|
for (const key of Object.keys(value))
|
|
if (!allowed.includes(key)) throw new CsoError('INVALID_SCHEMA', `Unexpected ${name} field: ${key}`);
|
|
};
|
|
const hash = (value: unknown, name: string): string => {
|
|
if (typeof value !== 'string' || !HASH.test(value))
|
|
throw new CsoError('INVALID_SCHEMA', `${name} must be a sha256 hash`);
|
|
return value;
|
|
};
|
|
const timestamp = (value: unknown, name: string): string => {
|
|
const result = string(value, name, 64),
|
|
ms = Date.parse(result);
|
|
if (!Number.isFinite(ms) || new Date(ms).toISOString() !== result)
|
|
throw new CsoError('INVALID_SCHEMA', `${name} must be a canonical UTC timestamp`);
|
|
return result;
|
|
};
|
|
|
|
export function validateAssertionWitnessBinding(value: unknown): AssertionWitnessBinding {
|
|
const v = object(value, 'assertion witness binding'),
|
|
runtime = object(v.runtime, 'assertion witness runtime'),
|
|
runner = object(v.runner, 'assertion witness runner');
|
|
exact(
|
|
v,
|
|
[
|
|
'schemaVersion',
|
|
'protocol',
|
|
'nonce',
|
|
'phase',
|
|
'issuedAt',
|
|
'expiresAt',
|
|
'runId',
|
|
'findingId',
|
|
'policyHash',
|
|
'auditPolicyHash',
|
|
'runtime',
|
|
'runner',
|
|
'sourceHash',
|
|
'dependencyHash',
|
|
'configurationHash',
|
|
'requestHash',
|
|
'patchHash',
|
|
'harnessHash',
|
|
'assertionHash',
|
|
'fixturesHash',
|
|
],
|
|
'assertion witness binding',
|
|
);
|
|
exact(runtime, ['image', 'verifierImage', 'platform', 'profile'], 'assertion witness runtime');
|
|
exact(
|
|
runner,
|
|
['testToolchain', 'startPlanHash', 'testPlanHash', 'commandsHash', 'minimumPassingTestsHash'],
|
|
'assertion witness runner',
|
|
);
|
|
if (v.schemaVersion !== 1 || v.protocol !== PROTOCOL)
|
|
throw new CsoError('INVALID_SCHEMA', 'Unsupported assertion witness protocol');
|
|
const issuedAt = timestamp(v.issuedAt, 'assertion witness issuedAt'),
|
|
expiresAt = timestamp(v.expiresAt, 'assertion witness expiresAt'),
|
|
duration = Date.parse(expiresAt) - Date.parse(issuedAt);
|
|
if (duration <= 0 || duration > MAX_RECEIPT_AGE)
|
|
throw new CsoError('INVALID_SCHEMA', 'Assertion witness lifetime exceeds the bounded attempt policy');
|
|
if (typeof v.nonce !== 'string' || !HASH.test(v.nonce))
|
|
throw new CsoError('INVALID_SCHEMA', 'Assertion witness nonce must be 32 random bytes');
|
|
const findingId = string(v.findingId, 'assertion witness findingId', 64);
|
|
if (!/^[a-f0-9]{32}$/.test(findingId))
|
|
throw new CsoError('INVALID_SCHEMA', 'Assertion witness findingId is invalid');
|
|
return {
|
|
schemaVersion: 1,
|
|
protocol: PROTOCOL,
|
|
nonce: v.nonce,
|
|
phase: oneOf(v.phase, ['before', 'after'], 'assertion witness phase'),
|
|
issuedAt,
|
|
expiresAt,
|
|
runId: string(v.runId, 'assertion witness runId', 200),
|
|
findingId,
|
|
policyHash: hash(v.policyHash, 'assertion witness policyHash'),
|
|
auditPolicyHash: hash(v.auditPolicyHash, 'assertion witness auditPolicyHash'),
|
|
runtime: {
|
|
image: string(runtime.image, 'assertion witness runtime image', 500),
|
|
verifierImage: string(runtime.verifierImage, 'assertion witness verifier image', 500),
|
|
platform: string(runtime.platform, 'assertion witness runtime platform', 100),
|
|
profile: string(runtime.profile, 'assertion witness runtime profile', 100),
|
|
},
|
|
runner: {
|
|
testToolchain: oneOf(runner.testToolchain, ['runtime', 'project'], 'assertion witness test toolchain'),
|
|
startPlanHash: hash(runner.startPlanHash, 'assertion witness start plan'),
|
|
testPlanHash: hash(runner.testPlanHash, 'assertion witness test plan'),
|
|
commandsHash: hash(runner.commandsHash, 'assertion witness commands'),
|
|
minimumPassingTestsHash: hash(runner.minimumPassingTestsHash, 'assertion witness execution floors'),
|
|
},
|
|
sourceHash: hash(v.sourceHash, 'assertion witness sourceHash'),
|
|
dependencyHash: hash(v.dependencyHash, 'assertion witness dependencyHash'),
|
|
configurationHash: hash(v.configurationHash, 'assertion witness configurationHash'),
|
|
requestHash: hash(v.requestHash, 'assertion witness requestHash'),
|
|
patchHash: hash(v.patchHash, 'assertion witness patchHash'),
|
|
harnessHash: hash(v.harnessHash, 'assertion witness harnessHash'),
|
|
assertionHash: hash(v.assertionHash, 'assertion witness assertionHash'),
|
|
fixturesHash: hash(v.fixturesHash, 'assertion witness fixturesHash'),
|
|
};
|
|
}
|
|
|
|
function receiptUnsigned(receipt: AssertionWitnessReceipt): Omit<AssertionWitnessReceipt, 'signature'> {
|
|
const { signature: _, ...unsigned } = receipt;
|
|
return unsigned;
|
|
}
|
|
function observationForReceipt(
|
|
observation: VerificationObservation,
|
|
binding: AssertionWitnessBinding,
|
|
diagnosticTestsPassed: boolean,
|
|
): VerificationObservation {
|
|
const checked = validateVerificationObservation(observation);
|
|
return { ...checked, existingTests: diagnosticTestsPassed, inputHash: binding.harnessHash };
|
|
}
|
|
export function witnessObservationHash(observation: VerificationObservation): string {
|
|
return sha256(canonical(validateVerificationObservation(observation)));
|
|
}
|
|
|
|
export function validateStoredAssertionWitnessReceipt(value: unknown): AssertionWitnessReceipt {
|
|
const v = object(value, 'assertion witness receipt'),
|
|
binding = validateAssertionWitnessBinding(v.binding);
|
|
exact(
|
|
v,
|
|
[
|
|
'schemaVersion',
|
|
'binding',
|
|
'keyId',
|
|
'publicKey',
|
|
'observationHash',
|
|
'externalAssertionsPassed',
|
|
'diagnosticTestsPassed',
|
|
'executions',
|
|
'signature',
|
|
],
|
|
'assertion witness receipt',
|
|
);
|
|
if (
|
|
v.schemaVersion !== 1 ||
|
|
typeof v.keyId !== 'string' ||
|
|
!HASH.test(v.keyId) ||
|
|
typeof v.publicKey !== 'string' ||
|
|
!PUBLIC_KEY.test(v.publicKey) ||
|
|
typeof v.signature !== 'string' ||
|
|
!SIGNATURE.test(v.signature)
|
|
)
|
|
throw new CsoError('INVALID_SCHEMA', 'Assertion witness cryptographic metadata is invalid');
|
|
if (sha256(Buffer.from(v.publicKey, 'hex')) !== v.keyId)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Assertion witness key identity does not match its public key');
|
|
if (
|
|
typeof v.observationHash !== 'string' ||
|
|
!HASH.test(v.observationHash) ||
|
|
typeof v.externalAssertionsPassed !== 'boolean' ||
|
|
typeof v.diagnosticTestsPassed !== 'boolean' ||
|
|
!Array.isArray(v.executions) ||
|
|
!v.executions.length ||
|
|
v.executions.length > 100
|
|
)
|
|
throw new CsoError('INVALID_SCHEMA', 'Assertion witness outcomes are malformed');
|
|
const executions = v.executions.map((raw: any, index: number) => {
|
|
const item = object(raw, `assertion witness execution ${index}`);
|
|
exact(
|
|
item,
|
|
[
|
|
'commandHash',
|
|
'exitCode',
|
|
'outputHash',
|
|
'minimumPassingTests',
|
|
'executedTests',
|
|
'passingTests',
|
|
'reportedPassed',
|
|
],
|
|
`assertion witness execution ${index}`,
|
|
);
|
|
if (
|
|
!Number.isSafeInteger(item.exitCode) ||
|
|
item.exitCode < -1 ||
|
|
item.exitCode > 255 ||
|
|
!Number.isSafeInteger(item.minimumPassingTests) ||
|
|
item.minimumPassingTests < 1 ||
|
|
!Number.isSafeInteger(item.executedTests) ||
|
|
item.executedTests < 0 ||
|
|
!Number.isSafeInteger(item.passingTests) ||
|
|
item.passingTests < 0 ||
|
|
item.passingTests > item.executedTests ||
|
|
typeof item.reportedPassed !== 'boolean' ||
|
|
(item.reportedPassed && item.passingTests < item.minimumPassingTests)
|
|
)
|
|
throw new CsoError('INVALID_SCHEMA', 'Assertion witness execution outcome is malformed');
|
|
return {
|
|
commandHash: hash(item.commandHash, 'assertion witness commandHash'),
|
|
exitCode: item.exitCode,
|
|
outputHash: hash(item.outputHash, 'assertion witness outputHash'),
|
|
minimumPassingTests: item.minimumPassingTests,
|
|
executedTests: item.executedTests,
|
|
passingTests: item.passingTests,
|
|
reportedPassed: item.reportedPassed,
|
|
};
|
|
});
|
|
if (v.diagnosticTestsPassed !== executions.every((item) => item.reportedPassed))
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Assertion witness diagnostic summary does not match its executions',
|
|
);
|
|
const receipt: AssertionWitnessReceipt = {
|
|
schemaVersion: 1,
|
|
binding,
|
|
keyId: v.keyId,
|
|
publicKey: v.publicKey,
|
|
observationHash: v.observationHash,
|
|
externalAssertionsPassed: v.externalAssertionsPassed,
|
|
diagnosticTestsPassed: v.diagnosticTestsPassed,
|
|
executions,
|
|
signature: v.signature,
|
|
};
|
|
let valid = false;
|
|
try {
|
|
valid = verify(
|
|
null,
|
|
Buffer.from(canonical(receiptUnsigned(receipt))),
|
|
createPublicKey({ key: Buffer.from(receipt.publicKey, 'hex'), format: 'der', type: 'spki' }),
|
|
Buffer.from(receipt.signature, 'hex'),
|
|
);
|
|
} catch {}
|
|
if (!valid) throw new CsoError('INCOMPATIBLE_INPUT', 'Assertion witness signature is invalid');
|
|
return receipt;
|
|
}
|
|
|
|
export function validateAssertionWitnessReceipt(
|
|
value: unknown,
|
|
expected: AssertionWitnessBinding,
|
|
expectedPublicKey: string,
|
|
observation: VerificationObservation,
|
|
now = Date.now(),
|
|
): AssertionWitnessReceipt {
|
|
const receipt = validateStoredAssertionWitnessReceipt(value),
|
|
binding = validateAssertionWitnessBinding(expected);
|
|
if (canonical(receipt.binding) !== canonical(binding) || receipt.publicKey !== expectedPublicKey)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Assertion witness receipt does not bind this verification challenge',
|
|
);
|
|
if (now < Date.parse(binding.issuedAt) || now > Date.parse(binding.expiresAt))
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Assertion witness receipt is stale');
|
|
const normalized = observationForReceipt(observation, binding, receipt.diagnosticTestsPassed),
|
|
external = normalized.booted && normalized.legitimate && normalized.security !== 'inconclusive';
|
|
if (
|
|
receipt.observationHash !== witnessObservationHash(normalized) ||
|
|
receipt.externalAssertionsPassed !== external
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Assertion witness receipt does not bind the external verifier observation',
|
|
);
|
|
return receipt;
|
|
}
|
|
|
|
export function assertionWitnessSemanticValue(receipt: AssertionWitnessReceipt): unknown {
|
|
const checked = validateStoredAssertionWitnessReceipt(receipt),
|
|
{ nonce: _, issuedAt: __, expiresAt: ___, ...stable } = checked.binding;
|
|
return {
|
|
binding: stable,
|
|
observationHash: checked.observationHash,
|
|
externalAssertionsPassed: checked.externalAssertionsPassed,
|
|
diagnosticTestsPassed: checked.diagnosticTestsPassed,
|
|
executions: checked.executions,
|
|
};
|
|
}
|
|
export function assertionWitnessPairHash(pair: {
|
|
before: AssertionWitnessReceipt;
|
|
after: AssertionWitnessReceipt;
|
|
}): string {
|
|
return sha256(
|
|
canonical({
|
|
before: assertionWitnessSemanticValue(pair.before),
|
|
after: assertionWitnessSemanticValue(pair.after),
|
|
}),
|
|
);
|
|
}
|
|
|
|
function witnessReplayValue(receipt: AssertionWitnessReceipt): unknown {
|
|
const checked = validateStoredAssertionWitnessReceipt(receipt),
|
|
{ nonce: _, issuedAt: __, expiresAt: ___, ...binding } = checked.binding;
|
|
return {
|
|
binding,
|
|
externalAssertionsPassed: checked.externalAssertionsPassed,
|
|
diagnosticTestsPassed: checked.diagnosticTestsPassed,
|
|
executions: checked.executions.map(({ outputHash: _, ...execution }) => execution),
|
|
};
|
|
}
|
|
export function assertionWitnessReplayHash(pair: {
|
|
before: AssertionWitnessReceipt;
|
|
after: AssertionWitnessReceipt;
|
|
}): string {
|
|
return sha256(
|
|
canonical({ before: witnessReplayValue(pair.before), after: witnessReplayValue(pair.after) }),
|
|
);
|
|
}
|
|
|
|
interface TestExecutionSummary {
|
|
executedTests: number;
|
|
passingTests: number;
|
|
reportedPassed: boolean;
|
|
}
|
|
function testExecutionSummary(
|
|
command: Command,
|
|
code: number,
|
|
output: string,
|
|
minimumPassingTests = 1,
|
|
): TestExecutionSummary {
|
|
const failed = { executedTests: 0, passingTests: 0, reportedPassed: false };
|
|
if (
|
|
!Number.isInteger(minimumPassingTests) ||
|
|
minimumPassingTests < 1 ||
|
|
code !== 0 ||
|
|
!output ||
|
|
output.includes('[sensitive process output redacted]')
|
|
)
|
|
return failed;
|
|
const clean = output.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, ''),
|
|
args = command.args,
|
|
name = basename(command.executable),
|
|
json = () => {
|
|
const end = clean.lastIndexOf('}');
|
|
if (end < 0) return undefined;
|
|
for (let start = clean.lastIndexOf('{', end); start >= 0; start = clean.lastIndexOf('{', start - 1)) {
|
|
try {
|
|
const value = JSON.parse(clean.slice(start, end + 1));
|
|
if (value && typeof value === 'object') return value;
|
|
} catch {}
|
|
}
|
|
};
|
|
let executedTests = 0,
|
|
passingTests = 0,
|
|
valid = false;
|
|
if (name === 'node' && args.includes('--test') && args.includes('--test-reporter=tap')) {
|
|
const paths = args.filter((arg) => arg.startsWith('./')).map((arg) => arg.slice(2)),
|
|
registered = [...clean.matchAll(/^# Subtest:\s+(.+?)\s*$/gm)].map((match) => match[1]),
|
|
isPathWrapper = (label: string) => paths.some((path) => label === path || label.endsWith(`/${path}`));
|
|
executedTests = Number(clean.match(/^# tests\s+(\d+)\s*$/m)?.[1]);
|
|
passingTests = Number(clean.match(/^# pass\s+(\d+)\s*$/m)?.[1]);
|
|
valid =
|
|
registered.some((label) => !isPathWrapper(label)) &&
|
|
!registered.some(isPathWrapper) &&
|
|
executedTests >= passingTests &&
|
|
/^# fail\s+0\s*$/m.test(clean) &&
|
|
/^# cancelled\s+0\s*$/m.test(clean);
|
|
} else if (name === 'bun' && args.includes('test')) {
|
|
passingTests = Number(clean.match(/^\s*(\d+)\s+pass(?:es)?\s*$/im)?.[1]);
|
|
executedTests = Number(clean.match(/\bRan\s+(\d+)\s+tests?\b/i)?.[1]);
|
|
valid = executedTests >= passingTests && /^\s*0\s+fail(?:ures?)?\s*$/im.test(clean);
|
|
} else if (name === 'jest' && args.includes('--json')) {
|
|
const value = json();
|
|
passingTests = Number(value?.numPassedTests);
|
|
executedTests = Number(value?.numTotalTests);
|
|
valid =
|
|
value?.success === true &&
|
|
value?.numFailedTests === 0 &&
|
|
value?.numRuntimeErrorTestSuites === 0 &&
|
|
executedTests >= passingTests;
|
|
} else if (name === 'vitest' && args.includes('--reporter=verbose')) {
|
|
const match = clean.match(/^\s*Tests\s+.*?(\d+)\s+passed.*?\((\d+)\)\s*$/im);
|
|
passingTests = Number(match?.[1]);
|
|
executedTests = Number(match?.[2]);
|
|
valid = executedTests >= passingTests && !/\b\d+\s+failed\b/i.test(match?.[0] ?? '');
|
|
} else if (name === 'mocha' && args.includes('json')) {
|
|
const stats = json()?.stats;
|
|
passingTests = Number(stats?.passes);
|
|
executedTests = Number(stats?.tests);
|
|
valid =
|
|
stats?.failures === 0 &&
|
|
Number.isSafeInteger(stats?.pending) &&
|
|
executedTests === passingTests + stats.pending;
|
|
} else if (name === 'ava' && args.includes('--tap')) {
|
|
executedTests = Number(clean.match(/^# tests\s+(\d+)\s*$/m)?.[1]);
|
|
passingTests = Number(clean.match(/^# pass\s+(\d+)\s*$/m)?.[1]);
|
|
valid = executedTests >= passingTests && /^# fail\s+0\s*$/m.test(clean);
|
|
} else if (
|
|
name === 'python' &&
|
|
args.some((arg) => arg.includes('import pytest;') && arg.includes('pytest.main'))
|
|
) {
|
|
passingTests = Number(clean.match(/(?:^|\s)(\d+)\s+passed\b/i)?.[1]);
|
|
const skipped = Number(clean.match(/(?:^|\s)(\d+)\s+skipped\b/i)?.[1] ?? 0);
|
|
executedTests = passingTests + skipped;
|
|
valid = true;
|
|
} else if (
|
|
name === 'python' &&
|
|
args.some((arg) => arg.includes('import os,sys,unittest;') && arg.includes('unittest.main'))
|
|
) {
|
|
executedTests = Number(clean.match(/\bRan\s+(\d+)\s+tests?\b/i)?.[1]);
|
|
const skipped = Number(clean.match(/\bskipped=(\d+)\b/i)?.[1] ?? 0);
|
|
passingTests = executedTests - skipped;
|
|
valid = Number.isSafeInteger(skipped);
|
|
} else if (name === 'bundle' && args[0] === 'exec' && args[1] === 'rspec' && args.includes('json')) {
|
|
const summary = json()?.summary,
|
|
pending = Number(summary?.pending_count ?? 0);
|
|
executedTests = Number(summary?.example_count);
|
|
passingTests = executedTests - pending;
|
|
valid =
|
|
Number.isSafeInteger(pending) &&
|
|
summary?.failure_count === 0 &&
|
|
(summary?.errors_outside_of_examples_count ?? 0) === 0;
|
|
} else if (
|
|
name === 'bundle' &&
|
|
args[0] === 'exec' &&
|
|
args[1] === 'rails' &&
|
|
args[2] === 'test' &&
|
|
args.includes('--no-color')
|
|
) {
|
|
const match = clean.match(
|
|
/\b(\d+)\s+runs?\s*,\s*(\d+)\s+assertions?\s*,\s*0\s+failures?\s*,\s*0\s+errors?\s*,\s*(\d+)\s+skips?\b/i,
|
|
),
|
|
skipped = Number(match?.[3]);
|
|
executedTests = Number(match?.[1]);
|
|
passingTests = executedTests - skipped;
|
|
valid = Number.isSafeInteger(skipped);
|
|
}
|
|
const countsValid =
|
|
Number.isSafeInteger(executedTests) &&
|
|
executedTests >= 0 &&
|
|
Number.isSafeInteger(passingTests) &&
|
|
passingTests >= 0 &&
|
|
executedTests >= passingTests;
|
|
return countsValid
|
|
? { executedTests, passingTests, reportedPassed: valid && passingTests >= minimumPassingTests }
|
|
: failed;
|
|
}
|
|
export function testExecutionPassed(
|
|
command: Command,
|
|
code: number,
|
|
output: string,
|
|
minimumPassingTests = 1,
|
|
): boolean {
|
|
return testExecutionSummary(command, code, output, minimumPassingTests).reportedPassed;
|
|
}
|
|
|
|
interface ChildRequest {
|
|
privateKey: string;
|
|
publicKey: string;
|
|
binding: AssertionWitnessBinding;
|
|
observation: VerificationObservation;
|
|
executions: WitnessTestExecution[];
|
|
}
|
|
async function readChildInput(): Promise<string> {
|
|
const chunks: Buffer[] = [];
|
|
let bytes = 0;
|
|
for await (const value of process.stdin) {
|
|
const chunk = Buffer.from(value);
|
|
bytes += chunk.length;
|
|
if (bytes > 2 * MAX_OUTPUT)
|
|
throw new CsoError('INVALID_SCHEMA', 'Assertion witness request exceeds the bounded input limit');
|
|
chunks.push(chunk);
|
|
}
|
|
return Buffer.concat(chunks).toString('utf8');
|
|
}
|
|
function createReceipt(input: unknown): AssertionWitnessReceipt {
|
|
const v = object(input, 'assertion witness child request');
|
|
exact(
|
|
v,
|
|
['privateKey', 'publicKey', 'binding', 'observation', 'executions'],
|
|
'assertion witness child request',
|
|
);
|
|
const binding = validateAssertionWitnessBinding(v.binding);
|
|
if (Date.now() < Date.parse(binding.issuedAt) || Date.now() > Date.parse(binding.expiresAt))
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Assertion witness challenge is stale');
|
|
if (
|
|
typeof v.privateKey !== 'string' ||
|
|
v.privateKey.length > 4096 ||
|
|
typeof v.publicKey !== 'string' ||
|
|
!PUBLIC_KEY.test(v.publicKey)
|
|
)
|
|
throw new CsoError('INVALID_SCHEMA', 'Assertion witness signing input is invalid');
|
|
let privateKey;
|
|
try {
|
|
privateKey = createPrivateKey(v.privateKey);
|
|
const derived = createPublicKey(privateKey).export({ format: 'der', type: 'spki' }).toString('hex');
|
|
if (derived !== v.publicKey) throw new Error();
|
|
} catch {
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Assertion witness signing authority does not match the challenge',
|
|
);
|
|
}
|
|
const rawObservation = validateVerificationObservation(v.observation);
|
|
if (!Array.isArray(v.executions) || !v.executions.length || v.executions.length > 100)
|
|
throw new CsoError('INVALID_SCHEMA', 'Assertion witness needs one or more canonical test executions');
|
|
let outputBytes = 0;
|
|
const rawExecutions: WitnessTestExecution[] = v.executions.map((raw: any, index: number) => {
|
|
const item = object(raw, `witness execution ${index}`);
|
|
exact(item, ['command', 'code', 'output', 'minimumPassingTests'], `witness execution ${index}`);
|
|
const command = validateCommand(item.command, `witness execution ${index}.command`);
|
|
if (
|
|
!Number.isSafeInteger(item.code) ||
|
|
item.code < -1 ||
|
|
item.code > 255 ||
|
|
typeof item.output !== 'string' ||
|
|
item.output.includes('\0') ||
|
|
!Number.isSafeInteger(item.minimumPassingTests) ||
|
|
item.minimumPassingTests < 1
|
|
)
|
|
throw new CsoError('INVALID_SCHEMA', 'Assertion witness test execution is malformed');
|
|
outputBytes += Buffer.byteLength(item.output);
|
|
if (outputBytes > MAX_OUTPUT)
|
|
throw new CsoError('INVALID_SCHEMA', 'Assertion witness test output exceeds the group capture limit');
|
|
return { command, code: item.code, output: item.output, minimumPassingTests: item.minimumPassingTests };
|
|
});
|
|
if (
|
|
sha256(canonical(rawExecutions.map((item) => item.command))) !== binding.runner.commandsHash ||
|
|
sha256(canonical(rawExecutions.map((item) => item.minimumPassingTests))) !==
|
|
binding.runner.minimumPassingTestsHash
|
|
)
|
|
throw new CsoError(
|
|
'INCOMPATIBLE_INPUT',
|
|
'Assertion witness executions do not match the helper-derived runner',
|
|
);
|
|
const executions = rawExecutions.map((item) => {
|
|
const summary = testExecutionSummary(item.command, item.code, item.output, item.minimumPassingTests);
|
|
return {
|
|
commandHash: sha256(canonical(item.command)),
|
|
exitCode: item.code,
|
|
outputHash: sha256(item.output),
|
|
minimumPassingTests: item.minimumPassingTests,
|
|
...summary,
|
|
};
|
|
}),
|
|
diagnosticTestsPassed = executions.every((item) => item.reportedPassed),
|
|
observation = observationForReceipt(rawObservation, binding, diagnosticTestsPassed),
|
|
externalAssertionsPassed =
|
|
observation.booted && observation.legitimate && observation.security !== 'inconclusive';
|
|
const unsigned: Omit<AssertionWitnessReceipt, 'signature'> = {
|
|
schemaVersion: 1,
|
|
binding,
|
|
keyId: sha256(Buffer.from(v.publicKey, 'hex')),
|
|
publicKey: v.publicKey,
|
|
observationHash: witnessObservationHash(observation),
|
|
externalAssertionsPassed,
|
|
diagnosticTestsPassed,
|
|
executions,
|
|
};
|
|
return { ...unsigned, signature: sign(null, Buffer.from(canonical(unsigned)), privateKey).toString('hex') };
|
|
}
|
|
|
|
export async function runAssertionWitnessChild(): Promise<void> {
|
|
const receipt = createReceipt(JSON.parse(await readChildInput()));
|
|
process.stdout.write(JSON.stringify(receipt) + '\n');
|
|
}
|
|
|
|
export function assertionWitnessChildCommand(input: {
|
|
execPath: string;
|
|
platform: NodeJS.Platform;
|
|
modulePath: string;
|
|
systemRoot?: string;
|
|
windir?: string;
|
|
}): { file: string; args: string[]; env: Record<string, string> } {
|
|
const paths = input.platform === 'win32' ? win32 : posix,
|
|
directory = paths.dirname(input.execPath);
|
|
if (/^bun(?:\.exe)?$/i.test(paths.basename(input.execPath)))
|
|
return {
|
|
file: input.execPath,
|
|
args: [input.modulePath, '--child'],
|
|
env: witnessChildEnv(input, directory),
|
|
};
|
|
return {
|
|
file: paths.join(
|
|
directory,
|
|
input.platform === 'win32' ? 'gstack-cso-launcher.exe' : 'gstack-cso-launcher',
|
|
),
|
|
args: ['__cso-assertion-witness'],
|
|
env: witnessChildEnv(input, directory),
|
|
};
|
|
}
|
|
|
|
function witnessChildEnv(
|
|
input: { platform: NodeJS.Platform; systemRoot?: string; windir?: string },
|
|
directory: string,
|
|
): Record<string, string> {
|
|
return input.platform === 'win32'
|
|
? {
|
|
PATH: directory,
|
|
SYSTEMROOT: input.systemRoot ?? 'C:\\Windows',
|
|
WINDIR: input.windir ?? 'C:\\Windows',
|
|
}
|
|
: { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC' };
|
|
}
|
|
|
|
export class AssertionWitnessSession {
|
|
private privateKey: string;
|
|
readonly publicKey: string;
|
|
readonly keyId: string;
|
|
private nonces = new Set<string>();
|
|
constructor(
|
|
private workDirectory: string,
|
|
private deadline: number,
|
|
private execPath: string = process.execPath,
|
|
) {
|
|
const stat = lstatSync(workDirectory),
|
|
real = realpathSync(workDirectory),
|
|
resolved = lstatSync(real);
|
|
if (
|
|
!stat.isDirectory() ||
|
|
stat.isSymbolicLink() ||
|
|
!resolved.isDirectory() ||
|
|
resolved.isSymbolicLink() ||
|
|
stat.dev !== resolved.dev ||
|
|
stat.ino !== resolved.ino ||
|
|
(process.getuid && resolved.uid !== process.getuid()) ||
|
|
(resolved.mode & 0o022) !== 0
|
|
)
|
|
throw new CsoError('UNSAFE_PATH', 'Assertion witness working directory must be private and owned');
|
|
this.workDirectory = real;
|
|
const pair = generateKeyPairSync('ed25519');
|
|
this.privateKey = pair.privateKey.export({ format: 'pem', type: 'pkcs8' }).toString();
|
|
this.publicKey = pair.publicKey.export({ format: 'der', type: 'spki' }).toString('hex');
|
|
this.keyId = sha256(Buffer.from(this.publicKey, 'hex'));
|
|
}
|
|
handle(
|
|
stable: Omit<AssertionWitnessBinding, 'schemaVersion' | 'protocol' | 'nonce' | 'issuedAt' | 'expiresAt'>,
|
|
): AssertionWitnessHandle {
|
|
const now = Date.now(),
|
|
expires = Math.min(this.deadline, now + MAX_RECEIPT_AGE);
|
|
if (expires <= now)
|
|
throw new CsoError('DEADLINE', 'No time remains for an authenticated assertion witness');
|
|
let nonce = '';
|
|
do {
|
|
nonce = randomBytes(32).toString('hex');
|
|
} while (this.nonces.has(nonce));
|
|
this.nonces.add(nonce);
|
|
const binding = validateAssertionWitnessBinding({
|
|
schemaVersion: 1,
|
|
protocol: PROTOCOL,
|
|
nonce,
|
|
issuedAt: new Date(now).toISOString(),
|
|
expiresAt: new Date(expires).toISOString(),
|
|
...stable,
|
|
});
|
|
let consumed = false;
|
|
return {
|
|
binding,
|
|
attest: async (observation, executions) => {
|
|
if (consumed)
|
|
throw new CsoError('INCOMPATIBLE_INPUT', 'Assertion witness challenge was already consumed');
|
|
consumed = true;
|
|
const input = JSON.stringify({
|
|
privateKey: this.privateKey,
|
|
publicKey: this.publicKey,
|
|
binding,
|
|
observation,
|
|
executions,
|
|
} satisfies ChildRequest);
|
|
if (Buffer.byteLength(input) > 2 * MAX_OUTPUT)
|
|
throw new CsoError(
|
|
'REDACTION_FAILED',
|
|
'Assertion witness input exceeds the bounded helper channel',
|
|
);
|
|
const { file, args, env } = assertionWitnessChildCommand({
|
|
execPath: this.execPath,
|
|
platform: process.platform,
|
|
modulePath: import.meta.path,
|
|
systemRoot: process.env.SYSTEMROOT,
|
|
windir: process.env.WINDIR,
|
|
});
|
|
if (!existsSync(file))
|
|
throw new CsoError('PREREQUISITE', `Assertion witness launcher is missing: ${file}`);
|
|
const result = await runProcess(file, args, {
|
|
cwd: this.workDirectory,
|
|
env,
|
|
timeoutMs: Math.max(1, expires - Date.now()),
|
|
maxBytes: 128 * 1024,
|
|
input,
|
|
raw: true,
|
|
});
|
|
if (result.timedOut)
|
|
throw new CsoError('DEADLINE', 'Assertion witness exceeded the verification deadline');
|
|
if (result.truncated || result.code !== 0)
|
|
throw new CsoError(
|
|
'TOOL_FAILED',
|
|
'Authenticated assertion witness did not return a bounded receipt',
|
|
);
|
|
let receipt: unknown;
|
|
try {
|
|
receipt = JSON.parse(result.stdout);
|
|
} catch {
|
|
throw new CsoError('TOOL_FAILED', 'Authenticated assertion witness returned invalid output');
|
|
}
|
|
return validateAssertionWitnessReceipt(receipt, binding, this.publicKey, observation);
|
|
},
|
|
validate: (receipt, observation, current = Date.now()) =>
|
|
validateAssertionWitnessReceipt(receipt, binding, this.publicKey, observation, current),
|
|
};
|
|
}
|
|
}
|
|
|
|
if (import.meta.main && process.argv.at(-1) === '--child')
|
|
runAssertionWitnessChild().catch(() => {
|
|
process.stderr.write('assertion witness failed\n');
|
|
process.exitCode = 1;
|
|
});
|