#!/usr/bin/env node 'use strict'; const crypto = require('node:crypto'); const fs = require('node:fs'); const net = require('node:net'); const os = require('node:os'); const path = require('node:path'); const { TextDecoder } = require('node:util'); const FIXTURE = 'qinglong/worker-credential-management-live-ceremony@v1'; const TYPE = 'ql3-worker-credential-management+jwt'; const PURPOSE = 'worker-credential-management'; const AUDIENCE = 'qinglong3-worker-credential-management'; const MAX_FILE_BYTES = 1024 * 1024; const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/; const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/; const TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/; const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/; const BANNED_KEYS = new Set([ 'assertion', 'authorization', 'bearer', 'connectionstring', 'dsn', 'password', 'privatekey', 'secret', 'tlskey', 'token', ]); class WorkerCredentialManagementLiveCeremonyError extends Error { constructor(message) { super(`Worker credential management live ceremony failed: ${message}`); this.name = 'WorkerCredentialManagementLiveCeremonyError'; } } function fail(message) { throw new WorkerCredentialManagementLiveCeremonyError(message); } function exactObject(value, keys, label) { if ( !value || typeof value !== 'object' || Array.isArray(value) || JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...keys].sort()) ) { fail(`${label} shape is invalid`); } return value; } function exactKeys(value, keys) { return ( value !== null && typeof value === 'object' && !Array.isArray(value) && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort()) ); } function sha256(domain, value) { return `sha256:${crypto .createHash('sha256') .update(domain) .update('\0') .update(value) .digest('hex')}`; } function externalIssuer(value) { if (typeof value !== 'string' || value.length > 512) return false; let parsed; try { parsed = new URL(value); } catch { return false; } const hostname = parsed.hostname.toLowerCase(); return ( parsed.protocol === 'https:' && parsed.username === '' && parsed.password === '' && parsed.search === '' && parsed.hash === '' && parsed.toString() === value && net.isIP(hostname) === 0 && hostname !== 'localhost' && !hostname.endsWith('.localhost') && !hostname.endsWith('.local') && !hostname.endsWith('.test') && !hostname.endsWith('.invalid') && !hostname.endsWith('.example') ); } function canonicalFile(filePath, label, options = {}) { 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 privateFile = options.private !== false; const uid = typeof process.getuid === 'function' ? process.getuid() : null; if ( !stat.isFile() || stat.isSymbolicLink() || stat.size < 1 || stat.size > (options.maximum ?? MAX_FILE_BYTES) || fs.realpathSync(filePath) !== filePath || (privateFile && (uid === null || stat.uid !== uid)) || (stat.mode & (privateFile ? 0o077 : 0o022)) !== 0 ) { fail( `${label} must be a canonical bounded ${ privateFile ? 'private ' : '' }file`, ); } return filePath; } function readPrivateText(filePath, label, maximum = MAX_FILE_BYTES) { canonicalFile(filePath, label, { maximum }); const before = fs.lstatSync(filePath); 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`); } return new TextDecoder('utf-8', { fatal: true }).decode(bytes).trim(); } catch (error) { if (error instanceof WorkerCredentialManagementLiveCeremonyError) { throw error; } fail(`${label} could not be read safely`); } finally { bytes?.fill(0); if (descriptor >= 0) fs.closeSync(descriptor); } } function readPrivateJson(filePath, label) { try { return JSON.parse(readPrivateText(filePath, label)); } catch (error) { if (error instanceof WorkerCredentialManagementLiveCeremonyError) { throw error; } fail(`${label} must contain JSON`); } } function parseSegment(value, label, maximum) { if ( typeof value !== 'string' || value.length < 1 || !BASE64URL_PATTERN.test(value) ) { fail(`${label} encoding is invalid`); } const bytes = Buffer.from(value, 'base64url'); if ( bytes.length < 2 || bytes.length > maximum || bytes.toString('base64url') !== value ) { fail(`${label} encoding is invalid`); } try { const value = JSON.parse(bytes.toString('utf8')); if (!value || typeof value !== 'object' || Array.isArray(value)) { fail(`${label} must be an object`); } return value; } catch (error) { if (error instanceof WorkerCredentialManagementLiveCeremonyError) { throw error; } fail(`${label} JSON is invalid`); } } function assertionIdentity(assertion, nowMs) { if ( typeof assertion !== 'string' || assertion.length > 16 * 1024 || CONTROL_PATTERN.test(assertion) ) { fail('identity assertion is invalid'); } const segments = assertion.split('.'); if (segments.length !== 3) fail('identity assertion compact shape is invalid'); if ( !BASE64URL_PATTERN.test(segments[2]) || Buffer.from(segments[2], 'base64url').length < 32 || Buffer.from(segments[2], 'base64url').length > 512 || Buffer.from(segments[2], 'base64url').toString('base64url') !== segments[2] ) { fail('identity signature encoding is invalid'); } const header = parseSegment(segments[0], 'identity header', 1024); exactObject(header, ['alg', 'kid', 'typ'], 'identity header'); const claims = parseSegment(segments[1], 'identity claims', 8 * 1024); const claimKeys = [ 'acr', 'amr', 'aud', 'auth_time', 'exp', 'iat', 'iss', 'jti', 'ql3_purpose', 'sub', ]; if (Object.hasOwn(claims, 'nbf')) claimKeys.push('nbf'); exactObject(claims, claimKeys, 'identity claims'); const now = Math.floor(nowMs / 1000); if ( header.typ !== TYPE || typeof header.alg !== 'string' || header.alg === 'none' || !TOKEN_PATTERN.test(header.kid) || claims.aud !== AUDIENCE || claims.ql3_purpose !== PURPOSE || !externalIssuer(claims.iss) || !TOKEN_PATTERN.test(claims.sub) || !TOKEN_PATTERN.test(claims.jti) || typeof claims.acr !== 'string' || claims.acr.length < 1 || claims.acr.length > 256 || CONTROL_PATTERN.test(claims.acr) || !Array.isArray(claims.amr) || claims.amr.length < 1 || claims.amr.length > 8 || claims.amr.some((value) => !TOKEN_PATTERN.test(value)) || new Set(claims.amr).size !== claims.amr.length || ![claims.iat, claims.auth_time, claims.exp].every( (value) => Number.isSafeInteger(value) && value >= 0, ) || claims.exp <= now || claims.iat > now + 60 || claims.auth_time > now + 60 || claims.exp - claims.iat < 30 || claims.exp - claims.iat > 15 * 60 || now - claims.auth_time > 15 * 60 || (claims.nbf !== undefined && (!Number.isSafeInteger(claims.nbf) || claims.nbf > now + 60)) ) { fail( 'identity assertion does not bind one live strong Worker management identity', ); } return Object.freeze({ issuer: claims.iss, subject: claims.sub, jti: claims.jti, kid: header.kid, acr: claims.acr, amr: Object.freeze([...claims.amr]), }); } function ceremonyCommands(value, normalize) { exactObject( value, [ 'schemaVersion', 'planRequest', 'approvalRequestId', 'approvalAuditEventId', 'requesterDecisionId', 'requesterDecisionAuditEventId', 'reviewerDecisionId', 'reviewerDecisionAuditEventId', 'decisionReasonCode', 'inspectionId', ], 'ceremony', ); if (value.schemaVersion !== 1) fail('ceremony schemaVersion is invalid'); for (const name of [ 'approvalRequestId', 'approvalAuditEventId', 'requesterDecisionId', 'requesterDecisionAuditEventId', 'reviewerDecisionId', 'reviewerDecisionAuditEventId', 'decisionReasonCode', 'inspectionId', ]) { if (!TOKEN_PATTERN.test(value[name])) fail(`ceremony ${name} is invalid`); } if ( value.requesterDecisionId === value.reviewerDecisionId || value.requesterDecisionAuditEventId === value.reviewerDecisionAuditEventId ) { fail('requester and reviewer decision identities must be distinct'); } const shared = { actionRef: value.planRequest?.actionRef, authorityProjectId: value.planRequest?.authorityProjectId, approvalRequestId: value.approvalRequestId, }; const commands = { plan: { schemaVersion: 1, operation: 'worker-credential.plan', request: value.planRequest, }, propose: { schemaVersion: 1, operation: 'worker-credential.propose', request: { ...shared, approvalAuditEventId: value.approvalAuditEventId, }, }, requesterDecide: { schemaVersion: 1, operation: 'worker-credential.decide', request: { ...shared, expectedVersion: 1, decisionId: value.requesterDecisionId, auditEventId: value.requesterDecisionAuditEventId, decision: 'approved', reasonCode: value.decisionReasonCode, }, }, reviewerDecide: { schemaVersion: 1, operation: 'worker-credential.decide', request: { ...shared, expectedVersion: 1, decisionId: value.reviewerDecisionId, auditEventId: value.reviewerDecisionAuditEventId, decision: 'approved', reasonCode: value.decisionReasonCode, }, }, inspect: { schemaVersion: 1, operation: 'worker-credential.inspect', request: { ...shared, inspectionId: value.inspectionId }, }, }; for (const command of Object.values(commands)) normalize(command); return Object.freeze(commands); } function containsSensitiveMaterial(value, key = '') { if (BANNED_KEYS.has(key.toLowerCase())) return true; if (typeof value === 'string') { return ( /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/.test(value) || /postgres(?:ql)?:\/\/[^/\s]+:[^@\s]+@/i.test(value) || /\beyJ[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/.test( value, ) ); } if (Array.isArray(value)) { return value.some((entry) => containsSensitiveMaterial(entry)); } if (value && typeof value === 'object') { return Object.entries(value).some(([childKey, child]) => containsSensitiveMaterial(child, childKey), ); } return false; } function isIsoTime(value) { return ( typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(value) && Number.isFinite(Date.parse(value)) ); } function validateWorkerCredentialManagementLiveCeremony(report) { const findings = []; const add = (code) => findings.push(Object.freeze({ code })); if ( !report || typeof report !== 'object' || Array.isArray(report) || JSON.stringify(Object.keys(report).sort()) !== JSON.stringify( [ 'schemaVersion', 'fixture', 'observedAt', 'identity', 'ceremony', 'gates', ].sort(), ) || report.schemaVersion !== 1 || report.fixture !== FIXTURE || !isIsoTime(report.observedAt) ) { add('QL3_WORKER_MANAGEMENT_LIVE_CEREMONY_SHAPE'); } const identity = report?.identity; if ( !exactKeys(identity, [ 'providerKind', 'issuer', 'discoveryDocumentSha256', 'jwksSha256', 'audience', 'type', 'purpose', 'requesterSubjectSha256', 'reviewerSubjectSha256', 'requesterKeyIdSha256', 'reviewerKeyIdSha256', ]) || !externalIssuer(identity.issuer) || identity.providerKind !== 'external_oidc' || identity.audience !== AUDIENCE || identity.type !== TYPE || identity.purpose !== PURPOSE || !SHA256_PATTERN.test(identity.discoveryDocumentSha256) || !SHA256_PATTERN.test(identity.jwksSha256) || !SHA256_PATTERN.test(identity.requesterSubjectSha256) || !SHA256_PATTERN.test(identity.reviewerSubjectSha256) || identity.requesterSubjectSha256 === identity.reviewerSubjectSha256 || !SHA256_PATTERN.test(identity.requesterKeyIdSha256) || !SHA256_PATTERN.test(identity.reviewerKeyIdSha256) ) { add('QL3_WORKER_MANAGEMENT_LIVE_CEREMONY_IDENTITY'); } const ceremony = report?.ceremony; if ( !exactKeys(ceremony, [ 'actionRefSha256', 'authorityProjectIdSha256', 'planStatus', 'approvalStatus', 'requesterSelfDecisionStatus', 'requesterSelfDecisionCode', 'reviewerDecisionStatus', 'approvalState', 'inspectionStale', 'dispatchCreated', 'approvalConsumed', 'planDigest', 'previewDigest', 'requestIdSha256', ]) || !['created', 'existing'].includes(ceremony.planStatus) || !['created', 'existing'].includes(ceremony.approvalStatus) || ceremony.requesterSelfDecisionStatus !== 403 || ceremony.requesterSelfDecisionCode !== 'forbidden' || !['decided', 'existing'].includes(ceremony.reviewerDecisionStatus) || ceremony.approvalState !== 'approved' || ceremony.inspectionStale !== false || ceremony.dispatchCreated !== false || ceremony.approvalConsumed !== false || !SHA256_PATTERN.test(ceremony.actionRefSha256) || !SHA256_PATTERN.test(ceremony.authorityProjectIdSha256) || !/^[a-f0-9]{64}$/.test(ceremony.planDigest) || !/^[a-f0-9]{64}$/.test(ceremony.previewDigest) || !Array.isArray(ceremony.requestIdSha256) || ceremony.requestIdSha256.length !== 5 || ceremony.requestIdSha256.some((value) => !SHA256_PATTERN.test(value)) || new Set(ceremony.requestIdSha256).size !== 5 ) { add('QL3_WORKER_MANAGEMENT_LIVE_CEREMONY_FLOW'); } const gates = report?.gates; if ( !exactKeys(gates, [ 'externalIdentity', 'workerPurposeBound', 'requesterAndReviewerDistinct', 'requesterSelfDecisionRejected', 'reviewerDecisionAccepted', 'inspectionAuthorized', 'noExecutionOrConsumption', 'passed', ]) || Object.values(gates).some((value) => value !== true) ) { add('QL3_WORKER_MANAGEMENT_LIVE_CEREMONY_GATES'); } if (containsSensitiveMaterial(report)) { add('QL3_WORKER_MANAGEMENT_LIVE_CEREMONY_SECRET_EXPOSURE'); } return Object.freeze({ compatible: findings.length === 0, findings: Object.freeze(findings), }); } function unusedOutput(filePath) { if ( typeof filePath !== 'string' || !path.isAbsolute(filePath) || fs.existsSync(filePath) || fs.realpathSync(path.dirname(filePath)) !== path.dirname(filePath) ) { fail('output must be one unused canonical absolute path'); } return filePath; } function writeNoReplace(filePath, report) { const descriptor = fs.openSync(filePath, 'wx', 0o600); try { fs.writeFileSync(descriptor, `${JSON.stringify(report, null, 2)}\n`); fs.fsyncSync(descriptor); } finally { fs.closeSync(descriptor); } } async function withCommandFile(command, run) { const directory = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-worker-ceremony-')), ); fs.chmodSync(directory, 0o700); const commandFile = path.join(directory, 'command.json'); try { fs.writeFileSync(commandFile, `${JSON.stringify(command)}\n`, { mode: 0o600, }); return await run(commandFile); } finally { fs.rmSync(directory, { recursive: true, force: true }); } } async function runWorkerCredentialManagementLiveCeremony( options, dependencies = {}, ) { exactObject( options, [ 'configFile', 'requesterAssertionFile', 'reviewerAssertionFile', 'ceremonyFile', 'outputFile', ], 'options', ); unusedOutput(options.outputFile); canonicalFile(options.configFile, 'client config'); const now = dependencies.now ?? Date.now; let requesterRaw = readPrivateText( options.requesterAssertionFile, 'requester identity assertion', 16 * 1024, ); let reviewerRaw = readPrivateText( options.reviewerAssertionFile, 'reviewer identity assertion', 16 * 1024, ); const requester = assertionIdentity(requesterRaw, now()); const reviewer = assertionIdentity(reviewerRaw, now()); if ( requester.issuer !== reviewer.issuer || requester.subject === reviewer.subject ) { fail('requester and reviewer must be distinct Users from one issuer'); } const normalize = dependencies.normalize ?? require('@qinglong/cluster-admin/worker-credential-management-transport') .normalizeClusterWorkerCredentialManagementCommand; const commands = ceremonyCommands( readPrivateJson(options.ceremonyFile, 'ceremony'), normalize, ); const collectOidc = dependencies.collectOidc ?? require('./ql3-plugin-package-management-live-evidence-collect.cjs') .collectOidcSnapshot; const oidc = await collectOidc({ issuer: requester.issuer }); const execute = dependencies.execute ?? require('@qinglong/cluster-admin/worker-credential-management-client') .executeClusterWorkerCredentialManagementClient; const invoke = (command, assertionFile) => withCommandFile(command, (commandFile) => execute({ configFile: options.configFile, commandFile, assertionFile }), ); const plan = await invoke(commands.plan, options.requesterAssertionFile); const proposed = await invoke( commands.propose, options.requesterAssertionFile, ); let selfDecision; try { await invoke(commands.requesterDecide, options.requesterAssertionFile); fail('requester self-decision was accepted'); } catch (error) { if ( error instanceof WorkerCredentialManagementLiveCeremonyError || error?.statusCode !== 403 || error?.responseCode !== 'forbidden' || typeof error?.requestId !== 'string' ) { throw error; } selfDecision = error; } const decided = await invoke( commands.reviewerDecide, options.reviewerAssertionFile, ); const inspected = await invoke( commands.inspect, options.reviewerAssertionFile, ); const planValue = plan.result.plan; const proposedApproval = proposed.result.approval; const decidedApproval = decided.result.approval; const inspectedApproval = inspected.result.approval; if ( planValue.requestedBy?.id !== requester.subject || proposed.result.plan.planDigest !== planValue.planDigest || proposedApproval.requestedBy?.id !== requester.subject || proposedApproval.version !== 1 || proposedApproval.state !== 'pending' || proposedApproval.actionDigest !== planValue.planDigest || proposedApproval.previewDigest !== planValue.previewDigest || decidedApproval.requestedBy?.id !== requester.subject || decidedApproval.decidedBy?.id !== reviewer.subject || decidedApproval.state !== 'approved' || inspected.result.stale !== false || !inspectedApproval || inspectedApproval.state !== 'approved' || inspectedApproval.decidedBy?.id !== reviewer.subject || inspectedApproval.dispatchId !== null || inspectedApproval.consumedAtMs !== null ) { fail( 'management results do not prove the reviewed separation-of-duty flow', ); } const report = Object.freeze({ schemaVersion: 1, fixture: FIXTURE, observedAt: new Date(now()).toISOString(), identity: Object.freeze({ providerKind: 'external_oidc', issuer: requester.issuer, discoveryDocumentSha256: oidc.discoveryDocumentSha256, jwksSha256: oidc.jwksSha256, audience: AUDIENCE, type: TYPE, purpose: PURPOSE, requesterSubjectSha256: sha256( 'qinglong3.worker-management.subject.v1', requester.subject, ), reviewerSubjectSha256: sha256( 'qinglong3.worker-management.subject.v1', reviewer.subject, ), requesterKeyIdSha256: sha256( 'qinglong3.worker-management.kid.v1', requester.kid, ), reviewerKeyIdSha256: sha256( 'qinglong3.worker-management.kid.v1', reviewer.kid, ), }), ceremony: Object.freeze({ actionRefSha256: sha256( 'qinglong3.worker-management.action-ref.v1', planValue.actionRef, ), authorityProjectIdSha256: sha256( 'qinglong3.worker-management.project.v1', planValue.authorityProjectId, ), planStatus: plan.result.status, approvalStatus: proposed.result.approvalStatus, requesterSelfDecisionStatus: 403, requesterSelfDecisionCode: 'forbidden', reviewerDecisionStatus: decided.result.status, approvalState: inspectedApproval.state, inspectionStale: inspected.result.stale, dispatchCreated: inspectedApproval.dispatchId !== null, approvalConsumed: inspectedApproval.consumedAtMs !== null, planDigest: planValue.planDigest, previewDigest: planValue.previewDigest, requestIdSha256: Object.freeze( [ plan.requestId, proposed.requestId, selfDecision.requestId, decided.requestId, inspected.requestId, ].map((value) => sha256('qinglong3.worker-management.request-id.v1', value), ), ), }), gates: Object.freeze({ externalIdentity: true, workerPurposeBound: true, requesterAndReviewerDistinct: true, requesterSelfDecisionRejected: true, reviewerDecisionAccepted: true, inspectionAuthorized: true, noExecutionOrConsumption: true, passed: true, }), }); const audit = validateWorkerCredentialManagementLiveCeremony(report); if (!audit.compatible) { fail( `assembled report failed audit: ${audit.findings .map(({ code }) => code) .join(',')}`, ); } writeNoReplace(options.outputFile, report); requesterRaw = undefined; reviewerRaw = undefined; return report; } 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 keys = [ 'config', 'requester-assertion', 'reviewer-assertion', 'ceremony', 'output', ]; if ( JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(keys.sort()) ) { fail('arguments are invalid'); } return Object.freeze({ configFile: values.config, requesterAssertionFile: values['requester-assertion'], reviewerAssertionFile: values['reviewer-assertion'], ceremonyFile: values.ceremony, outputFile: values.output, }); } async function runCli(argv) { if (process.env.QL3_WORKER_CREDENTIAL_MANAGEMENT_LIVE_CEREMONY !== '1') { fail('explicit live ceremony opt-in is required'); } await runWorkerCredentialManagementLiveCeremony(parseArguments(argv)); process.stdout.write( `${JSON.stringify({ schemaVersion: 1, fixture: FIXTURE, compatible: true, })}\n`, ); } if (require.main === module) { runCli(process.argv.slice(2)).catch((error) => { process.stderr.write( `${ error instanceof Error ? error.message : 'Worker credential management live ceremony failed' }\n`, ); process.exitCode = 1; }); } module.exports = { FIXTURE, WorkerCredentialManagementLiveCeremonyError, assertionIdentity, ceremonyCommands, parseArguments, runWorkerCredentialManagementLiveCeremony, validateWorkerCredentialManagementLiveCeremony, };