mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
818 lines
25 KiB
JavaScript
818 lines
25 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
'use strict';
|
|
|
|
const { createHash } = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const net = require('node:net');
|
|
const path = require('node:path');
|
|
const { TextDecoder } = require('node:util');
|
|
|
|
const {
|
|
FIXTURE: CEREMONY_FIXTURE,
|
|
validateWorkerCredentialManagementLiveCeremony,
|
|
} = require('./ql3-worker-credential-management-live-ceremony.cjs');
|
|
const {
|
|
FIXTURE: DURABLE_FIXTURE,
|
|
validateWorkerCredentialManagementDurableAuditEvidence,
|
|
} = require('./ql3-worker-credential-management-durable-audit-evidence.cjs');
|
|
const {
|
|
FIXTURE: PKI_FIXTURE,
|
|
validateWorkerCredentialManagementPkiRotationEvidence,
|
|
} = require('./ql3-worker-credential-management-pki-rotation-evidence.cjs');
|
|
const {
|
|
FIXTURE: CA_ROLLOVER_FIXTURE,
|
|
validateWorkerCredentialManagementCaRolloverEvidence,
|
|
} = require('./ql3-worker-credential-management-ca-rollover-evidence.cjs');
|
|
|
|
const FIXTURE = 'qinglong/worker-credential-management-release-evidence@v1';
|
|
const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
const HEX_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
const MAX_FILE_BYTES = 1024 * 1024;
|
|
const BANNED_KEYS = new Set([
|
|
'assertion',
|
|
'authorization',
|
|
'bearer',
|
|
'certificate',
|
|
'connectionstring',
|
|
'crl',
|
|
'dsn',
|
|
'kubeconfig',
|
|
'password',
|
|
'privatekey',
|
|
'secret',
|
|
'tlskey',
|
|
'token',
|
|
]);
|
|
|
|
class WorkerCredentialManagementReleaseEvidenceError extends Error {
|
|
constructor(message) {
|
|
super(`Worker management release evidence failed: ${message}`);
|
|
this.name = 'WorkerCredentialManagementReleaseEvidenceError';
|
|
}
|
|
}
|
|
|
|
function fail(message) {
|
|
throw new WorkerCredentialManagementReleaseEvidenceError(message);
|
|
}
|
|
|
|
function exactKeys(value, keys) {
|
|
return (
|
|
value !== null &&
|
|
typeof value === 'object' &&
|
|
!Array.isArray(value) &&
|
|
JSON.stringify(Object.keys(value).sort()) ===
|
|
JSON.stringify([...keys].sort())
|
|
);
|
|
}
|
|
|
|
function exactObject(value, keys, label) {
|
|
if (!exactKeys(value, keys)) fail(`${label} shape is invalid`);
|
|
return value;
|
|
}
|
|
|
|
function rawDigest(value) {
|
|
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
}
|
|
|
|
function isIsoTime(value) {
|
|
return (
|
|
typeof value === 'string' &&
|
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) &&
|
|
Number.isFinite(Date.parse(value))
|
|
);
|
|
}
|
|
|
|
function externalIssuer(value) {
|
|
if (typeof value !== 'string' || value.length > 2048) return false;
|
|
try {
|
|
const parsed = new URL(value);
|
|
return (
|
|
parsed.protocol === 'https:' &&
|
|
parsed.username === '' &&
|
|
parsed.password === '' &&
|
|
parsed.search === '' &&
|
|
parsed.hash === '' &&
|
|
net.isIP(parsed.hostname) === 0 &&
|
|
parsed.hostname !== 'localhost' &&
|
|
!parsed.hostname.endsWith('.localhost') &&
|
|
!parsed.hostname.endsWith('.local') &&
|
|
!parsed.hostname.endsWith('.test') &&
|
|
!parsed.hostname.endsWith('.invalid') &&
|
|
!parsed.hostname.endsWith('.example')
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function containsSensitiveMaterial(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.some((entry) => containsSensitiveMaterial(entry));
|
|
}
|
|
if (!value || typeof value !== 'object') return false;
|
|
for (const [key, entry] of Object.entries(value)) {
|
|
const normalized = key.toLowerCase().replaceAll(/[^a-z0-9]/g, '');
|
|
if (BANNED_KEYS.has(normalized)) return true;
|
|
if (containsSensitiveMaterial(entry)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function canonicalFile(filePath, label) {
|
|
if (
|
|
typeof filePath !== 'string' ||
|
|
!path.isAbsolute(filePath) ||
|
|
filePath.length > 4096 ||
|
|
CONTROL_PATTERN.test(filePath)
|
|
) {
|
|
fail(`${label} path is invalid`);
|
|
}
|
|
let stat;
|
|
try {
|
|
stat = fs.lstatSync(filePath);
|
|
} catch {
|
|
fail(`${label} is unavailable`);
|
|
}
|
|
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
if (
|
|
!stat.isFile() ||
|
|
stat.isSymbolicLink() ||
|
|
stat.size < 2 ||
|
|
stat.size > MAX_FILE_BYTES ||
|
|
uid === null ||
|
|
stat.uid !== uid ||
|
|
(stat.mode & 0o077) !== 0 ||
|
|
fs.realpathSync(filePath) !== filePath
|
|
) {
|
|
fail(`${label} must be one canonical owner-private bounded file`);
|
|
}
|
|
return stat;
|
|
}
|
|
|
|
function readDocument(filePath, label) {
|
|
const before = canonicalFile(filePath, label);
|
|
let descriptor = -1;
|
|
let bytes;
|
|
try {
|
|
descriptor = fs.openSync(
|
|
filePath,
|
|
fs.constants.O_RDONLY |
|
|
(fs.constants.O_CLOEXEC ?? 0) |
|
|
(fs.constants.O_NOFOLLOW ?? 0),
|
|
);
|
|
const opened = fs.fstatSync(descriptor);
|
|
if (
|
|
opened.dev !== before.dev ||
|
|
opened.ino !== before.ino ||
|
|
opened.uid !== before.uid ||
|
|
opened.mode !== before.mode ||
|
|
opened.size !== before.size
|
|
) {
|
|
fail(`${label} changed before it was opened`);
|
|
}
|
|
bytes = Buffer.alloc(opened.size);
|
|
let offset = 0;
|
|
while (offset < bytes.length) {
|
|
const count = fs.readSync(
|
|
descriptor,
|
|
bytes,
|
|
offset,
|
|
bytes.length - offset,
|
|
offset,
|
|
);
|
|
if (count < 1) fail(`${label} could not be read completely`);
|
|
offset += count;
|
|
}
|
|
const after = fs.fstatSync(descriptor);
|
|
if (
|
|
after.dev !== opened.dev ||
|
|
after.ino !== opened.ino ||
|
|
after.uid !== opened.uid ||
|
|
after.mode !== opened.mode ||
|
|
after.size !== opened.size
|
|
) {
|
|
fail(`${label} changed while it was read`);
|
|
}
|
|
let value;
|
|
try {
|
|
value = JSON.parse(
|
|
new TextDecoder('utf-8', { fatal: true }).decode(bytes),
|
|
);
|
|
} catch {
|
|
fail(`${label} must contain UTF-8 JSON`);
|
|
}
|
|
return Object.freeze({ bytes, value });
|
|
} catch (error) {
|
|
if (error instanceof WorkerCredentialManagementReleaseEvidenceError) {
|
|
throw error;
|
|
}
|
|
fail(`${label} could not be read safely`);
|
|
} finally {
|
|
if (descriptor >= 0) fs.closeSync(descriptor);
|
|
}
|
|
}
|
|
|
|
function unusedOutput(filePath) {
|
|
if (
|
|
typeof filePath !== 'string' ||
|
|
!path.isAbsolute(filePath) ||
|
|
filePath.length > 4096 ||
|
|
CONTROL_PATTERN.test(filePath) ||
|
|
fs.existsSync(filePath) ||
|
|
fs.realpathSync(path.dirname(filePath)) !== path.dirname(filePath)
|
|
) {
|
|
fail('output path must be unused in one canonical directory');
|
|
}
|
|
}
|
|
|
|
function writeNoReplace(filePath, value) {
|
|
unusedOutput(filePath);
|
|
const bytes = Buffer.from(`${JSON.stringify(value)}\n`, 'utf8');
|
|
let descriptor = -1;
|
|
try {
|
|
descriptor = fs.openSync(
|
|
filePath,
|
|
fs.constants.O_WRONLY |
|
|
fs.constants.O_CREAT |
|
|
fs.constants.O_EXCL |
|
|
(fs.constants.O_CLOEXEC ?? 0) |
|
|
(fs.constants.O_NOFOLLOW ?? 0),
|
|
0o600,
|
|
);
|
|
let offset = 0;
|
|
while (offset < bytes.length) {
|
|
const count = fs.writeSync(
|
|
descriptor,
|
|
bytes,
|
|
offset,
|
|
bytes.length - offset,
|
|
offset,
|
|
);
|
|
if (count < 1) fail('release evidence could not be written completely');
|
|
offset += count;
|
|
}
|
|
fs.fsyncSync(descriptor);
|
|
} finally {
|
|
bytes.fill(0);
|
|
if (descriptor >= 0) fs.closeSync(descriptor);
|
|
}
|
|
}
|
|
|
|
function readSourceDocuments(options) {
|
|
const documents = {};
|
|
try {
|
|
documents.ceremony = readDocument(
|
|
options.ceremonyReportFile,
|
|
'ceremony report',
|
|
);
|
|
documents.durable = readDocument(
|
|
options.durableAuditReportFile,
|
|
'durable audit report',
|
|
);
|
|
documents.pki = readDocument(
|
|
options.pkiRotationReportFile,
|
|
'PKI rotation report',
|
|
);
|
|
documents.caRollover = readDocument(
|
|
options.caRolloverReportFile,
|
|
'CA rollover report',
|
|
);
|
|
return Object.freeze(documents);
|
|
} catch (error) {
|
|
for (const document of Object.values(documents)) document.bytes.fill(0);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function clearSourceDocuments(documents) {
|
|
for (const document of Object.values(documents)) document.bytes.fill(0);
|
|
}
|
|
|
|
function sourceAudits(documents) {
|
|
return [
|
|
validateWorkerCredentialManagementLiveCeremony(
|
|
documents.ceremony.value,
|
|
),
|
|
validateWorkerCredentialManagementDurableAuditEvidence(
|
|
documents.durable.value,
|
|
),
|
|
validateWorkerCredentialManagementPkiRotationEvidence(
|
|
documents.pki.value,
|
|
),
|
|
validateWorkerCredentialManagementCaRolloverEvidence(
|
|
documents.caRollover.value,
|
|
),
|
|
];
|
|
}
|
|
|
|
function verifySourceDocuments(documents) {
|
|
if (sourceAudits(documents).some(({ compatible }) => !compatible)) {
|
|
fail('one or more source reports are incompatible');
|
|
}
|
|
const ceremony = documents.ceremony.value;
|
|
const durable = documents.durable.value;
|
|
const pki = documents.pki.value;
|
|
const caRollover = documents.caRollover.value;
|
|
const observed = [ceremony, durable, pki, caRollover].map(({ observedAt }) =>
|
|
Date.parse(observedAt),
|
|
);
|
|
if (
|
|
observed[1] < observed[0] ||
|
|
observed[2] < observed[1] ||
|
|
observed[3] < observed[1]
|
|
) {
|
|
fail('source report observation order is invalid');
|
|
}
|
|
const ceremonySha256 = rawDigest(documents.ceremony.bytes);
|
|
const durableSha256 = rawDigest(documents.durable.bytes);
|
|
if (
|
|
durable.source.ceremonyReportSha256 !== ceremonySha256 ||
|
|
pki.source.ceremonyReportSha256 !== ceremonySha256 ||
|
|
caRollover.source.ceremonyReportSha256 !== ceremonySha256 ||
|
|
pki.source.durableAuditReportSha256 !== durableSha256 ||
|
|
caRollover.source.durableAuditReportSha256 !== durableSha256
|
|
) {
|
|
fail('source report digest chain is not exact');
|
|
}
|
|
if (
|
|
durable.durableState.actionRefSha256 !==
|
|
ceremony.ceremony.actionRefSha256 ||
|
|
durable.durableState.authorityProjectIdSha256 !==
|
|
ceremony.ceremony.authorityProjectIdSha256 ||
|
|
durable.durableState.planDigest !== ceremony.ceremony.planDigest ||
|
|
durable.durableState.previewDigest !== ceremony.ceremony.previewDigest ||
|
|
durable.durableState.requesterSubjectSha256 !==
|
|
ceremony.identity.requesterSubjectSha256 ||
|
|
durable.durableState.reviewerSubjectSha256 !==
|
|
ceremony.identity.reviewerSubjectSha256
|
|
) {
|
|
fail('durable review state is not bound to the ceremony');
|
|
}
|
|
const operatorSubjects = [
|
|
ceremony.identity.requesterSubjectSha256,
|
|
ceremony.identity.reviewerSubjectSha256,
|
|
];
|
|
for (const identity of [pki.identity, caRollover.identity]) {
|
|
if (
|
|
identity.providerKind !== ceremony.identity.providerKind ||
|
|
identity.issuer !== ceremony.identity.issuer ||
|
|
identity.audience !== ceremony.identity.audience ||
|
|
identity.type !== ceremony.identity.type ||
|
|
identity.purpose !== ceremony.identity.purpose ||
|
|
!operatorSubjects.includes(identity.subjectSha256)
|
|
) {
|
|
fail('source report identity does not match the reviewed ceremony');
|
|
}
|
|
}
|
|
if (pki.identity.subjectSha256 !== caRollover.identity.subjectSha256) {
|
|
fail('source reports were not collected by one reviewed operator');
|
|
}
|
|
if (
|
|
pki.transport.endpointSha256 !==
|
|
caRollover.transport.endpointSha256 ||
|
|
pki.transport.servernameSha256 !==
|
|
caRollover.transport.servernameSha256 ||
|
|
pki.transport.serverTrustBundleSha256 !==
|
|
caRollover.transport.serverTrustBundleSha256 ||
|
|
pki.transport.commandSha256 !== caRollover.transport.commandSha256
|
|
) {
|
|
fail('source report management transport is not identical');
|
|
}
|
|
if (
|
|
pki.kubernetes.clusterServerSha256 !==
|
|
caRollover.kubernetes.clusterServerSha256 ||
|
|
pki.kubernetes.collectorSubjectSha256 !==
|
|
caRollover.kubernetes.collectorSubjectSha256 ||
|
|
pki.kubernetes.deploymentUidSha256 !==
|
|
caRollover.kubernetes.deploymentUidSha256
|
|
) {
|
|
fail('source report Kubernetes authority is not identical');
|
|
}
|
|
return Object.freeze({
|
|
ceremony,
|
|
durable,
|
|
pki,
|
|
caRollover,
|
|
digests: Object.freeze({
|
|
ceremony: ceremonySha256,
|
|
durable: durableSha256,
|
|
pki: rawDigest(documents.pki.bytes),
|
|
caRollover: rawDigest(documents.caRollover.bytes),
|
|
}),
|
|
});
|
|
}
|
|
|
|
function buildReleaseEvidence(facts, observedAt) {
|
|
const { ceremony, durable, pki, caRollover, digests } = facts;
|
|
return Object.freeze({
|
|
schemaVersion: 1,
|
|
fixture: FIXTURE,
|
|
observedAt,
|
|
source: Object.freeze({
|
|
ceremonyReportSha256: digests.ceremony,
|
|
durableAuditReportSha256: digests.durable,
|
|
pkiRotationReportSha256: digests.pki,
|
|
caRolloverReportSha256: digests.caRollover,
|
|
ceremonyFixture: CEREMONY_FIXTURE,
|
|
durableAuditFixture: DURABLE_FIXTURE,
|
|
pkiRotationFixture: PKI_FIXTURE,
|
|
caRolloverFixture: CA_ROLLOVER_FIXTURE,
|
|
}),
|
|
authority: Object.freeze({
|
|
providerKind: ceremony.identity.providerKind,
|
|
issuer: ceremony.identity.issuer,
|
|
audience: ceremony.identity.audience,
|
|
type: ceremony.identity.type,
|
|
purpose: ceremony.identity.purpose,
|
|
requesterSubjectSha256: ceremony.identity.requesterSubjectSha256,
|
|
reviewerSubjectSha256: ceremony.identity.reviewerSubjectSha256,
|
|
operatorSubjectSha256: pki.identity.subjectSha256,
|
|
actionRefSha256: ceremony.ceremony.actionRefSha256,
|
|
authorityProjectIdSha256:
|
|
ceremony.ceremony.authorityProjectIdSha256,
|
|
planDigest: ceremony.ceremony.planDigest,
|
|
previewDigest: ceremony.ceremony.previewDigest,
|
|
}),
|
|
transport: Object.freeze({
|
|
endpointSha256: pki.transport.endpointSha256,
|
|
servernameSha256: pki.transport.servernameSha256,
|
|
serverTrustBundleSha256: pki.transport.serverTrustBundleSha256,
|
|
commandSha256: pki.transport.commandSha256,
|
|
}),
|
|
deployment: Object.freeze({
|
|
clusterServerSha256: pki.kubernetes.clusterServerSha256,
|
|
collectorSubjectSha256: pki.kubernetes.collectorSubjectSha256,
|
|
deploymentUidSha256: pki.kubernetes.deploymentUidSha256,
|
|
pkiGenerations: Object.freeze([
|
|
pki.kubernetes.beforeGeneration,
|
|
pki.kubernetes.afterGeneration,
|
|
]),
|
|
caRolloverGenerations: caRollover.kubernetes.generations,
|
|
pkiPodsFullyReplaced: pki.kubernetes.oldPodsFullyReplaced,
|
|
caRolloverPodsFullyReplaced:
|
|
caRollover.kubernetes.allGenerationsFullyReplaced,
|
|
twoReadyReplicasOnDistinctNodes: true,
|
|
}),
|
|
controls: Object.freeze({
|
|
externalCeremonyObserved: ceremony.gates.passed,
|
|
durableAuditObserved: durable.gates.passed,
|
|
sameIssuerRevocationObserved: pki.gates.passed,
|
|
caRolloverObserved: caRollover.gates.passed,
|
|
serverAndClientTrustSeparated:
|
|
pki.gates.serverTrustSeparatedFromClientIssuer &&
|
|
caRollover.gates.serverTrustSeparatedFromClientIssuer,
|
|
noExecutionOrConsumption:
|
|
ceremony.gates.noExecutionOrConsumption &&
|
|
durable.gates.noExecutionOrConsumption,
|
|
exactReadOnlyCollectorAuthority:
|
|
pki.gates.readOnlyCollectorAuthority &&
|
|
caRollover.gates.readOnlyCollectorAuthority,
|
|
}),
|
|
gates: Object.freeze({
|
|
sourceReportsCompatible: true,
|
|
sourceDigestChainBound: true,
|
|
externalIdentityBound: true,
|
|
reviewedDurableStateBound: true,
|
|
managementTransportBound: true,
|
|
deploymentAuthorityBound: true,
|
|
sameIssuerRevocationObserved: true,
|
|
clientCaRolloverObserved: true,
|
|
serverAndClientTrustSeparated: true,
|
|
noExecutionOrConsumption: true,
|
|
readOnlyEvidenceAuthority: true,
|
|
passed: true,
|
|
}),
|
|
});
|
|
}
|
|
|
|
function validIncreasingIntegers(value, length) {
|
|
return (
|
|
Array.isArray(value) &&
|
|
value.length === length &&
|
|
value.every((entry) => Number.isSafeInteger(entry) && entry >= 1) &&
|
|
value.every((entry, index) => index === 0 || value[index - 1] < entry)
|
|
);
|
|
}
|
|
|
|
function validateWorkerCredentialManagementReleaseEvidence(report) {
|
|
const findings = [];
|
|
const add = (code) => findings.push(Object.freeze({ code }));
|
|
if (
|
|
!exactKeys(report, [
|
|
'schemaVersion',
|
|
'fixture',
|
|
'observedAt',
|
|
'source',
|
|
'authority',
|
|
'transport',
|
|
'deployment',
|
|
'controls',
|
|
'gates',
|
|
]) ||
|
|
report?.schemaVersion !== 1 ||
|
|
report?.fixture !== FIXTURE ||
|
|
!isIsoTime(report?.observedAt)
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_RELEASE_EVIDENCE_SHAPE');
|
|
}
|
|
const source = report?.source;
|
|
if (
|
|
!exactKeys(source, [
|
|
'ceremonyReportSha256',
|
|
'durableAuditReportSha256',
|
|
'pkiRotationReportSha256',
|
|
'caRolloverReportSha256',
|
|
'ceremonyFixture',
|
|
'durableAuditFixture',
|
|
'pkiRotationFixture',
|
|
'caRolloverFixture',
|
|
]) ||
|
|
![
|
|
source?.ceremonyReportSha256,
|
|
source?.durableAuditReportSha256,
|
|
source?.pkiRotationReportSha256,
|
|
source?.caRolloverReportSha256,
|
|
].every((entry) => SHA256_PATTERN.test(entry)) ||
|
|
source?.ceremonyFixture !== CEREMONY_FIXTURE ||
|
|
source?.durableAuditFixture !== DURABLE_FIXTURE ||
|
|
source?.pkiRotationFixture !== PKI_FIXTURE ||
|
|
source?.caRolloverFixture !== CA_ROLLOVER_FIXTURE
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_RELEASE_EVIDENCE_SOURCE');
|
|
}
|
|
const authority = report?.authority;
|
|
if (
|
|
!exactKeys(authority, [
|
|
'providerKind',
|
|
'issuer',
|
|
'audience',
|
|
'type',
|
|
'purpose',
|
|
'requesterSubjectSha256',
|
|
'reviewerSubjectSha256',
|
|
'operatorSubjectSha256',
|
|
'actionRefSha256',
|
|
'authorityProjectIdSha256',
|
|
'planDigest',
|
|
'previewDigest',
|
|
]) ||
|
|
authority?.providerKind !== 'external_oidc' ||
|
|
!externalIssuer(authority?.issuer) ||
|
|
authority?.audience !== 'qinglong3-worker-credential-management' ||
|
|
authority?.type !== 'ql3-worker-credential-management+jwt' ||
|
|
authority?.purpose !== 'worker-credential-management' ||
|
|
![
|
|
authority?.requesterSubjectSha256,
|
|
authority?.reviewerSubjectSha256,
|
|
authority?.operatorSubjectSha256,
|
|
authority?.actionRefSha256,
|
|
authority?.authorityProjectIdSha256,
|
|
].every((entry) => SHA256_PATTERN.test(entry)) ||
|
|
authority?.requesterSubjectSha256 ===
|
|
authority?.reviewerSubjectSha256 ||
|
|
![
|
|
authority?.requesterSubjectSha256,
|
|
authority?.reviewerSubjectSha256,
|
|
].includes(authority?.operatorSubjectSha256) ||
|
|
!HEX_DIGEST_PATTERN.test(authority?.planDigest) ||
|
|
!HEX_DIGEST_PATTERN.test(authority?.previewDigest)
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_RELEASE_EVIDENCE_AUTHORITY');
|
|
}
|
|
if (
|
|
!exactKeys(report?.transport, [
|
|
'endpointSha256',
|
|
'servernameSha256',
|
|
'serverTrustBundleSha256',
|
|
'commandSha256',
|
|
]) ||
|
|
Object.values(report?.transport ?? {}).some(
|
|
(entry) => !SHA256_PATTERN.test(entry),
|
|
)
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_RELEASE_EVIDENCE_TRANSPORT');
|
|
}
|
|
const deployment = report?.deployment;
|
|
if (
|
|
!exactKeys(deployment, [
|
|
'clusterServerSha256',
|
|
'collectorSubjectSha256',
|
|
'deploymentUidSha256',
|
|
'pkiGenerations',
|
|
'caRolloverGenerations',
|
|
'pkiPodsFullyReplaced',
|
|
'caRolloverPodsFullyReplaced',
|
|
'twoReadyReplicasOnDistinctNodes',
|
|
]) ||
|
|
![
|
|
deployment?.clusterServerSha256,
|
|
deployment?.collectorSubjectSha256,
|
|
deployment?.deploymentUidSha256,
|
|
].every((entry) => SHA256_PATTERN.test(entry)) ||
|
|
!validIncreasingIntegers(deployment?.pkiGenerations, 2) ||
|
|
!validIncreasingIntegers(deployment?.caRolloverGenerations, 3) ||
|
|
deployment?.pkiPodsFullyReplaced !== true ||
|
|
deployment?.caRolloverPodsFullyReplaced !== true ||
|
|
deployment?.twoReadyReplicasOnDistinctNodes !== true
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_RELEASE_EVIDENCE_DEPLOYMENT');
|
|
}
|
|
if (
|
|
!exactKeys(report?.controls, [
|
|
'externalCeremonyObserved',
|
|
'durableAuditObserved',
|
|
'sameIssuerRevocationObserved',
|
|
'caRolloverObserved',
|
|
'serverAndClientTrustSeparated',
|
|
'noExecutionOrConsumption',
|
|
'exactReadOnlyCollectorAuthority',
|
|
]) ||
|
|
Object.values(report?.controls ?? {}).some((entry) => entry !== true)
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_RELEASE_EVIDENCE_CONTROLS');
|
|
}
|
|
if (
|
|
!exactKeys(report?.gates, [
|
|
'sourceReportsCompatible',
|
|
'sourceDigestChainBound',
|
|
'externalIdentityBound',
|
|
'reviewedDurableStateBound',
|
|
'managementTransportBound',
|
|
'deploymentAuthorityBound',
|
|
'sameIssuerRevocationObserved',
|
|
'clientCaRolloverObserved',
|
|
'serverAndClientTrustSeparated',
|
|
'noExecutionOrConsumption',
|
|
'readOnlyEvidenceAuthority',
|
|
'passed',
|
|
]) ||
|
|
Object.values(report?.gates ?? {}).some((entry) => entry !== true)
|
|
) {
|
|
add('QL3_WORKER_MANAGEMENT_RELEASE_EVIDENCE_GATES');
|
|
}
|
|
if (containsSensitiveMaterial(report)) {
|
|
add('QL3_WORKER_MANAGEMENT_RELEASE_EVIDENCE_SECRET_EXPOSURE');
|
|
}
|
|
return Object.freeze({
|
|
compatible: findings.length === 0,
|
|
findings: Object.freeze(findings),
|
|
});
|
|
}
|
|
|
|
function auditWorkerCredentialManagementReleaseEvidence(report, documents) {
|
|
const structural = validateWorkerCredentialManagementReleaseEvidence(report);
|
|
if (!structural.compatible) return structural;
|
|
try {
|
|
const facts = verifySourceDocuments(documents);
|
|
const newestSourceMs = Math.max(
|
|
Date.parse(facts.ceremony.observedAt),
|
|
Date.parse(facts.durable.observedAt),
|
|
Date.parse(facts.pki.observedAt),
|
|
Date.parse(facts.caRollover.observedAt),
|
|
);
|
|
if (Date.parse(report.observedAt) < newestSourceMs) {
|
|
fail('release evidence predates one or more source reports');
|
|
}
|
|
const expected = buildReleaseEvidence(facts, report.observedAt);
|
|
if (JSON.stringify(report) !== JSON.stringify(expected)) {
|
|
return Object.freeze({
|
|
compatible: false,
|
|
findings: Object.freeze([
|
|
Object.freeze({
|
|
code: 'QL3_WORKER_MANAGEMENT_RELEASE_EVIDENCE_SOURCE_MISMATCH',
|
|
}),
|
|
]),
|
|
});
|
|
}
|
|
return Object.freeze({ compatible: true, findings: Object.freeze([]) });
|
|
} catch (error) {
|
|
if (!(error instanceof WorkerCredentialManagementReleaseEvidenceError)) {
|
|
throw error;
|
|
}
|
|
return Object.freeze({
|
|
compatible: false,
|
|
findings: Object.freeze([
|
|
Object.freeze({
|
|
code: 'QL3_WORKER_MANAGEMENT_RELEASE_EVIDENCE_SOURCE_INCOMPATIBLE',
|
|
}),
|
|
]),
|
|
});
|
|
}
|
|
}
|
|
|
|
function optionKeys() {
|
|
return [
|
|
'ceremonyReportFile',
|
|
'durableAuditReportFile',
|
|
'pkiRotationReportFile',
|
|
'caRolloverReportFile',
|
|
'outputFile',
|
|
];
|
|
}
|
|
|
|
function runWorkerCredentialManagementReleaseEvidence(
|
|
options,
|
|
dependencies = { now: Date.now },
|
|
) {
|
|
exactObject(options, optionKeys(), 'release evidence options');
|
|
exactObject(dependencies, ['now'], 'release evidence dependencies');
|
|
if (typeof dependencies.now !== 'function') {
|
|
fail('release evidence clock is invalid');
|
|
}
|
|
unusedOutput(options.outputFile);
|
|
const nowMs = dependencies.now();
|
|
if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
|
|
fail('release evidence clock is invalid');
|
|
}
|
|
const documents = readSourceDocuments(options);
|
|
try {
|
|
const facts = verifySourceDocuments(documents);
|
|
const report = buildReleaseEvidence(
|
|
facts,
|
|
new Date(nowMs).toISOString(),
|
|
);
|
|
const audit = auditWorkerCredentialManagementReleaseEvidence(
|
|
report,
|
|
documents,
|
|
);
|
|
if (!audit.compatible) fail('assembled release evidence failed audit');
|
|
writeNoReplace(options.outputFile, report);
|
|
return report;
|
|
} finally {
|
|
clearSourceDocuments(documents);
|
|
}
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const values = {};
|
|
for (const argument of argv) {
|
|
if (argument === '--') continue;
|
|
const match = /^--([a-z-]+)=(.+)$/.exec(argument);
|
|
if (!match || Object.hasOwn(values, match[1])) fail('arguments are invalid');
|
|
values[match[1]] = match[2];
|
|
}
|
|
const expected = [
|
|
'ceremony-report',
|
|
'durable-audit-report',
|
|
'pki-rotation-report',
|
|
'ca-rollover-report',
|
|
'output',
|
|
];
|
|
if (
|
|
JSON.stringify(Object.keys(values).sort()) !==
|
|
JSON.stringify(expected.sort())
|
|
) {
|
|
fail('arguments are invalid');
|
|
}
|
|
return Object.freeze({
|
|
ceremonyReportFile: values['ceremony-report'],
|
|
durableAuditReportFile: values['durable-audit-report'],
|
|
pkiRotationReportFile: values['pki-rotation-report'],
|
|
caRolloverReportFile: values['ca-rollover-report'],
|
|
outputFile: values.output,
|
|
});
|
|
}
|
|
|
|
function runCli(argv) {
|
|
const options = parseArguments(argv);
|
|
runWorkerCredentialManagementReleaseEvidence(options, { now: Date.now });
|
|
process.stdout.write(
|
|
`${JSON.stringify({ schemaVersion: 1, fixture: FIXTURE, compatible: true })}\n`,
|
|
);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
runCli(process.argv.slice(2));
|
|
} catch (error) {
|
|
process.stderr.write(
|
|
`${
|
|
error instanceof Error
|
|
? error.message
|
|
: 'Worker management release evidence failed'
|
|
}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
CA_ROLLOVER_FIXTURE,
|
|
CEREMONY_FIXTURE,
|
|
DURABLE_FIXTURE,
|
|
FIXTURE,
|
|
PKI_FIXTURE,
|
|
WorkerCredentialManagementReleaseEvidenceError,
|
|
auditWorkerCredentialManagementReleaseEvidence,
|
|
buildReleaseEvidence,
|
|
clearSourceDocuments,
|
|
parseArguments,
|
|
readDocument,
|
|
readSourceDocuments,
|
|
runWorkerCredentialManagementReleaseEvidence,
|
|
validateWorkerCredentialManagementReleaseEvidence,
|
|
verifySourceDocuments,
|
|
};
|