mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
653 lines
20 KiB
JavaScript
653 lines
20 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const crypto = require('node:crypto');
|
|
const { spawnSync } = require('node:child_process');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { afterEach, test } = require('node:test');
|
|
|
|
const {
|
|
WorkerCredentialManagementReleaseEvidenceError,
|
|
auditWorkerCredentialManagementReleaseEvidence,
|
|
clearSourceDocuments,
|
|
parseArguments,
|
|
readSourceDocuments,
|
|
runWorkerCredentialManagementReleaseEvidence,
|
|
validateWorkerCredentialManagementReleaseEvidence,
|
|
} = require('../../scripts/ql3-worker-credential-management-release-evidence.cjs');
|
|
const {
|
|
MAX_EVIDENCE_AGE_MS,
|
|
MAX_FUTURE_SKEW_MS,
|
|
WorkerCredentialManagementReleaseGateError,
|
|
auditWorkerCredentialManagementReleaseGate,
|
|
parseArguments: parseGateArguments,
|
|
} = require('../../scripts/ql3-worker-credential-management-release-gate.cjs');
|
|
|
|
const NOW_MS = 1_700_000_000_000;
|
|
const ISSUER = 'https://identity.production.example.org/';
|
|
const temporaryDirectories = [];
|
|
|
|
function sha(character) {
|
|
return `sha256:${character.repeat(64)}`;
|
|
}
|
|
|
|
function digest(domain, value) {
|
|
return `sha256:${crypto
|
|
.createHash('sha256')
|
|
.update(domain)
|
|
.update('\0')
|
|
.update(String(value))
|
|
.digest('hex')}`;
|
|
}
|
|
|
|
function rawDigest(value) {
|
|
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
|
|
}
|
|
|
|
function ceremonyReport() {
|
|
return {
|
|
schemaVersion: 1,
|
|
fixture: 'qinglong/worker-credential-management-live-ceremony@v1',
|
|
observedAt: new Date(NOW_MS).toISOString(),
|
|
identity: {
|
|
providerKind: 'external_oidc',
|
|
issuer: ISSUER,
|
|
discoveryDocumentSha256: sha('a'),
|
|
jwksSha256: sha('b'),
|
|
audience: 'qinglong3-worker-credential-management',
|
|
type: 'ql3-worker-credential-management+jwt',
|
|
purpose: 'worker-credential-management',
|
|
requesterSubjectSha256: digest(
|
|
'qinglong3.worker-management.subject.v1',
|
|
'operator-a',
|
|
),
|
|
reviewerSubjectSha256: digest(
|
|
'qinglong3.worker-management.subject.v1',
|
|
'reviewer-b',
|
|
),
|
|
requesterKeyIdSha256: sha('c'),
|
|
reviewerKeyIdSha256: sha('d'),
|
|
},
|
|
ceremony: {
|
|
actionRefSha256: sha('e'),
|
|
authorityProjectIdSha256: sha('f'),
|
|
planStatus: 'created',
|
|
approvalStatus: 'created',
|
|
requesterSelfDecisionStatus: 403,
|
|
requesterSelfDecisionCode: 'forbidden',
|
|
reviewerDecisionStatus: 'decided',
|
|
approvalState: 'approved',
|
|
inspectionStale: false,
|
|
dispatchCreated: false,
|
|
approvalConsumed: false,
|
|
planDigest: 'a'.repeat(64),
|
|
previewDigest: 'b'.repeat(64),
|
|
requestIdSha256: ['0', '1', '2', '3', '4'].map(sha),
|
|
},
|
|
gates: {
|
|
externalIdentity: true,
|
|
workerPurposeBound: true,
|
|
requesterAndReviewerDistinct: true,
|
|
requesterSelfDecisionRejected: true,
|
|
reviewerDecisionAccepted: true,
|
|
inspectionAuthorized: true,
|
|
noExecutionOrConsumption: true,
|
|
passed: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
function durableReport(ceremonyBytes, ceremony) {
|
|
return {
|
|
schemaVersion: 1,
|
|
fixture: 'qinglong/worker-credential-management-durable-audit-evidence@v1',
|
|
observedAt: new Date(NOW_MS + 100).toISOString(),
|
|
source: {
|
|
ceremonyReportSha256: rawDigest(ceremonyBytes),
|
|
ceremonyDefinitionSha256: sha('5'),
|
|
ceremonyFixture: ceremony.fixture,
|
|
},
|
|
database: {
|
|
postgresVersionNumber: 180004,
|
|
transactionReadOnly: true,
|
|
roleNameSha256: sha('6'),
|
|
roleCanLogin: true,
|
|
privilegedAttributesDenied: true,
|
|
privilegedMembershipDenied: true,
|
|
exactTargetSelect: true,
|
|
ql3TableMutationDenied: true,
|
|
},
|
|
durableState: {
|
|
actionRefSha256: ceremony.ceremony.actionRefSha256,
|
|
authorityProjectIdSha256: ceremony.ceremony.authorityProjectIdSha256,
|
|
approvalRequestIdSha256: sha('7'),
|
|
reviewerDecisionIdSha256: sha('8'),
|
|
planDigest: ceremony.ceremony.planDigest,
|
|
previewDigest: ceremony.ceremony.previewDigest,
|
|
approvalVersion: 2,
|
|
approvalState: 'approved',
|
|
requesterSubjectSha256: ceremony.identity.requesterSubjectSha256,
|
|
reviewerSubjectSha256: ceremony.identity.reviewerSubjectSha256,
|
|
dispatchCreated: false,
|
|
approvalConsumed: false,
|
|
requesterSelfDecisionAuditAbsent: true,
|
|
auditRows: [
|
|
{
|
|
kind: 'proposal',
|
|
eventIdSha256: sha('9'),
|
|
operationId: 'approval.request',
|
|
outcome: 'approval_required',
|
|
subjectSha256: ceremony.identity.requesterSubjectSha256,
|
|
authenticationIdSha256: sha('a'),
|
|
reasonCode: 'worker_credential_review',
|
|
policyFencePresent: true,
|
|
},
|
|
{
|
|
kind: 'reviewer_decision',
|
|
eventIdSha256: sha('b'),
|
|
operationId: 'approval.decide',
|
|
outcome: 'allowed',
|
|
subjectSha256: ceremony.identity.reviewerSubjectSha256,
|
|
authenticationIdSha256: sha('c'),
|
|
reasonCode: 'worker_credential_review',
|
|
policyFencePresent: true,
|
|
},
|
|
],
|
|
},
|
|
gates: {
|
|
sourceBound: true,
|
|
readOnlyEvidenceRole: true,
|
|
immutablePlanObserved: true,
|
|
reviewedApprovalObserved: true,
|
|
requesterSelfDecisionLeftNoAudit: true,
|
|
proposalAndDecisionAuditObserved: true,
|
|
noExecutionOrConsumption: true,
|
|
passed: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
function commonIdentity(ceremony) {
|
|
return {
|
|
providerKind: 'external_oidc',
|
|
issuer: ceremony.identity.issuer,
|
|
audience: ceremony.identity.audience,
|
|
type: ceremony.identity.type,
|
|
purpose: ceremony.identity.purpose,
|
|
subjectSha256: ceremony.identity.requesterSubjectSha256,
|
|
ceremonyIdentityBound: true,
|
|
durableAuditBound: true,
|
|
};
|
|
}
|
|
|
|
function pkiReport(ceremonyBytes, durableBytes, ceremony) {
|
|
return {
|
|
schemaVersion: 2,
|
|
fixture: 'qinglong/worker-credential-management-pki-rotation-evidence@v2',
|
|
observedAt: new Date(NOW_MS + 200).toISOString(),
|
|
source: {
|
|
beforeStateSha256: sha('d'),
|
|
ceremonyReportSha256: rawDigest(ceremonyBytes),
|
|
durableAuditReportSha256: rawDigest(durableBytes),
|
|
ceremonyFixture: ceremony.fixture,
|
|
durableAuditFixture:
|
|
'qinglong/worker-credential-management-durable-audit-evidence@v1',
|
|
},
|
|
identity: commonIdentity(ceremony),
|
|
transport: {
|
|
endpointSha256: sha('e'),
|
|
servernameSha256: sha('f'),
|
|
serverTrustBundleSha256: sha('0'),
|
|
serverAuthoritySha256: [sha('1')],
|
|
commandSha256: sha('2'),
|
|
oldClientCertificateSha256: sha('3'),
|
|
newClientCertificateSha256: sha('4'),
|
|
beforeOldStatus: 200,
|
|
beforeNewStatus: 200,
|
|
afterOldStatus: 401,
|
|
afterOldCode: 'client_certificate_required',
|
|
afterNewStatus: 200,
|
|
},
|
|
pki: {
|
|
clientIssuerBundleSha256: sha('5'),
|
|
clientIssuerCaSha256: sha('6'),
|
|
clientIssuerSubjectSha256: sha('7'),
|
|
beforeCrlSha256: sha('8'),
|
|
afterCrlSha256: sha('9'),
|
|
crlIssuerSha256: sha('7'),
|
|
beforeCrlNumber: '1000',
|
|
afterCrlNumber: '1001',
|
|
crlNumberIncreased: true,
|
|
oldCertificateRevoked: true,
|
|
replacementCertificateAccepted: true,
|
|
},
|
|
kubernetes: {
|
|
clusterServerSha256: sha('a'),
|
|
collectorSubjectSha256: sha('b'),
|
|
deploymentUidSha256: sha('c'),
|
|
beforeDeploymentResourceVersionSha256: sha('d'),
|
|
afterDeploymentResourceVersionSha256: sha('e'),
|
|
beforeGeneration: 10,
|
|
afterGeneration: 11,
|
|
beforeCrlAnnotationSha256: sha('f'),
|
|
afterCrlAnnotationSha256: sha('0'),
|
|
beforePodUidSha256: [sha('1'), sha('2')],
|
|
afterPodUidSha256: [sha('3'), sha('4')],
|
|
oldPodsFullyReplaced: true,
|
|
twoReadyReplicasOnDistinctNodes: true,
|
|
exactReadOnlyCollectorAuthority: true,
|
|
secretReadDenied: true,
|
|
mutationDenied: true,
|
|
},
|
|
gates: {
|
|
sourceReportsBound: true,
|
|
externalIdentityBound: true,
|
|
serverTrustSeparatedFromClientIssuer: true,
|
|
sameClientIssuer: true,
|
|
oldAndReplacementInitiallyAccepted: true,
|
|
crlMonotonic: true,
|
|
deploymentRolled: true,
|
|
oldPodsRetired: true,
|
|
revokedCertificateRejected: true,
|
|
replacementCertificateAccepted: true,
|
|
readOnlyCollectorAuthority: true,
|
|
passed: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
function caRolloverReport(ceremonyBytes, durableBytes, ceremony) {
|
|
return {
|
|
schemaVersion: 1,
|
|
fixture: 'qinglong/worker-credential-management-ca-rollover-evidence@v1',
|
|
observedAt: new Date(NOW_MS + 300).toISOString(),
|
|
source: {
|
|
oldStateSha256: sha('1'),
|
|
overlapStateSha256: sha('2'),
|
|
ceremonyReportSha256: rawDigest(ceremonyBytes),
|
|
durableAuditReportSha256: rawDigest(durableBytes),
|
|
ceremonyFixture: ceremony.fixture,
|
|
durableAuditFixture:
|
|
'qinglong/worker-credential-management-durable-audit-evidence@v1',
|
|
},
|
|
identity: commonIdentity(ceremony),
|
|
transport: {
|
|
endpointSha256: sha('e'),
|
|
servernameSha256: sha('f'),
|
|
serverTrustBundleSha256: sha('0'),
|
|
commandSha256: sha('2'),
|
|
oldClientCertificateSha256: sha('5'),
|
|
newClientCertificateSha256: sha('6'),
|
|
},
|
|
trustTransition: {
|
|
oldCaBundleSha256: sha('3'),
|
|
overlapCaBundleSha256: sha('4'),
|
|
newCaBundleSha256: sha('5'),
|
|
oldCrlBundleSha256: sha('6'),
|
|
overlapCrlBundleSha256: sha('7'),
|
|
newCrlBundleSha256: sha('8'),
|
|
oldIssuerCaSha256: sha('1'),
|
|
newIssuerCaSha256: sha('2'),
|
|
oldSet: [sha('1')],
|
|
overlapSet: [sha('1'), sha('2')],
|
|
newSet: [sha('2')],
|
|
crlIssuerCoverageExact: true,
|
|
},
|
|
access: {
|
|
oldCertificateStatus: [200, 200, 401],
|
|
newCertificateStatus: [401, 200, 200],
|
|
},
|
|
kubernetes: {
|
|
clusterServerSha256: sha('a'),
|
|
collectorSubjectSha256: sha('b'),
|
|
deploymentUidSha256: sha('c'),
|
|
generations: [20, 21, 22],
|
|
resourceVersionSha256: [sha('9'), sha('a'), sha('b')],
|
|
podUidSha256: [
|
|
[sha('0'), sha('1')],
|
|
[sha('2'), sha('3')],
|
|
[sha('4'), sha('5')],
|
|
],
|
|
allGenerationsFullyReplaced: true,
|
|
twoReadyReplicasOnDistinctNodes: true,
|
|
exactReadOnlyCollectorAuthority: true,
|
|
},
|
|
gates: {
|
|
sourceReportsBound: true,
|
|
externalIdentityBound: true,
|
|
serverTrustSeparatedFromClientIssuer: true,
|
|
oldOnlyObserved: true,
|
|
exactOverlapObserved: true,
|
|
safeRetirementObserved: true,
|
|
crlCoverageObservedEveryPhase: true,
|
|
allPodGenerationsReplaced: true,
|
|
readOnlyCollectorAuthority: true,
|
|
passed: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
function writeJson(directory, name, value) {
|
|
const filePath = path.join(directory, name);
|
|
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
return filePath;
|
|
}
|
|
|
|
function fixture(mutate = () => {}) {
|
|
const directory = fs.realpathSync(
|
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-worker-release-test-')),
|
|
);
|
|
fs.chmodSync(directory, 0o700);
|
|
temporaryDirectories.push(directory);
|
|
const ceremony = ceremonyReport();
|
|
mutate({ phase: 'ceremony', report: ceremony });
|
|
const ceremonyReportFile = writeJson(directory, 'ceremony.json', ceremony);
|
|
const ceremonyBytes = fs.readFileSync(ceremonyReportFile);
|
|
const durable = durableReport(ceremonyBytes, ceremony);
|
|
mutate({ phase: 'durable', report: durable });
|
|
const durableAuditReportFile = writeJson(directory, 'durable.json', durable);
|
|
const durableBytes = fs.readFileSync(durableAuditReportFile);
|
|
const pki = pkiReport(ceremonyBytes, durableBytes, ceremony);
|
|
mutate({ phase: 'pki', report: pki });
|
|
const pkiRotationReportFile = writeJson(directory, 'pki.json', pki);
|
|
const caRollover = caRolloverReport(ceremonyBytes, durableBytes, ceremony);
|
|
mutate({ phase: 'ca', report: caRollover });
|
|
const caRolloverReportFile = writeJson(
|
|
directory,
|
|
'ca-rollover.json',
|
|
caRollover,
|
|
);
|
|
return {
|
|
directory,
|
|
ceremonyReportFile,
|
|
durableAuditReportFile,
|
|
pkiRotationReportFile,
|
|
caRolloverReportFile,
|
|
outputFile: path.join(directory, 'release.json'),
|
|
};
|
|
}
|
|
|
|
function options(paths) {
|
|
return {
|
|
ceremonyReportFile: paths.ceremonyReportFile,
|
|
durableAuditReportFile: paths.durableAuditReportFile,
|
|
pkiRotationReportFile: paths.pkiRotationReportFile,
|
|
caRolloverReportFile: paths.caRolloverReportFile,
|
|
outputFile: paths.outputFile,
|
|
};
|
|
}
|
|
|
|
function gateOptions(paths) {
|
|
return {
|
|
reportFile: paths.outputFile,
|
|
ceremonyReportFile: paths.ceremonyReportFile,
|
|
durableAuditReportFile: paths.durableAuditReportFile,
|
|
pkiRotationReportFile: paths.pkiRotationReportFile,
|
|
caRolloverReportFile: paths.caRolloverReportFile,
|
|
sourceCommit: 'a'.repeat(40),
|
|
releaseVersion: '3.0.0-alpha.1',
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const directory of temporaryDirectories.splice(0)) {
|
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('assembles and independently re-audits one digest-bound release gate', () => {
|
|
const paths = fixture();
|
|
const report = runWorkerCredentialManagementReleaseEvidence(
|
|
options(paths),
|
|
{ now: () => NOW_MS + 1_000 },
|
|
);
|
|
assert.equal(
|
|
validateWorkerCredentialManagementReleaseEvidence(report).compatible,
|
|
true,
|
|
);
|
|
assert.equal(report.gates.passed, true);
|
|
assert.deepEqual(report.deployment.pkiGenerations, [10, 11]);
|
|
assert.deepEqual(report.deployment.caRolloverGenerations, [20, 21, 22]);
|
|
assert.equal(fs.statSync(paths.outputFile).mode & 0o777, 0o600);
|
|
assert.doesNotMatch(
|
|
JSON.stringify(report),
|
|
/operator-a|reviewer-b|BEGIN|eyJ|pod-|node-|deployment-a/,
|
|
);
|
|
|
|
const audit = spawnSync(
|
|
process.execPath,
|
|
[
|
|
path.resolve(
|
|
__dirname,
|
|
'../../scripts/ql3-worker-credential-management-release-evidence-audit.cjs',
|
|
),
|
|
`--report=${paths.outputFile}`,
|
|
`--ceremony-report=${paths.ceremonyReportFile}`,
|
|
`--durable-audit-report=${paths.durableAuditReportFile}`,
|
|
`--pki-rotation-report=${paths.pkiRotationReportFile}`,
|
|
`--ca-rollover-report=${paths.caRolloverReportFile}`,
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.equal(audit.status, 0, audit.stderr);
|
|
assert.equal(JSON.parse(audit.stdout).compatible, true);
|
|
});
|
|
|
|
test('binds fresh source-aware evidence to one release workflow identity', () => {
|
|
const paths = fixture();
|
|
runWorkerCredentialManagementReleaseEvidence(options(paths), {
|
|
now: () => NOW_MS + 1_000,
|
|
});
|
|
const result = auditWorkerCredentialManagementReleaseGate(
|
|
gateOptions(paths),
|
|
{ now: () => NOW_MS + 2_000 },
|
|
);
|
|
assert.deepEqual(result, {
|
|
schemaVersion: 1,
|
|
fixture: 'qinglong/worker-credential-management-release-gate@v1',
|
|
compatible: true,
|
|
sourceCommit: 'a'.repeat(40),
|
|
releaseVersion: '3.0.0-alpha.1',
|
|
evidenceReportSha256: rawDigest(fs.readFileSync(paths.outputFile)),
|
|
maximumEvidenceAgeSeconds: 86400,
|
|
});
|
|
});
|
|
|
|
test('rejects stale or future-dated release evidence', () => {
|
|
const paths = fixture();
|
|
runWorkerCredentialManagementReleaseEvidence(options(paths), {
|
|
now: () => NOW_MS + 1_000,
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
auditWorkerCredentialManagementReleaseGate(gateOptions(paths), {
|
|
now: () => NOW_MS + 1_000 + MAX_EVIDENCE_AGE_MS + 1,
|
|
}),
|
|
/older than the 24 hour release window/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
auditWorkerCredentialManagementReleaseGate(gateOptions(paths), {
|
|
now: () => NOW_MS + 1_000 - MAX_FUTURE_SKEW_MS - 1,
|
|
}),
|
|
/too far in the future/,
|
|
);
|
|
});
|
|
|
|
test('release gate re-audits sources instead of trusting the final report', () => {
|
|
const paths = fixture();
|
|
runWorkerCredentialManagementReleaseEvidence(options(paths), {
|
|
now: () => NOW_MS + 1_000,
|
|
});
|
|
const ceremony = JSON.parse(fs.readFileSync(paths.ceremonyReportFile, 'utf8'));
|
|
ceremony.ceremony.planDigest = 'f'.repeat(64);
|
|
fs.writeFileSync(paths.ceremonyReportFile, `${JSON.stringify(ceremony)}\n`, {
|
|
mode: 0o600,
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
auditWorkerCredentialManagementReleaseGate(gateOptions(paths), {
|
|
now: () => NOW_MS + 2_000,
|
|
}),
|
|
/source-aware evidence audit is incompatible/,
|
|
);
|
|
});
|
|
|
|
test('release gate accepts only exact immutable release identity arguments', () => {
|
|
const argv = [
|
|
'--report=/private/release.json',
|
|
'--ceremony-report=/private/ceremony.json',
|
|
'--durable-audit-report=/private/durable.json',
|
|
'--pki-rotation-report=/private/pki.json',
|
|
'--ca-rollover-report=/private/ca.json',
|
|
`--source-commit=${'a'.repeat(40)}`,
|
|
'--release-version=3.0.0',
|
|
];
|
|
assert.equal(parseGateArguments(argv).releaseVersion, '3.0.0');
|
|
assert.throws(
|
|
() => parseGateArguments([...argv, '--extra=true']),
|
|
WorkerCredentialManagementReleaseGateError,
|
|
);
|
|
const invalid = gateOptions({
|
|
outputFile: '/private/release.json',
|
|
ceremonyReportFile: '/private/ceremony.json',
|
|
durableAuditReportFile: '/private/durable.json',
|
|
pkiRotationReportFile: '/private/pki.json',
|
|
caRolloverReportFile: '/private/ca.json',
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
auditWorkerCredentialManagementReleaseGate(
|
|
{ ...invalid, releaseVersion: '3.00.0' },
|
|
{ now: () => NOW_MS },
|
|
),
|
|
/QingLong 3 SemVer image tag/,
|
|
);
|
|
});
|
|
|
|
test('rejects a broken source digest chain', () => {
|
|
const paths = fixture(({ phase, report }) => {
|
|
if (phase === 'ca') report.source.ceremonyReportSha256 = sha('f');
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
runWorkerCredentialManagementReleaseEvidence(options(paths), {
|
|
now: () => NOW_MS + 1_000,
|
|
}),
|
|
/digest chain is not exact/,
|
|
);
|
|
assert.equal(fs.existsSync(paths.outputFile), false);
|
|
});
|
|
|
|
test('rejects operator, transport and Deployment drift across reports', () => {
|
|
for (const mutate of [
|
|
({ phase, report }) => {
|
|
if (phase === 'ca') {
|
|
report.identity.subjectSha256 =
|
|
ceremonyReport().identity.reviewerSubjectSha256;
|
|
}
|
|
},
|
|
({ phase, report }) => {
|
|
if (phase === 'ca') report.transport.endpointSha256 = sha('d');
|
|
},
|
|
({ phase, report }) => {
|
|
if (phase === 'ca') report.kubernetes.deploymentUidSha256 = sha('d');
|
|
},
|
|
]) {
|
|
const paths = fixture(mutate);
|
|
assert.throws(
|
|
() =>
|
|
runWorkerCredentialManagementReleaseEvidence(options(paths), {
|
|
now: () => NOW_MS + 1_000,
|
|
}),
|
|
/reviewed operator|management transport|Kubernetes authority/,
|
|
);
|
|
assert.equal(fs.existsSync(paths.outputFile), false);
|
|
}
|
|
});
|
|
|
|
test('rejects an incompatible source report before release output', () => {
|
|
const paths = fixture(({ phase, report }) => {
|
|
if (phase === 'pki') report.gates.passed = false;
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
runWorkerCredentialManagementReleaseEvidence(options(paths), {
|
|
now: () => NOW_MS + 1_000,
|
|
}),
|
|
/source reports are incompatible/,
|
|
);
|
|
assert.equal(fs.existsSync(paths.outputFile), false);
|
|
});
|
|
|
|
test('offline audit recomputes source binding instead of trusting claimed gates', () => {
|
|
const paths = fixture();
|
|
const report = runWorkerCredentialManagementReleaseEvidence(
|
|
options(paths),
|
|
{ now: () => NOW_MS + 1_000 },
|
|
);
|
|
for (const changed of [
|
|
{ ...structuredClone(report), extra: true },
|
|
{
|
|
...structuredClone(report),
|
|
gates: { ...report.gates, passed: false },
|
|
},
|
|
{ ...structuredClone(report), privateKey: 'forbidden' },
|
|
{
|
|
...structuredClone(report),
|
|
transport: { ...report.transport, endpointSha256: sha('d') },
|
|
},
|
|
]) {
|
|
const documents = readSourceDocuments(options(paths));
|
|
try {
|
|
assert.equal(
|
|
auditWorkerCredentialManagementReleaseEvidence(changed, documents)
|
|
.compatible,
|
|
false,
|
|
);
|
|
} finally {
|
|
clearSourceDocuments(documents);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('rejects a release timestamp older than a source observation', () => {
|
|
const paths = fixture();
|
|
const report = runWorkerCredentialManagementReleaseEvidence(
|
|
options(paths),
|
|
{ now: () => NOW_MS + 1_000 },
|
|
);
|
|
const changed = {
|
|
...structuredClone(report),
|
|
observedAt: new Date(NOW_MS + 150).toISOString(),
|
|
};
|
|
const documents = readSourceDocuments(options(paths));
|
|
try {
|
|
assert.equal(
|
|
auditWorkerCredentialManagementReleaseEvidence(changed, documents)
|
|
.compatible,
|
|
false,
|
|
);
|
|
} finally {
|
|
clearSourceDocuments(documents);
|
|
}
|
|
});
|
|
|
|
test('parses only the exact five path-based runner arguments', () => {
|
|
const argv = [
|
|
'--ceremony-report=/private/ceremony.json',
|
|
'--durable-audit-report=/private/durable.json',
|
|
'--pki-rotation-report=/private/pki.json',
|
|
'--ca-rollover-report=/private/ca.json',
|
|
'--output=/private/release.json',
|
|
];
|
|
assert.equal(
|
|
parseArguments(argv).pkiRotationReportFile,
|
|
'/private/pki.json',
|
|
);
|
|
assert.throws(
|
|
() => parseArguments([...argv, '--extra=true']),
|
|
WorkerCredentialManagementReleaseEvidenceError,
|
|
);
|
|
});
|