#!/usr/bin/env node import { createPrivateKey, createPublicKey, sign, verify } from "node:crypto"; import { readFile, realpath, stat as fileStat } from "node:fs/promises"; import { isAbsolute, relative, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { canonicalJsonDigest, canonicalValueBytes, isSha256Hex, parseCanonicalJson, } from "../../src/security/canonical-digest.ts"; import { validateReleaseManifest } from "../../src/security/release-manifest.ts"; import { APPROVAL_PROJECTION_ATTESTATION_SCHEMA, APPROVAL_PROJECTION_ATTESTATION_SCOPE, verifyApprovalProjectionAttestation, } from "../../src/security/approval-closure.ts"; const rootContract = { approval: { scope: "portfolio-approval-decision/v1", recovery: ["freeze-publication", "audit-checkpoint", "fresh-approvals"], }, projection: { scope: APPROVAL_PROJECTION_ATTESTATION_SCOPE, recovery: ["freeze-publication", "revoke-epoch", "fresh-evidence"], }, privacyWorkload: { scope: "portfolio-privacy-workload/v1", recovery: ["freeze-publication", "revoke-epoch", "fresh-evidence"], }, policy: { scope: "portfolio-policy-attestation/v1", recovery: ["freeze-publication", "revoke-epoch", "fresh-evidence"], }, toolImage: { scope: "portfolio-tool-image-provenance/v1", recovery: ["freeze-publication", "revoke-epoch", "fresh-evidence"], }, release: { scope: "portfolio-release-signing/v1", recovery: ["freeze-publication", "revoke-epoch", "fresh-evidence"], }, publisher: { scope: "portfolio-publisher-fencing/v1", recovery: [ "freeze-publication", "rotate-credential", "advance-fence", "reconcile-staging", ], }, }; const rootFields = [ "keyId", "issuerEpoch", "scope", "publicKeyPem", "revokedEpochs", "compromiseResponse", ]; const policyFields = ["schema", "policyVersion", "publicationFrozen", "roots"]; const candidateFields = [ "schema", "releaseManifest", "latestLedgerHeadHash", "latestLedgerRevision", "sourceClosureDigest", "publicObjectClosureDigest", "publicRefMap", "lfsOids", "releaseArchives", "buildProvenanceDigest", "privacyPolicyDigest", "toolImageDigests", ]; const signatureFields = [ "algorithm", "encoding", "keyId", "issuerEpoch", "scope", "value", ]; const attestationPayloadFields = { privacyWorkload: [ "schema", "keyId", "issuerEpoch", "scope", "issuedAt", "expiresAt", "digest", "candidateDigest", "evidenceDigest", ], policy: [ "schema", "keyId", "issuerEpoch", "scope", "issuedAt", "expiresAt", "digest", "policyVersion", ], toolImage: [ "schema", "keyId", "issuerEpoch", "scope", "issuedAt", "expiresAt", "digest", "imageDigests", ], }; const signedFields = ["schema", "payload", "payloadDigest", "signature"]; const timestamp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u; const safeToken = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u; function object(value, label) { if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`); return value; } function exact(value, fields, label) { if ( JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...fields].sort()) ) throw new Error(`${label} contains missing or unknown fields.`); } function digest(value, label) { if (!isSha256Hex(value)) throw new Error(`${label} is not a canonical SHA-256 digest.`); } function compare(left, right) { return Buffer.from(left).compare(Buffer.from(right)); } export function validateTrustPolicy(value) { const policy = object(value, "trust policy"); exact(policy, policyFields, "trust policy"); if ( policy.schema !== "portfolio-trust-policy/v1" || typeof policy.policyVersion !== "string" || !safeToken.test(policy.policyVersion) || typeof policy.publicationFrozen !== "boolean" ) throw new Error("Trust policy metadata is invalid."); const roots = object(policy.roots, "trust roots"); exact(roots, Object.keys(rootContract), "trust roots"); const seenKeys = new Set(); const seenPublicKeys = new Set(); for (const [role, contract] of Object.entries(rootContract)) { const root = object(roots[role], `${role} root`); exact(root, rootFields, `${role} root`); if ( typeof root.keyId !== "string" || !safeToken.test(root.keyId) || seenKeys.has(root.keyId) || !Number.isSafeInteger(root.issuerEpoch) || root.issuerEpoch < 1 ) throw new Error(`${role} key ID or issuer epoch is invalid.`); seenKeys.add(root.keyId); if (root.scope !== contract.scope) throw new Error(`${role} trust-root scope is invalid.`); if (typeof root.publicKeyPem !== "string") throw new Error(`${role} public key is invalid.`); const key = createPublicKey(root.publicKeyPem); if (key.asymmetricKeyType !== "ed25519") throw new Error(`${role} key must be Ed25519.`); const publicFingerprint = key .export({ type: "spki", format: "der" }) .toString("base64"); if (seenPublicKeys.has(publicFingerprint)) throw new Error("Trust roots must use distinct protected keys."); seenPublicKeys.add(publicFingerprint); if ( !Array.isArray(root.revokedEpochs) || root.revokedEpochs.some( (epoch) => !Number.isSafeInteger(epoch) || epoch < 1, ) || new Set(root.revokedEpochs).size !== root.revokedEpochs.length ) throw new Error(`${role} revocation list is invalid.`); if (root.revokedEpochs.includes(root.issuerEpoch)) throw new Error(`${role} current issuer epoch is revoked.`); if ( !Array.isArray(root.compromiseResponse) || contract.recovery.some( (action) => !root.compromiseResponse.includes(action), ) ) { const detail = role === "approval" ? "checkpoint audit and fresh approvals" : role === "publisher" ? "fence advance and staging reconciliation" : "fresh evidence"; throw new Error(`${role} compromise response lacks ${detail}.`); } } return policy; } function verifyAttestation(value, root, expectedSchema, role, now) { const envelope = object(value, "attestation"); exact(envelope, ["payload", "signature"], "attestation"); const payload = object(envelope.payload, "attestation payload"); exact(payload, attestationPayloadFields[role], "attestation payload"); const signature = object(envelope.signature, "attestation signature"); exact(signature, ["algorithm", "encoding", "value"], "attestation signature"); if ( payload.schema !== expectedSchema || payload.keyId !== root.keyId || payload.issuerEpoch !== root.issuerEpoch || payload.scope !== root.scope ) throw new Error( "Attestation schema, scope, key, or issuer epoch is not trusted.", ); if ( typeof payload.issuedAt !== "string" || typeof payload.expiresAt !== "string" || !timestamp.test(payload.issuedAt) || !timestamp.test(payload.expiresAt) || Date.parse(payload.issuedAt) > Date.parse(now) || Date.parse(now) >= Date.parse(payload.expiresAt) ) throw new Error("Attestation is not current at trusted time."); for (const field of [ "digest", "productionInputRoot", "approvedRecordRoot", "approvedAssetRoot", "approvalProofSetDigest", "latestLedgerHeadHash", "candidateDigest", "evidenceDigest", ]) if (field in payload) digest(payload[field], `attestation ${field}`); if ( "latestLedgerRevision" in payload && (!Number.isSafeInteger(payload.latestLedgerRevision) || payload.latestLedgerRevision < 0) ) throw new Error("Attestation ledger revision is invalid."); if ( "imageDigests" in payload && (!Array.isArray(payload.imageDigests) || payload.imageDigests.length < 2 || payload.imageDigests.some((item) => !isSha256Hex(item))) ) throw new Error("Attested image digests are invalid."); if ( signature.algorithm !== "Ed25519" || signature.encoding !== "base64url-nopad" || typeof signature.value !== "string" || !/^[A-Za-z0-9_-]{86}$/u.test(signature.value) ) throw new Error("Attestation signature metadata is invalid."); const bytes = Buffer.from(signature.value, "base64url"); if ( bytes.toString("base64url") !== signature.value || !verify( null, canonicalValueBytes(payload), createPublicKey(root.publicKeyPem), bytes, ) ) throw new Error("Attestation signature is invalid."); return payload; } function validateCandidate(value, now) { const candidate = object(value, "candidate"); exact(candidate, candidateFields, "candidate"); if (candidate.schema !== "portfolio-candidate-release-manifest/v1") throw new Error("Candidate schema is invalid."); digest(candidate.latestLedgerHeadHash, "latestLedgerHeadHash"); digest(candidate.sourceClosureDigest, "sourceClosureDigest"); digest(candidate.publicObjectClosureDigest, "publicObjectClosureDigest"); digest(candidate.buildProvenanceDigest, "buildProvenanceDigest"); digest(candidate.privacyPolicyDigest, "privacyPolicyDigest"); if ( !Number.isSafeInteger(candidate.latestLedgerRevision) || candidate.latestLedgerRevision < 0 ) throw new Error("latestLedgerRevision is invalid."); const refs = object(candidate.publicRefMap, "publicRefMap"); for (const [name, hash] of Object.entries(refs)) if (!safeToken.test(name) || !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(hash)) throw new Error("Public ref map is invalid."); if ( !Array.isArray(candidate.lfsOids) || candidate.lfsOids.some((oid) => !/^sha256:[a-f0-9]{64}$/u.test(oid)) || JSON.stringify(candidate.lfsOids) !== JSON.stringify([...candidate.lfsOids].sort(compare)) || new Set(candidate.lfsOids).size !== candidate.lfsOids.length ) throw new Error("LFS closure is invalid."); if (!Array.isArray(candidate.releaseArchives)) throw new Error("Release archive closure is invalid."); for (const archive of candidate.releaseArchives) { const record = object(archive, "release archive"); exact(record, ["path", "sha256"], "release archive"); if ( typeof record.path !== "string" || record.path.startsWith("/") || record.path.includes("..") ) throw new Error("Release archive path is invalid."); digest(record.sha256, "release archive digest"); } const archiveKeys = candidate.releaseArchives.map( (archive) => `${archive.path}\0${archive.sha256}`, ); if ( new Set(archiveKeys).size !== archiveKeys.length || JSON.stringify(archiveKeys) !== JSON.stringify([...archiveKeys].sort(compare)) ) throw new Error("Release archive closure must be unique and byte sorted."); if ( !Array.isArray(candidate.toolImageDigests) || candidate.toolImageDigests.length < 2 || candidate.toolImageDigests.some((value) => !isSha256Hex(value)) || new Set(candidate.toolImageDigests).size !== candidate.toolImageDigests.length || JSON.stringify(candidate.toolImageDigests) !== JSON.stringify([...candidate.toolImageDigests].sort(compare)) ) throw new Error("Tool image closure must contain unique, sorted digests."); const releaseManifest = validateReleaseManifest(candidate.releaseManifest, { now, }); return { candidate, releaseManifest }; } function validateEvidence(value, expectedDigest) { const evidence = object(value, "scan evidence"); exact( evidence, [ "schema", "sourceClosureDigest", "publicObjectClosureDigest", "coverageCount", "passedCount", "checks", ], "scan evidence", ); if ( evidence.schema !== "portfolio-scan-evidence/v1" || !Number.isSafeInteger(evidence.coverageCount) || evidence.coverageCount < 0 || evidence.passedCount !== evidence.coverageCount || !Array.isArray(evidence.checks) || evidence.checks.length !== evidence.coverageCount ) throw new Error("Scan evidence coverage is incomplete."); digest(evidence.sourceClosureDigest, "scan source closure digest"); digest( evidence.publicObjectClosureDigest, "scan public object closure digest", ); const keys = []; for (const raw of evidence.checks) { const check = object(raw, "scan evidence check"); exact( check, [ "inputDigest", "checkId", "normalizedOutputDigest", "normalizedMembers", "workerReports", "matcherResult", "ruleSetDigest", "appliedRuleIds", ], "scan evidence check", ); digest(check.inputDigest, "scan input digest"); digest(check.normalizedOutputDigest, "normalized output digest"); digest(check.matcherResult, "matcher result digest"); digest(check.ruleSetDigest, "rule set digest"); if ( !safeToken.test(check.checkId) || !Array.isArray(check.normalizedMembers) || check.normalizedMembers.some( (item) => !/^[a-z][a-z0-9-]*:[a-f0-9]{64}$/u.test(item), ) || !Array.isArray(check.workerReports) || check.workerReports.length !== 2 || check.workerReports.some((item) => !isSha256Hex(item)) ) throw new Error("Scan evidence check is invalid."); if ( !Array.isArray(check.appliedRuleIds) || check.appliedRuleIds.length === 0 || check.appliedRuleIds.some((item) => !safeToken.test(item)) || new Set(check.appliedRuleIds).size !== check.appliedRuleIds.length ) throw new Error("Applied matcher rule IDs are invalid."); keys.push(`${check.inputDigest}\0${check.checkId}`); } if ( new Set(keys).size !== keys.length || JSON.stringify(keys) !== JSON.stringify([...keys].sort(compare)) ) throw new Error("Scan evidence checks must be unique and byte sorted."); if ( canonicalJsonDigest("portfolio-scan-evidence/v1", evidence) !== expectedDigest ) throw new Error("Scan evidence bytes were substituted."); return evidence; } function validateProtectedState(state) { const value = object(state, "protected state"); exact( value, [ "latestLedgerHeadHash", "latestLedgerRevision", "currentManifestDigest", "publisherFence", ], "protected state", ); digest(value.latestLedgerHeadHash, "protected ledger head"); if ( !Number.isSafeInteger(value.latestLedgerRevision) || value.latestLedgerRevision < 0 || !Number.isSafeInteger(value.publisherFence) || value.publisherFence < 0 || (value.currentManifestDigest !== null && !isSha256Hex(value.currentManifestDigest)) ) throw new Error("Protected state is invalid."); return value; } function verifyCandidateAuthority({ candidate, evidence, attestations, trustPolicy, now, protectedState, sourceClosure, publicObjectClosure, }) { if (trustPolicy.publicationFrozen) throw new Error("Publication is frozen by trust policy."); if (typeof now !== "string" || !timestamp.test(now)) throw new Error("Trusted current time is invalid."); const { candidate: record, releaseManifest } = validateCandidate( candidate, now, ); const state = validateProtectedState(protectedState); const sourceRecord = object(sourceClosure, "source closure"); exact(sourceRecord, ["schema", "members"], "source closure"); if ( sourceRecord.schema !== "portfolio-publication-closure/v1" || !Array.isArray(sourceRecord.members) ) throw new Error("Source closure is invalid."); const publicRecord = object(publicObjectClosure, "public object closure"); exact(publicRecord, ["schema", "members"], "public object closure"); if ( publicRecord.schema !== "portfolio-public-object-closure/v1" || !Array.isArray(publicRecord.members) ) throw new Error("Public object closure is invalid."); const sourceClosureDigest = canonicalJsonDigest( "portfolio-publication-closure/v1", sourceRecord, ); const publicObjectClosureDigest = canonicalJsonDigest( "portfolio-public-object-closure/v1", publicRecord, ); if ( record.sourceClosureDigest !== sourceClosureDigest || record.publicObjectClosureDigest !== publicObjectClosureDigest ) throw new Error( "Candidate does not bind exact source/public closure bytes.", ); if (publicRecord.members.length !== 0) throw new Error( "Verified scanners are unavailable; non-empty public object closure cannot be signed.", ); if ( record.latestLedgerHeadHash !== state.latestLedgerHeadHash || record.latestLedgerRevision !== state.latestLedgerRevision ) throw new Error("Candidate is stale relative to the protected ledger."); if ( releaseManifest.currentManifestDigest !== state.currentManifestDigest || releaseManifest.predecessorManifestDigest !== state.currentManifestDigest ) throw new Error( "Candidate predecessor does not equal protected current manifest.", ); const validatedEvidence = validateEvidence( evidence, releaseManifest.scanEvidenceDigest, ); if ( validatedEvidence.sourceClosureDigest !== sourceClosureDigest || validatedEvidence.publicObjectClosureDigest !== publicObjectClosureDigest || validatedEvidence.coverageCount !== 0 || validatedEvidence.checks.length !== 0 ) throw new Error( "Scan evidence does not exactly cover the canonical empty public object closure.", ); const candidateDigest = canonicalJsonDigest( "portfolio-candidate-release-manifest/v1", record, ); const evidenceDigest = canonicalJsonDigest( "portfolio-scan-evidence/v1", evidence, ); const projectionRoot = trustPolicy.roots.projection; const projection = verifyApprovalProjectionAttestation( attestations.projection, { now, latestLedgerHeadHash: record.latestLedgerHeadHash, latestLedgerRevision: record.latestLedgerRevision, trustedKey: { keyId: projectionRoot.keyId, issuerEpoch: projectionRoot.issuerEpoch, scope: APPROVAL_PROJECTION_ATTESTATION_SCOPE, publicKey: createPublicKey(projectionRoot.publicKeyPem), }, }, ); const workload = verifyAttestation( attestations.privacyWorkload, trustPolicy.roots.privacyWorkload, "portfolio-privacy-workload-attestation/v1", "privacyWorkload", now, ); const policy = verifyAttestation( attestations.policy, trustPolicy.roots.policy, "portfolio-policy-attestation/v1", "policy", now, ); const tools = verifyAttestation( attestations.toolImage, trustPolicy.roots.toolImage, "portfolio-tool-image-provenance/v1", "toolImage", now, ); for (const [field, expected] of [ ["productionInputRoot", releaseManifest.productionInputRoot], ["approvedRecordRoot", releaseManifest.approvedRecordRoot], ["approvedAssetRoot", releaseManifest.approvedAssetRoot], ["approvalProofSetDigest", releaseManifest.approvalProofSetDigest], ["liveLedgerHeadHash", record.latestLedgerHeadHash], ["liveLedgerRevision", record.latestLedgerRevision], ]) { if (projection[field] !== expected) throw new Error(`Approval projection mismatch: ${field}.`); } if ( canonicalJsonDigest( "portfolio-approval-projection-attestation-envelope/v1", attestations.projection, ) !== releaseManifest.approvalProjectionAttestationDigest ) throw new Error("Projection attestation digest binding is invalid."); if ( workload.candidateDigest !== candidateDigest || workload.evidenceDigest !== evidenceDigest ) throw new Error( "Privacy workload does not bind exact candidate and evidence bytes.", ); if ( policy.digest !== record.privacyPolicyDigest || policy.policyVersion !== trustPolicy.policyVersion ) throw new Error("Privacy policy attestation is stale or substituted."); if ( !Array.isArray(tools.imageDigests) || JSON.stringify(tools.imageDigests) !== JSON.stringify(record.toolImageDigests) ) throw new Error("Tool-image provenance does not bind exact images."); return { record, releaseManifest, state, candidateDigest, evidenceDigest }; } async function externalPrivateKey(path) { if (typeof path !== "string" || !isAbsolute(path)) throw new Error("Release key path must be an external absolute path."); const workspace = await realpath(process.cwd()); const lexicalRelative = relative(workspace, resolve(path)); if ( lexicalRelative === "" || (!lexicalRelative.startsWith("..") && !isAbsolute(lexicalRelative)) ) throw new Error( "Release signing key must live outside the repository/workspace.", ); const canonicalPath = await realpath(path); const rel = relative(workspace, canonicalPath); if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel))) throw new Error( "Release signing key must live outside the repository/workspace.", ); const info = await fileStat(canonicalPath); if (!info.isFile() || (info.mode & 0o077) !== 0) throw new Error("External release key must be a private owner-only file."); const key = createPrivateKey(await readFile(canonicalPath)); if (key.asymmetricKeyType !== "ed25519") throw new Error("Release key must be Ed25519."); return key; } async function signReleaseCandidateAt(input, now) { const trustPolicy = validateTrustPolicy(input.trustPolicy); const verified = verifyCandidateAuthority({ ...input, trustPolicy, now }); const key = await externalPrivateKey(input.releaseKeyPath); const releaseRoot = trustPolicy.roots.release; const bytes = canonicalValueBytes(verified.record); const signatureBytes = sign(null, bytes, key); if ( !verify( null, bytes, createPublicKey(releaseRoot.publicKeyPem), signatureBytes, ) ) throw new Error( "External key does not match the active release trust root.", ); return { schema: "portfolio-signed-release-manifest/v1", payload: verified.record, payloadDigest: verified.candidateDigest, signature: { algorithm: "Ed25519", encoding: "base64url-nopad", keyId: releaseRoot.keyId, issuerEpoch: releaseRoot.issuerEpoch, scope: releaseRoot.scope, value: signatureBytes.toString("base64url"), }, }; } export async function signReleaseCandidate(input) { if (Object.prototype.hasOwnProperty.call(input, "now")) throw new Error("Production signer does not accept caller-selected now."); return signReleaseCandidateAt(input, new Date().toISOString()); } export async function signReleaseCandidateForTest(input) { if (typeof input?.now !== "string") throw new Error("Test-only signer requires an explicit clock."); const { now, ...rest } = input; return signReleaseCandidateAt(rest, now); } function verifySignedReleaseEnvelopeAt({ signedRelease, trustPolicy: trustValue, now, protectedState, }) { const trustPolicy = validateTrustPolicy(trustValue); const envelope = object(signedRelease, "signed release"); exact(envelope, signedFields, "signed release"); if (envelope.schema !== "portfolio-signed-release-manifest/v1") throw new Error("Signed release schema is invalid."); const signature = object(envelope.signature, "release signature"); exact(signature, signatureFields, "release signature"); const root = trustPolicy.roots.release; if ( signature.algorithm !== "Ed25519" || signature.encoding !== "base64url-nopad" || signature.keyId !== root.keyId || signature.issuerEpoch !== root.issuerEpoch || signature.scope !== root.scope || typeof signature.value !== "string" ) throw new Error( "Release signature key, epoch, scope, or encoding is invalid.", ); const { candidate, releaseManifest } = validateCandidate( envelope.payload, now, ); const payloadDigest = canonicalJsonDigest( "portfolio-candidate-release-manifest/v1", candidate, ); if (envelope.payloadDigest !== payloadDigest) throw new Error("Signed release payload digest is invalid."); const bytes = Buffer.from(signature.value, "base64url"); if ( bytes.toString("base64url") !== signature.value || !verify( null, canonicalValueBytes(candidate), createPublicKey(root.publicKeyPem), bytes, ) ) throw new Error("Release signature is invalid."); const state = validateProtectedState(protectedState); if ( candidate.latestLedgerHeadHash !== state.latestLedgerHeadHash || candidate.latestLedgerRevision !== state.latestLedgerRevision || releaseManifest.currentManifestDigest !== state.currentManifestDigest || releaseManifest.predecessorManifestDigest !== state.currentManifestDigest ) throw new Error("Signed release is stale relative to protected state."); return { schema: "portfolio-verified-release-admission/v1", manifestDigest: payloadDigest, manifestPayload: releaseManifest, expiresAt: releaseManifest.expiresAt, predecessorManifestDigest: releaseManifest.predecessorManifestDigest, latestLedgerHeadHash: candidate.latestLedgerHeadHash, latestLedgerRevision: candidate.latestLedgerRevision, expectedFence: state.publisherFence + 1, signatureVerified: true, }; } export function verifySignedReleaseEnvelope(input) { if (Object.prototype.hasOwnProperty.call(input, "now")) throw new Error("Production verifier does not accept caller-selected now."); return verifySignedReleaseEnvelopeAt({ ...input, now: new Date().toISOString(), }); } export function verifySignedReleaseEnvelopeForTest(input) { if (typeof input?.now !== "string") throw new Error("Test-only verifier requires an explicit clock."); return verifySignedReleaseEnvelopeAt(input); } async function main() { const args = process.argv.slice(2); if (args.length !== 4 || args[0] !== "--request" || args[2] !== "--key") throw new Error( "Usage: sign-release.mjs --request --key ", ); const request = parseCanonicalJson(await readFile(resolve(args[1]))); const signed = await signReleaseCandidate({ ...object(request, "sign request"), releaseKeyPath: resolve(args[3]), }); process.stdout.write(Buffer.from(canonicalValueBytes(signed))); process.stdout.write("\n"); } if ( process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href ) { main().catch((error) => { process.stderr.write( `Release signing failed: ${error instanceof Error ? error.message : "unknown error"}\n`, ); process.exitCode = 1; }); }