Files
qinglong/scripts/ql3-linux-resource-release-evidence.cjs
T

548 lines
16 KiB
JavaScript

#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const { createHash } = require('node:crypto');
const {
RESOURCE_TIERS,
createWorkloadPlans,
} = require('./ql3-linux-resource-gate.cjs');
const ARCHITECTURES = Object.freeze(['x64', 'arm64']);
const TIER_NAMES = Object.freeze([
'router-stress-ci',
'edge-release-ci',
'cluster-control-ci',
]);
const MAX_EVIDENCE_BYTES = 8 * 1024 * 1024;
const MAX_CANONICAL_DEPTH = 32;
const MAX_CANONICAL_NODES = 100_000;
const REVISION_PATTERN = /^[0-9a-f]{40}$/;
const REPOSITORY_PATTERN =
/^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})\/[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})$/;
const RUN_ID_PATTERN = /^[1-9][0-9]{0,31}$/;
const NODE_VERSION = 'v24.18.0';
class QingLong3LinuxResourceReleaseEvidenceError extends Error {
constructor(message) {
super(`QingLong 3.0 Linux resource release evidence failed: ${message}`);
this.name = 'QingLong3LinuxResourceReleaseEvidenceError';
}
}
function fail(message) {
throw new QingLong3LinuxResourceReleaseEvidenceError(message);
}
function isRecord(value) {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
Object.getPrototypeOf(value) === Object.prototype
);
}
function assertRecord(value, label) {
if (!isRecord(value)) fail(`${label} must be a plain object`);
return value;
}
function assertExactKeys(value, expected, label) {
const actual = Object.keys(assertRecord(value, label)).sort();
const wanted = [...expected].sort();
if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
fail(`${label} fields are invalid`);
}
}
function assertSafeInteger(value, label, minimum = 0) {
if (!Number.isSafeInteger(value) || value < minimum) {
fail(`${label} must be an integer >= ${minimum}`);
}
return value;
}
function canonicalize(value, depth = 0, budget = { nodes: 0 }) {
budget.nodes += 1;
if (budget.nodes > MAX_CANONICAL_NODES) fail('evidence node budget exceeded');
if (depth > MAX_CANONICAL_DEPTH) fail('evidence depth budget exceeded');
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
if (!Number.isFinite(value)) fail('evidence contains a non-finite number');
return value;
}
if (Array.isArray(value)) {
return value.map((entry) => canonicalize(entry, depth + 1, budget));
}
if (!isRecord(value)) fail('evidence contains an unsupported value');
const result = {};
for (const key of Object.keys(value).sort()) {
if (!key || key.length > 128) fail('evidence contains an invalid key');
result[key] = canonicalize(value[key], depth + 1, budget);
}
return result;
}
function evidenceDigest(value) {
return createHash('sha256')
.update('qinglong/linux-resource-release-evidence\0')
.update(JSON.stringify(canonicalize(value)))
.digest('hex');
}
function normalizeSource(source) {
assertExactKeys(
source,
['repository', 'revision', 'workflow', 'runId', 'runAttempt'],
'source',
);
if (
typeof source.repository !== 'string' ||
!REPOSITORY_PATTERN.test(source.repository)
) {
fail('source repository is invalid');
}
if (
typeof source.revision !== 'string' ||
!REVISION_PATTERN.test(source.revision)
) {
fail('source revision must be a lowercase 40-character commit SHA');
}
if (
typeof source.workflow !== 'string' ||
source.workflow.length < 1 ||
source.workflow.length > 128 ||
/[\u0000-\u001f\u007f]/.test(source.workflow)
) {
fail('source workflow is invalid');
}
if (
typeof source.runId !== 'string' ||
!RUN_ID_PATTERN.test(source.runId)
) {
fail('source runId is invalid');
}
assertSafeInteger(source.runAttempt, 'source runAttempt', 1);
return Object.freeze({
repository: source.repository,
revision: source.revision,
workflow: source.workflow,
runId: source.runId,
runAttempt: source.runAttempt,
});
}
function validateMemoryEvents(before, after, tierName) {
assertRecord(before, `${tierName} memoryEventsBefore`);
assertRecord(after, `${tierName} memoryEventsAfter`);
for (const event of ['max', 'oom', 'oom_kill', 'oom_group_kill']) {
const previous = assertSafeInteger(
before[event],
`${tierName} memoryEventsBefore.${event}`,
);
const current = assertSafeInteger(
after[event],
`${tierName} memoryEventsAfter.${event}`,
);
if (current !== previous) fail(`${tierName} memory event ${event} changed`);
}
}
function validateTierReport(report, expectedTier, expectedArchitecture) {
assertExactKeys(
report,
[
'schemaVersion',
'tier',
'evidenceClass',
'supportedMinimum',
'identity',
'envelope',
'workloads',
'gates',
],
`${expectedTier} report`,
);
const tier = RESOURCE_TIERS[expectedTier];
if (
report.schemaVersion !== 1 ||
report.tier !== expectedTier ||
report.evidenceClass !== tier.evidenceClass ||
report.supportedMinimum !== tier.supportedMinimum
) {
fail(`${expectedTier} identity is invalid`);
}
assertExactKeys(
report.identity,
['platform', 'architecture', 'node', 'uid', 'gid'],
`${expectedTier} process identity`,
);
if (
report.identity.platform !== 'linux' ||
report.identity.architecture !== expectedArchitecture ||
report.identity.node !== NODE_VERSION ||
report.identity.uid !== 65532 ||
report.identity.gid !== 65532
) {
fail(`${expectedTier} did not run under the reviewed native identity`);
}
assertExactKeys(
report.envelope,
[
'memoryMaxBytes',
'memoryPeakBytes',
'swapMaxBytes',
'cpuQuotaCores',
'pidsMax',
'noNewPrivileges',
'seccompMode',
'rootReadOnly',
'workspaceReadOnly',
'tmpWritable',
'memoryEventsBefore',
'memoryEventsAfter',
],
`${expectedTier} envelope`,
);
if (
report.envelope.memoryMaxBytes !== tier.memoryMaxBytes ||
report.envelope.swapMaxBytes !== tier.swapMaxBytes ||
report.envelope.cpuQuotaCores !== tier.cpuQuotaCores ||
report.envelope.pidsMax !== tier.pidsMax ||
report.envelope.noNewPrivileges !== 1 ||
report.envelope.seccompMode !== 2 ||
report.envelope.rootReadOnly !== true ||
report.envelope.workspaceReadOnly !== true ||
report.envelope.tmpWritable !== true
) {
fail(`${expectedTier} envelope drifted`);
}
const peak = assertSafeInteger(
report.envelope.memoryPeakBytes,
`${expectedTier} memoryPeakBytes`,
1,
);
if (peak > tier.memoryMaxBytes) fail(`${expectedTier} memory peak exceeded`);
validateMemoryEvents(
report.envelope.memoryEventsBefore,
report.envelope.memoryEventsAfter,
expectedTier,
);
if (!Array.isArray(report.workloads)) fail(`${expectedTier} workloads are invalid`);
const expectedNames = createWorkloadPlans('/workspace', expectedTier).map(
({ name }) => name,
);
const actualNames = report.workloads.map((entry, index) => {
assertExactKeys(entry, ['name', 'report'], `${expectedTier} workload ${index}`);
if (!isRecord(entry.report)) fail(`${expectedTier} workload report is invalid`);
return entry.name;
});
if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) {
fail(`${expectedTier} workload set drifted`);
}
assertExactKeys(report.gates, ['passed', 'violations'], `${expectedTier} gates`);
if (
report.gates.passed !== true ||
!Array.isArray(report.gates.violations) ||
report.gates.violations.length !== 0
) {
fail(`${expectedTier} gate did not pass`);
}
canonicalize(report);
return report;
}
function architecturePayload(source, architecture, reports) {
return {
schemaVersion: 1,
fixture: 'qinglong/linux-resource-architecture-evidence@v1',
source,
architecture,
reports,
gates: {
nativeArchitecture: true,
exactTierSet: true,
exactResourceEnvelopes: true,
memoryEventsStable: true,
allTierGatesPassed: true,
sourceBound: true,
passed: true,
},
};
}
function bundleArchitectureEvidence({ source, architecture, reports }) {
const normalizedSource = normalizeSource(source);
if (!ARCHITECTURES.includes(architecture)) fail('architecture is invalid');
assertExactKeys(reports, TIER_NAMES, 'reports');
const normalizedReports = Object.freeze(
Object.fromEntries(
TIER_NAMES.map((tierName) => [
tierName,
validateTierReport(reports[tierName], tierName, architecture),
]),
),
);
const payload = architecturePayload(
normalizedSource,
architecture,
normalizedReports,
);
return Object.freeze({
...payload,
bundleDigest: evidenceDigest(payload),
});
}
function validateArchitectureEvidence(value, expectedSource, architecture) {
assertExactKeys(
value,
[
'schemaVersion',
'fixture',
'source',
'architecture',
'reports',
'gates',
'bundleDigest',
],
`${architecture} architecture evidence`,
);
const rebuilt = bundleArchitectureEvidence({
source: value.source,
architecture: value.architecture,
reports: value.reports,
});
if (
value.schemaVersion !== rebuilt.schemaVersion ||
value.fixture !== rebuilt.fixture ||
value.architecture !== architecture ||
JSON.stringify(value.gates) !== JSON.stringify(rebuilt.gates) ||
value.bundleDigest !== rebuilt.bundleDigest
) {
fail(`${architecture} architecture evidence digest or gates drifted`);
}
if (JSON.stringify(rebuilt.source) !== JSON.stringify(normalizeSource(expectedSource))) {
fail(`${architecture} architecture evidence belongs to another source`);
}
return rebuilt;
}
function tierSummary(report) {
return Object.freeze({
tier: report.tier,
evidenceClass: report.evidenceClass,
memoryMaxBytes: report.envelope.memoryMaxBytes,
memoryPeakBytes: report.envelope.memoryPeakBytes,
cpuQuotaCores: report.envelope.cpuQuotaCores,
pidsMax: report.envelope.pidsMax,
workloadCount: report.workloads.length,
});
}
function releasePayload(source, x64, arm64) {
return {
schemaVersion: 1,
fixture: 'qinglong/linux-resource-cross-architecture-evidence@v1',
source,
architectures: [x64, arm64].map((entry) => ({
architecture: entry.architecture,
node: entry.reports['router-stress-ci'].identity.node,
bundleDigest: entry.bundleDigest,
tiers: TIER_NAMES.map((tierName) => tierSummary(entry.reports[tierName])),
})),
gates: {
nativeX64Passed: true,
nativeArm64Passed: true,
sameSourceRevision: true,
sameWorkflowRun: true,
exactTierParity: true,
releaseEvidenceComplete: true,
passed: true,
},
limitations: [
'CI cgroup evidence is not a supported minimum hardware claim',
'CI evidence does not replace fixed-device power-loss, flash, thermal, or soak evidence',
'GitHub workflow identity binding is not a cryptographic hardware attestation',
],
};
}
function mergeCrossArchitectureEvidence({ source, x64, arm64 }) {
const normalizedSource = normalizeSource(source);
const validatedX64 = validateArchitectureEvidence(x64, normalizedSource, 'x64');
const validatedArm64 = validateArchitectureEvidence(
arm64,
normalizedSource,
'arm64',
);
if (validatedX64.bundleDigest === validatedArm64.bundleDigest) {
fail('architecture bundles must be independently measured');
}
const payload = releasePayload(
normalizedSource,
validatedX64,
validatedArm64,
);
return Object.freeze({
...payload,
releaseDigest: evidenceDigest(payload),
});
}
function readJsonFile(filePath, label) {
const absolute = path.resolve(filePath);
if (!Number.isInteger(fs.constants.O_NOFOLLOW)) {
fail('platform does not support no-follow evidence reads');
}
let handle;
try {
handle = fs.openSync(
absolute,
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW,
);
} catch (error) {
fail(
`${label} must be a readable non-symlink file (${error?.code ?? 'unknown'})`,
);
}
try {
const stat = fs.fstatSync(handle);
if (!stat.isFile() || stat.size < 2 || stat.size > MAX_EVIDENCE_BYTES) {
fail(`${label} size is invalid`);
}
const content = fs.readFileSync(handle);
if (content.length < 2 || content.length > MAX_EVIDENCE_BYTES) {
fail(`${label} size is invalid`);
}
let value;
try {
value = JSON.parse(content.toString('utf8'));
} catch {
fail(`${label} must contain valid JSON`);
}
canonicalize(value);
return value;
} finally {
fs.closeSync(handle);
}
}
function writeJsonFile(filePath, value) {
const absolute = path.resolve(filePath);
fs.mkdirSync(path.dirname(absolute), { recursive: true });
const handle = fs.openSync(absolute, 'wx', 0o600);
try {
fs.writeFileSync(handle, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
} finally {
fs.closeSync(handle);
}
}
function parseArguments(argv) {
const options = {};
for (const argument of argv) {
const separator = argument.indexOf('=');
if (separator < 3 || !argument.startsWith('--')) {
fail(`unsupported argument ${argument}`);
}
const name = argument.slice(2, separator);
if (Object.hasOwn(options, name)) fail(`duplicate argument --${name}`);
options[name] = argument.slice(separator + 1);
}
const mode = options.mode;
const common = [
'mode',
'repository',
'revision',
'workflow',
'run-id',
'run-attempt',
'output',
];
const modeKeys =
mode === 'bundle'
? [...common, 'architecture', ...TIER_NAMES]
: mode === 'merge'
? [...common, 'x64', 'arm64']
: fail('--mode must be bundle or merge');
if (JSON.stringify(Object.keys(options).sort()) !== JSON.stringify(modeKeys.sort())) {
fail(`${mode} arguments are incomplete or widened`);
}
const runAttempt = Number(options['run-attempt']);
const source = normalizeSource({
repository: options.repository,
revision: options.revision,
workflow: options.workflow,
runId: options['run-id'],
runAttempt,
});
return Object.freeze({ ...options, mode, source });
}
function main() {
const options = parseArguments(process.argv.slice(2));
if (options.mode === 'bundle') {
const reports = Object.fromEntries(
TIER_NAMES.map((tierName) => [
tierName,
readJsonFile(options[tierName], tierName),
]),
);
const result = bundleArchitectureEvidence({
source: options.source,
architecture: options.architecture,
reports,
});
writeJsonFile(options.output, result);
process.stdout.write(
`${JSON.stringify({ schemaVersion: 1, mode: 'bundle', architecture: result.architecture, bundleDigest: result.bundleDigest, passed: true })}\n`,
);
return;
}
const result = mergeCrossArchitectureEvidence({
source: options.source,
x64: readJsonFile(options.x64, 'x64 evidence'),
arm64: readJsonFile(options.arm64, 'arm64 evidence'),
});
writeJsonFile(options.output, result);
process.stdout.write(
`${JSON.stringify({ schemaVersion: 1, mode: 'merge', releaseDigest: result.releaseDigest, passed: true })}\n`,
);
}
module.exports = {
ARCHITECTURES,
MAX_EVIDENCE_BYTES,
NODE_VERSION,
QingLong3LinuxResourceReleaseEvidenceError,
TIER_NAMES,
bundleArchitectureEvidence,
evidenceDigest,
mergeCrossArchitectureEvidence,
normalizeSource,
parseArguments,
readJsonFile,
validateArchitectureEvidence,
validateTierReport,
};
if (require.main === module) {
try {
main();
} catch (error) {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`,
);
process.exitCode = 1;
}
}