import type { KeyLike } from "node:crypto"; import { canonicalJsonDigest, isSha256Hex, verifyEd25519Envelope, type Sha256Hex, } from "./canonical-digest.ts"; import { type ApprovalEnvelope, type ApprovalTrustPolicy, verifyApprovalLedger, verifyApprovalEnvelope, } from "./approval-envelope.ts"; import { computeProductionInputRoot } from "./production-input-root.ts"; export const APPROVAL_PROJECTION_ATTESTATION_SCHEMA = "portfolio-approval-projection-attestation/v1" as const; export const APPROVAL_PROJECTION_ATTESTATION_SCOPE = APPROVAL_PROJECTION_ATTESTATION_SCHEMA; export interface ContentApprovalRecord { recordType: string; publicRecordId: string; approvedPublicRecordHash: string; envelope: ApprovalEnvelope; } export interface AssetApprovalRecord { assetId: string; approvedManifestRecordHash: string; sanitizedOutputHash: string; envelope: ApprovalEnvelope; } export interface ApprovalClosure { approvedRecordRoot: Sha256Hex; approvedAssetRoot: Sha256Hex; approvalProofSetDigest: Sha256Hex; earliestApprovalExpiry: string | null; } export interface ApprovalProjectionAttestationPayload { schema: typeof APPROVAL_PROJECTION_ATTESTATION_SCHEMA; keyId: string; issuerEpoch: number; scope: typeof APPROVAL_PROJECTION_ATTESTATION_SCOPE; approvedRecordRoot: string; approvedAssetRoot: string; approvalProofSetDigest: string; inputDigests: { PortfolioContent: string; PublicAssetManifest: string; PreparedAssetSet: string; SiteConfig: string; }; productionInputRoot: string; liveLedgerHeadHash: string; liveLedgerRevision: number; policyVersion: string; issuedAt: string; expiresAt: string; earliestApprovalExpiry: string; } export interface ApprovalProjectionAttestationEnvelope { schema: "portfolio-approval-projection-attestation-envelope/v1"; payload: ApprovalProjectionAttestationPayload; signature: { algorithm: "Ed25519"; encoding: "base64url-nopad"; value: string; }; } const encoder = new TextEncoder(); function utf8TupleCompare(left: string[], right: string[]): number { for (let field = 0; field < Math.max(left.length, right.length); field += 1) { const a = encoder.encode(left[field] ?? ""); const b = encoder.encode(right[field] ?? ""); for (let index = 0; index < Math.min(a.length, b.length); index += 1) { if (a[index] !== b[index]) return (a[index] ?? 0) - (b[index] ?? 0); } if (a.length !== b.length) return a.length - b.length; } return 0; } export function computeApprovalClosure(input: { contentRecords: ContentApprovalRecord[]; assetRecords: AssetApprovalRecord[]; trust: ApprovalTrustPolicy; }): ApprovalClosure { const latestLedger = verifyApprovalLedger(input.trust); const ids = new Set(); const expiries: string[] = []; const contentTuples = input.contentRecords .map((record) => { const typedId = record.publicRecordId; if (ids.has(typedId)) throw new Error("Duplicate content approval ID."); ids.add(typedId); if (!isSha256Hex(record.approvedPublicRecordHash)) throw new Error("Content record hash is invalid."); const payload = verifyApprovalEnvelope(record.envelope, input.trust); if ( payload.recordClass !== "content" || payload.recordType !== record.recordType || payload.publicRecordIdOrAssetId !== record.publicRecordId || payload.approvedPublicRecordHashOrManifestRecordHash !== record.approvedPublicRecordHash || payload.sanitizedOutputHashOrNull !== null ) { throw new Error( "Content approval does not bind the exact emitted record.", ); } expiries.push(payload.expiresAt); const envelopeDigest = canonicalJsonDigest( "portfolio-approval-envelope-digest/v1", record.envelope, ); return [ record.recordType, record.publicRecordId, record.approvedPublicRecordHash, envelopeDigest, ] as const; }) .sort((a, b) => utf8TupleCompare([a[0], a[1]], [b[0], b[1]])); const assetTuples = input.assetRecords .map((record) => { const typedId = record.assetId; if (ids.has(typedId)) throw new Error("Duplicate asset approval ID."); ids.add(typedId); if ( !isSha256Hex(record.approvedManifestRecordHash) || !isSha256Hex(record.sanitizedOutputHash) ) throw new Error("Asset record hash is invalid."); const payload = verifyApprovalEnvelope(record.envelope, input.trust); if ( payload.recordClass !== "asset" || payload.recordType !== "" || payload.publicRecordIdOrAssetId !== record.assetId || payload.approvedPublicRecordHashOrManifestRecordHash !== record.approvedManifestRecordHash || payload.sanitizedOutputHashOrNull !== record.sanitizedOutputHash ) { throw new Error( "Asset approval does not bind the exact emitted record and bytes.", ); } expiries.push(payload.expiresAt); const envelopeDigest = canonicalJsonDigest( "portfolio-approval-envelope-digest/v1", record.envelope, ); return [ record.assetId, record.approvedManifestRecordHash, record.sanitizedOutputHash, envelopeDigest, ] as const; }) .sort((a, b) => utf8TupleCompare([a[0]], [b[0]])); const proofList = [ ...contentTuples.map( (tuple) => ["content", tuple[0], tuple[1], tuple[3]] as const, ), ...assetTuples.map((tuple) => ["asset", "", tuple[0], tuple[3]] as const), ].sort((a, b) => utf8TupleCompare([a[0], a[1], a[2]], [b[0], b[1], b[2]])); const supplied = new Set([ ...input.contentRecords.map( (record) => `content\0${record.recordType}\0${record.publicRecordId}`, ), ...input.assetRecords.map((record) => `asset\0\0${record.assetId}`), ]); const required = new Set( [...latestLedger.entries()] .filter( ([, envelope]) => envelope.payload.decision === "approve" && Date.parse(input.trust.now) < Date.parse(envelope.payload.expiresAt), ) .map(([typedId]) => typedId), ); if ( supplied.size !== required.size || [...supplied].some((typedId) => !required.has(typedId)) ) { throw new Error( "Approval records are not the exact latest authenticated approved ledger projection.", ); } return { approvedRecordRoot: canonicalJsonDigest("portfolio-record-closure/v1", { records: contentTuples, }), approvedAssetRoot: canonicalJsonDigest("portfolio-asset-closure/v1", { assets: assetTuples, }), approvalProofSetDigest: canonicalJsonDigest( "portfolio-approval-proof-set/v1", { envelopes: proofList }, ), earliestApprovalExpiry: expiries.sort()[0] ?? null, }; } export function verifyApprovalProjectionAttestation( envelopeValue: unknown, trust: { now: string; latestLedgerHeadHash: string; latestLedgerRevision: number; trustedKey: { keyId: string; issuerEpoch: number; scope: typeof APPROVAL_PROJECTION_ATTESTATION_SCOPE; publicKey: KeyLike; revoked?: boolean; }; }, ): ApprovalProjectionAttestationPayload { if ( envelopeValue === null || typeof envelopeValue !== "object" || Array.isArray(envelopeValue) ) throw new Error("Projection envelope is invalid."); const envelope = envelopeValue as Record; if ( JSON.stringify(Object.keys(envelope).sort()) !== JSON.stringify(["payload", "schema", "signature"]) ) throw new Error("Projection envelope contains unknown fields."); if ( envelope.schema !== "portfolio-approval-projection-attestation-envelope/v1" ) throw new Error("Projection envelope schema is invalid."); if ( envelope.payload === null || typeof envelope.payload !== "object" || Array.isArray(envelope.payload) ) throw new Error("Projection payload is invalid."); const payload = envelope.payload as unknown as ApprovalProjectionAttestationPayload; const payloadFields = [ "schema", "keyId", "issuerEpoch", "scope", "approvedRecordRoot", "approvedAssetRoot", "approvalProofSetDigest", "inputDigests", "productionInputRoot", "liveLedgerHeadHash", "liveLedgerRevision", "policyVersion", "issuedAt", "expiresAt", "earliestApprovalExpiry", ].sort(); if ( JSON.stringify(Object.keys(payload).sort()) !== JSON.stringify(payloadFields) ) throw new Error("Projection payload contains missing or unknown fields."); if ( payload.schema !== APPROVAL_PROJECTION_ATTESTATION_SCHEMA || payload.scope !== APPROVAL_PROJECTION_ATTESTATION_SCOPE ) throw new Error("Projection payload schema/scope is invalid."); if ( trust.trustedKey.revoked || trust.trustedKey.scope !== payload.scope || trust.trustedKey.keyId !== payload.keyId || trust.trustedKey.issuerEpoch !== payload.issuerEpoch ) { throw new Error("Projection key, scope, or issuer epoch is not trusted."); } for (const digest of [ payload.approvedRecordRoot, payload.approvedAssetRoot, payload.approvalProofSetDigest, payload.productionInputRoot, payload.liveLedgerHeadHash, ]) { if (!isSha256Hex(digest)) throw new Error("Projection digest field is invalid."); } const inputKeys = Object.keys(payload.inputDigests).sort(); if ( JSON.stringify(inputKeys) !== JSON.stringify([ "PortfolioContent", "PreparedAssetSet", "PublicAssetManifest", "SiteConfig", ]) || Object.values(payload.inputDigests).some((digest) => !isSha256Hex(digest)) ) throw new Error("Projection input digest closure is invalid."); const expectedRoot = computeProductionInputRoot({ portfolioContentDigest: payload.inputDigests.PortfolioContent, publicAssetManifestDigest: payload.inputDigests.PublicAssetManifest, preparedAssetSetDigest: payload.inputDigests.PreparedAssetSet, siteConfigDigest: payload.inputDigests.SiteConfig, }); if (expectedRoot !== payload.productionInputRoot) throw new Error("Projection does not bind the exact four-input root."); if ( payload.liveLedgerHeadHash !== trust.latestLedgerHeadHash || payload.liveLedgerRevision !== trust.latestLedgerRevision ) throw new Error("Projection live-ledger state is stale."); for (const [label, value] of [ ["now", trust.now], ["issuedAt", payload.issuedAt], ["expiresAt", payload.expiresAt], ["earliestApprovalExpiry", payload.earliestApprovalExpiry], ] as const) { if ( !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u.test(value) || Number.isNaN(Date.parse(value)) ) throw new Error(`${label} timestamp is invalid.`); } if ( Date.parse(payload.issuedAt) > Date.parse(trust.now) || Date.parse(trust.now) >= Date.parse(payload.expiresAt) || Date.parse(payload.expiresAt) > Date.parse(payload.earliestApprovalExpiry) ) throw new Error("Projection is outside its trusted expiry bounds."); return verifyEd25519Envelope( envelope as unknown as ApprovalProjectionAttestationEnvelope, trust.trustedKey, ); }