Files
qinglong/scripts/ql3-worker-credential-management-release-evidence.cjs
T

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,
};