import type { KeyLike } from "node:crypto"; import { canonicalJsonDigest, assertWellFormedNfcString, isSha256Hex, parseCanonicalJson, verifyEd25519Envelope, } from "./canonical-digest.ts"; export interface ApprovalPayload { schema: "portfolio-approval-payload/v1"; keyId: string; issuerEpoch: number; authorizedRole: string; evidenceFingerprints: string[]; recordClass: "content" | "asset"; recordType: string; publicRecordIdOrAssetId: string; approvedPublicRecordHashOrManifestRecordHash: string; sanitizedOutputHashOrNull: string | null; decision: "approve" | "reject" | "revoke"; issuedAt: string; expiresAt: string; previousLedgerHeadHash: string; ledgerRevision: number; policyVersion: string; } export interface ApprovalEnvelope { schema: "portfolio-approval-envelope/v1"; payload: ApprovalPayload; signature: { algorithm: "Ed25519"; encoding: "base64url-nopad"; value: string; }; } export interface ApprovalTrustKey { keyId: string; issuerEpoch: number; scope: "portfolio-approval-decision/v1"; authorizedRoles: string[]; publicKey: KeyLike; revoked?: boolean; } export interface ApprovalTrustPolicy { now: string; latestLedgerHeadHash: string; latestLedgerRevision: number; trustedKeys: ApprovalTrustKey[]; ledgerProof: ApprovalLedgerProof; } export interface ApprovalLedgerProof { schema: "portfolio-approval-ledger-proof/v1"; genesisHeadHash: string; entries: ApprovalEnvelope[]; } const payloadKeys = [ "schema", "keyId", "issuerEpoch", "authorizedRole", "evidenceFingerprints", "recordClass", "recordType", "publicRecordIdOrAssetId", "approvedPublicRecordHashOrManifestRecordHash", "sanitizedOutputHashOrNull", "decision", "issuedAt", "expiresAt", "previousLedgerHeadHash", "ledgerRevision", "policyVersion", ] as const; const envelopeKeys = ["schema", "payload", "signature"] as const; const signatureKeys = ["algorithm", "encoding", "value"] as const; const utcTimestamp = /^\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 record(value: unknown, label: string): Record { if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`); return value as Record; } function exactKeys( value: Record, keys: readonly string[], label: string, ): void { const actual = Object.keys(value).sort(); const expected = [...keys].sort(); if ( actual.length !== expected.length || actual.some((key, index) => key !== expected[index]) ) { throw new Error(`${label} contains missing or unknown fields.`); } } function validTimestamp( value: unknown, label: string, ): asserts value is string { if ( typeof value !== "string" || !utcTimestamp.test(value) || Number.isNaN(Date.parse(value)) ) { throw new Error(`${label} must be a UTC RFC 3339 timestamp.`); } } export function validateApprovalEnvelope(value: unknown): ApprovalEnvelope { const outer = record(value, "approval envelope"); exactKeys(outer, envelopeKeys, "approval envelope"); if (outer.schema !== "portfolio-approval-envelope/v1") throw new Error("Approval envelope schema is invalid."); const payload = record(outer.payload, "approval payload"); exactKeys(payload, payloadKeys, "approval payload"); const signature = record(outer.signature, "approval signature"); exactKeys(signature, signatureKeys, "approval signature"); if (payload.schema !== "portfolio-approval-payload/v1") throw new Error("Approval payload schema is invalid."); for (const key of [ "keyId", "authorizedRole", "publicRecordIdOrAssetId", "policyVersion", ] as const) { const field = payload[key]; if (typeof field !== "string" || !safeToken.test(field)) throw new Error(`${key} is invalid.`); assertWellFormedNfcString(field, key); } if ( !Number.isSafeInteger(payload.issuerEpoch) || (payload.issuerEpoch as number) < 1 ) throw new Error("issuerEpoch is invalid."); if ( !Number.isSafeInteger(payload.ledgerRevision) || (payload.ledgerRevision as number) < 0 ) throw new Error("ledgerRevision is invalid."); if (payload.recordClass !== "content" && payload.recordClass !== "asset") throw new Error("recordClass is invalid."); if (typeof payload.recordType !== "string") throw new Error("recordType is invalid."); assertWellFormedNfcString(payload.recordType, "recordType"); if (payload.recordClass === "content" && !safeToken.test(payload.recordType)) throw new Error("Content recordType is required."); if (payload.recordClass === "asset" && payload.recordType !== "") throw new Error("Asset recordType must be empty."); if ( !isSha256Hex(payload.approvedPublicRecordHashOrManifestRecordHash) || !isSha256Hex(payload.previousLedgerHeadHash) ) { throw new Error("Approval hash field is invalid."); } if ( !Array.isArray(payload.evidenceFingerprints) || payload.evidenceFingerprints.length === 0 || payload.evidenceFingerprints.some((item) => !isSha256Hex(item)) || new Set(payload.evidenceFingerprints).size !== payload.evidenceFingerprints.length ) { throw new Error( "evidenceFingerprints must be a unique, non-empty digest list.", ); } if ( payload.recordClass === "content" && payload.sanitizedOutputHashOrNull !== null ) throw new Error("Content approval cannot bind sanitized bytes."); if ( payload.recordClass === "asset" && !isSha256Hex(payload.sanitizedOutputHashOrNull) ) throw new Error("Asset approval must bind sanitized bytes."); if (!["approve", "reject", "revoke"].includes(payload.decision as string)) throw new Error("Decision is invalid."); validTimestamp(payload.issuedAt, "issuedAt"); validTimestamp(payload.expiresAt, "expiresAt"); if (Date.parse(payload.issuedAt) >= Date.parse(payload.expiresAt)) throw new Error("Approval expiry must follow issue time."); if ( signature.algorithm !== "Ed25519" || signature.encoding !== "base64url-nopad" || typeof signature.value !== "string" ) { throw new Error("Approval signature metadata is invalid."); } return outer as unknown as ApprovalEnvelope; } export function parseApprovalEnvelopeJson( source: string | Uint8Array, ): ApprovalEnvelope { return validateApprovalEnvelope(parseCanonicalJson(source)); } function verifyEnvelopeSignature( envelopeValue: unknown, trust: ApprovalTrustPolicy, ): ApprovalEnvelope { const envelope = validateApprovalEnvelope(envelopeValue); const matchingKeys = trust.trustedKeys.filter( (candidate) => candidate.keyId === envelope.payload.keyId && candidate.issuerEpoch === envelope.payload.issuerEpoch, ); if (matchingKeys.length !== 1) throw new Error( "Approval trust policy must contain exactly one matching key epoch.", ); const key = matchingKeys[0]; if ( key === undefined || key.revoked || key.scope !== "portfolio-approval-decision/v1" ) throw new Error("Approval key, scope, or epoch is not trusted."); if (!key.authorizedRoles.includes(envelope.payload.authorizedRole)) throw new Error("Approval role is not authorized."); verifyEd25519Envelope(envelope, key); return envelope; } function typedDecisionId(payload: ApprovalPayload): string { return `${payload.recordClass}\0${payload.recordType}\0${payload.publicRecordIdOrAssetId}`; } export function verifyApprovalLedger( trust: ApprovalTrustPolicy, ): Map { validTimestamp(trust.now, "trusted now"); if ( !isSha256Hex(trust.latestLedgerHeadHash) || !Number.isSafeInteger(trust.latestLedgerRevision) || trust.latestLedgerRevision < 0 ) { throw new Error("Protected live-ledger state is invalid."); } const proof = trust.ledgerProof; if ( proof === null || typeof proof !== "object" || Array.isArray(proof) || JSON.stringify(Object.keys(proof).sort()) !== JSON.stringify(["entries", "genesisHeadHash", "schema"]) || proof.schema !== "portfolio-approval-ledger-proof/v1" || !isSha256Hex(proof.genesisHeadHash) || !Array.isArray(proof.entries) || proof.entries.length !== trust.latestLedgerRevision ) throw new Error("Authenticated approval ledger proof is incomplete."); let head = proof.genesisHeadHash; const latest = new Map(); proof.entries.forEach((raw, index) => { const envelope = verifyEnvelopeSignature(raw, trust); const revision = index + 1; if ( envelope.payload.ledgerRevision !== revision || envelope.payload.previousLedgerHeadHash !== head || Date.parse(envelope.payload.issuedAt) > Date.parse(trust.now) ) throw new Error( "Approval ledger chain is not contiguous through the protected revision.", ); const envelopeDigest = canonicalJsonDigest( "portfolio-approval-envelope-digest/v1", envelope, ); head = canonicalJsonDigest("portfolio-approval-ledger-entry/v1", { previousLedgerHeadHash: head, ledgerRevision: revision, envelopeDigest, }); latest.set(typedDecisionId(envelope.payload), envelope); }); if (head !== trust.latestLedgerHeadHash) throw new Error( "Approval ledger proof does not authenticate the protected head.", ); return latest; } export function verifyApprovalEnvelope( envelopeValue: unknown, trust: ApprovalTrustPolicy, ): ApprovalPayload { const envelope = verifyEnvelopeSignature(envelopeValue, trust); const latest = verifyApprovalLedger(trust).get( typedDecisionId(envelope.payload), ); if ( latest === undefined || canonicalJsonDigest("portfolio-approval-envelope-digest/v1", latest) !== canonicalJsonDigest("portfolio-approval-envelope-digest/v1", envelope) ) { throw new Error( "Approval is not the latest authenticated decision for this record.", ); } if ( Date.parse(trust.now) < Date.parse(envelope.payload.issuedAt) || Date.parse(trust.now) >= Date.parse(envelope.payload.expiresAt) ) { throw new Error("Approval is not current at the trusted time."); } if (envelope.payload.decision !== "approve") throw new Error("The latest decision is reject/revoke, not approval."); return envelope.payload; }