import { createCipheriv, createDecipheriv, createHash, randomBytes, } from 'node:crypto'; import { normalizeToolExecutionResultArtifact, normalizeToolExecutionResultKeyBinding, type ToolExecutionResultArtifact, type ToolExecutionResultArtifactReference, type ToolExecutionResultKeyBinding, } from './toolExecutionCompletion'; import { normalizeToolResultKeyCatalogFence, toolResultKeyMaterialProof, type ToolResultKeyCatalogFence, } from './toolResultKeyCatalog'; import { MAX_TOOL_OUTPUT_BYTES, ToolDefinitionRegistry, type ToolJsonValue, } from './tool-registry/toolRegistry'; export const TOOL_EXECUTION_RESULT_REKEY_OVERLAY_SCHEMA = 'qinglong/tool-execution-result-rekey-overlay@v1' as const; export const TOOL_EXECUTION_RESULT_REKEY_COMMAND_SCHEMA = 'qinglong/tool-execution-result-rekey-command@v1' as const; export const TOOL_RESULT_KEY_RETIREMENT_RECEIPT_SCHEMA = 'qinglong/tool-result-key-retirement-receipt@v1' as const; export const TOOL_RESULT_KEY_RETIREMENT_RECEIPT_COMMAND_SCHEMA = 'qinglong/tool-result-key-retirement-receipt-command@v1' as const; export const TOOL_EXECUTION_RESULT_REKEY_ALGORITHM = 'aes-256-gcm' as const; export const MAX_TOOL_EXECUTION_RESULT_REKEY_OVERLAY_JSON_BYTES = 384 * 1024; export interface ToolExecutionResultRekeyOverlay { readonly schema: typeof TOOL_EXECUTION_RESULT_REKEY_OVERLAY_SCHEMA; readonly overlayId: string; readonly sourceArtifact: Readonly; readonly sourceBindingDigest: string; readonly revision: number; readonly previousOverlayDigest: string | null; readonly fromKeyId: string; readonly targetCatalogFence: Readonly; readonly algorithm: typeof TOOL_EXECUTION_RESULT_REKEY_ALGORITHM; readonly nonce: string; readonly ciphertext: string; readonly authTag: string; readonly plaintextBytes: number; readonly rekeyedAtMs: number; readonly overlayDigest: string; } export interface ToolExecutionResultRekeyCommand { readonly schema: typeof TOOL_EXECUTION_RESULT_REKEY_COMMAND_SCHEMA; readonly expectedRevision: number; readonly expectedOverlayDigest: string | null; readonly mutationId: string; readonly overlay: Readonly; readonly commandDigest: string; } export interface CommitToolExecutionResultRekeyResult { readonly status: 'created' | 'existing'; readonly overlay: Readonly; } export interface ToolExecutionResultRekeyReader { findHeadByArtifactId( artifactId: string, ): Promise | null>; } export interface ToolExecutionResultRekeyRepository extends ToolExecutionResultRekeyReader { append( command: Readonly, ): Promise>; } export interface ToolResultKeyRetirementReceipt { readonly schema: typeof TOOL_RESULT_KEY_RETIREMENT_RECEIPT_SCHEMA; readonly catalogGeneration: number; readonly catalogDigest: string; readonly keyId: string; readonly materialProof: string; readonly mutationId: string; readonly bindingCount: number; readonly overlayHeadCount: number; readonly uncoveredBindingCount: 0; readonly uncoveredOverlayHeadCount: 0; readonly coverageDigest: string; readonly createdAtMs: number; readonly receiptDigest: string; } export interface ToolResultKeyRetirementReceiptCommand { readonly schema: typeof TOOL_RESULT_KEY_RETIREMENT_RECEIPT_COMMAND_SCHEMA; readonly expectedCatalogGeneration: number; readonly expectedCatalogDigest: string; readonly keyId: string; readonly mutationId: string; readonly commandDigest: string; } export interface ToolResultKeyRetirementCoverageFact { readonly artifactId: string; readonly bindingDigest: string; readonly bindingKeyId: string; readonly headOverlayDigest: string | null; readonly headTargetKeyId: string | null; readonly headTargetCatalogGeneration: number | null; readonly headTargetCatalogDigest: string | null; } export interface ToolResultKeyRetirementCoverage { readonly bindingCount: number; readonly overlayHeadCount: number; readonly uncoveredBindingCount: number; readonly uncoveredOverlayHeadCount: number; readonly coverageDigest: string; } export interface CommitToolResultKeyRetirementReceiptResult { readonly status: 'created' | 'existing'; readonly receipt: Readonly; } export interface ToolResultKeyRetirementReceiptRepository { findByDigest( receiptDigest: string, ): Promise | null>; create( command: Readonly, ): Promise>; } export class InvalidToolExecutionResultRekeyError extends TypeError { readonly code = 'TOOL_EXECUTION_RESULT_REKEY_INVALID'; constructor(message: string) { super(`Tool execution result rekey is invalid: ${message}`); this.name = 'InvalidToolExecutionResultRekeyError'; } } export class ToolExecutionResultRekeyConflictError extends Error { readonly code = 'TOOL_EXECUTION_RESULT_REKEY_CONFLICT'; constructor() { super('Tool execution result rekey conflicts with durable state'); this.name = 'ToolExecutionResultRekeyConflictError'; } } export class ToolExecutionResultRekeyUnavailableError extends Error { readonly code = 'TOOL_EXECUTION_RESULT_REKEY_UNAVAILABLE'; constructor(options?: ErrorOptions) { super('Tool execution result rekey authority is unavailable', options); this.name = 'ToolExecutionResultRekeyUnavailableError'; } } const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; const KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/; const DIGEST_PATTERN = /^[0-9a-f]{64}$/; const BASE64URL_PATTERN = /^[A-Za-z0-9_-]*$/; const OUTPUT_DIGEST_DOMAIN = Buffer.from( 'qinglong/trusted-tool-execution-output-digest@v1\0', 'utf8', ); const OVERLAY_DIGEST_DOMAIN = Buffer.from( 'qinglong/tool-execution-result-rekey-overlay-digest@v1\0', 'utf8', ); const COMMAND_DIGEST_DOMAIN = Buffer.from( 'qinglong/tool-execution-result-rekey-command-digest@v1\0', 'utf8', ); const RETIREMENT_RECEIPT_DIGEST_DOMAIN = Buffer.from( 'qinglong/tool-result-key-retirement-receipt-digest@v1\0', 'utf8', ); const RETIREMENT_RECEIPT_COMMAND_DIGEST_DOMAIN = Buffer.from( 'qinglong/tool-result-key-retirement-receipt-command-digest@v1\0', 'utf8', ); const RETIREMENT_COVERAGE_FACT_DIGEST_DOMAIN = Buffer.from( 'qinglong/tool-result-key-retirement-coverage-fact-digest@v1\0', 'utf8', ); const RETIREMENT_COVERAGE_SET_DIGEST_DOMAIN = Buffer.from( 'qinglong/tool-result-key-retirement-coverage-set-digest@v1\0', 'utf8', ); const RETIREMENT_COVERAGE_FINAL_DIGEST_DOMAIN = Buffer.from( 'qinglong/tool-result-key-retirement-coverage-digest@v1\0', 'utf8', ); function invalid(message: string): never { throw new InvalidToolExecutionResultRekeyError(message); } function hash(domain: Uint8Array, value: unknown): string { return createHash('sha256') .update(domain) .update(JSON.stringify(value)) .digest('hex'); } function record(value: unknown, label: string): Record { if ( !value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype ) { return invalid(`${label} is not a plain object`); } return value as Record; } function exactKeys( value: Record, expected: readonly string[], label: string, ): void { const actual = Object.keys(value).sort(); const sorted = [...expected].sort(); if ( actual.length !== sorted.length || actual.some((key, index) => key !== sorted[index]) ) { invalid(`${label} shape is invalid`); } } function identity(value: unknown, label: string): string { if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) { return invalid(`${label} is invalid`); } return value; } function keyId(value: unknown): string { if (typeof value !== 'string' || !KEY_ID_PATTERN.test(value)) { return invalid('key id is invalid'); } return value; } function digest(value: unknown, label: string): string { if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) { return invalid(`${label} is invalid`); } return value; } function nullableDigest(value: unknown, label: string): string | null { return value === null ? null : digest(value, label); } function integer(value: unknown, label: string, minimum = 0): number { if ( !Number.isSafeInteger(value) || (value as number) < minimum || (value as number) > 2_147_483_647 ) { return invalid(`${label} is invalid`); } return value as number; } function timestamp(value: unknown, label: string): number { if (!Number.isSafeInteger(value) || (value as number) < 0) { return invalid(`${label} is invalid`); } return value as number; } function base64url( value: unknown, label: string, exactLength?: number, ): Buffer { if ( typeof value !== 'string' || !BASE64URL_PATTERN.test(value) || value.length % 4 === 1 ) { return invalid(`${label} is invalid`); } const bytes = Buffer.from(value, 'base64url'); if ( bytes.toString('base64url') !== value || (exactLength !== undefined && bytes.length !== exactLength) ) { bytes.fill(0); return invalid(`${label} is invalid`); } return bytes; } function ownedKey(value: Uint8Array): Buffer { if (!(value instanceof Uint8Array) || value.byteLength !== 32) { return invalid('key material is invalid'); } return Buffer.from(value); } function sourceReference( artifact: Readonly, ): Readonly { return Object.freeze({ artifactId: artifact.artifactId, artifactDigest: artifact.artifactDigest, outputDigest: artifact.outputDigest, executionResultDigest: artifact.executionResultDigest, }); } function normalizeSourceReference( value: ToolExecutionResultArtifactReference, ): Readonly { const candidate = record(value, 'source Artifact reference'); exactKeys( candidate, ['artifactDigest', 'artifactId', 'executionResultDigest', 'outputDigest'], 'source Artifact reference', ); return Object.freeze({ artifactId: identity(value.artifactId, 'source Artifact id'), artifactDigest: digest(value.artifactDigest, 'source Artifact digest'), outputDigest: digest(value.outputDigest, 'source output digest'), executionResultDigest: digest( value.executionResultDigest, 'source execution result digest', ), }); } function overlayMetadata( value: Omit< ToolExecutionResultRekeyOverlay, 'authTag' | 'ciphertext' | 'nonce' | 'overlayDigest' >, ) { return Object.freeze(value); } function overlayAad(value: ReturnType): Buffer { return Buffer.from(JSON.stringify(value), 'utf8'); } export function normalizeToolExecutionResultRekeyOverlay( value: ToolExecutionResultRekeyOverlay, ): Readonly { const candidate = record(value, 'rekey overlay'); exactKeys( candidate, [ 'algorithm', 'authTag', 'ciphertext', 'fromKeyId', 'nonce', 'overlayDigest', 'overlayId', 'plaintextBytes', 'previousOverlayDigest', 'rekeyedAtMs', 'revision', 'schema', 'sourceArtifact', 'sourceBindingDigest', 'targetCatalogFence', ], 'rekey overlay', ); if ( value.schema !== TOOL_EXECUTION_RESULT_REKEY_OVERLAY_SCHEMA || value.algorithm !== TOOL_EXECUTION_RESULT_REKEY_ALGORITHM ) { return invalid('rekey overlay schema or algorithm is invalid'); } const revision = integer(value.revision, 'rekey revision', 1); const previousOverlayDigest = nullableDigest( value.previousOverlayDigest, 'previous overlay digest', ); if ((revision === 1) !== (previousOverlayDigest === null)) { return invalid('rekey overlay revision fence is invalid'); } const targetCatalogFence = normalizeToolResultKeyCatalogFence( value.targetCatalogFence, ); const fromKey = keyId(value.fromKeyId); if (fromKey === targetCatalogFence.keyId) { return invalid('rekey target must differ from source key'); } const nonce = base64url(value.nonce, 'rekey nonce', 12); const ciphertext = base64url(value.ciphertext, 'rekey ciphertext'); const authTag = base64url(value.authTag, 'rekey auth tag', 16); nonce.fill(0); ciphertext.fill(0); authTag.fill(0); const unsigned = Object.freeze({ schema: TOOL_EXECUTION_RESULT_REKEY_OVERLAY_SCHEMA, overlayId: identity(value.overlayId, 'rekey overlay id'), sourceArtifact: normalizeSourceReference(value.sourceArtifact), sourceBindingDigest: digest( value.sourceBindingDigest, 'source binding digest', ), revision, previousOverlayDigest, fromKeyId: fromKey, targetCatalogFence, algorithm: TOOL_EXECUTION_RESULT_REKEY_ALGORITHM, nonce: value.nonce, ciphertext: value.ciphertext, authTag: value.authTag, plaintextBytes: integer(value.plaintextBytes, 'plaintext byte count'), rekeyedAtMs: timestamp(value.rekeyedAtMs, 'rekey time'), }); if ( unsigned.plaintextBytes > MAX_TOOL_OUTPUT_BYTES || Buffer.byteLength(JSON.stringify(unsigned), 'utf8') > MAX_TOOL_EXECUTION_RESULT_REKEY_OVERLAY_JSON_BYTES ) { return invalid('rekey overlay exceeds its budget'); } const overlayDigest = digest(value.overlayDigest, 'rekey overlay digest'); if (hash(OVERLAY_DIGEST_DOMAIN, unsigned) !== overlayDigest) { return invalid('rekey overlay digest does not match'); } return Object.freeze({ ...unsigned, overlayDigest }); } export function normalizeToolExecutionResultRekeyCommand( value: ToolExecutionResultRekeyCommand, ): Readonly { const candidate = record(value, 'rekey command'); exactKeys( candidate, [ 'commandDigest', 'expectedOverlayDigest', 'expectedRevision', 'mutationId', 'overlay', 'schema', ], 'rekey command', ); if (value.schema !== TOOL_EXECUTION_RESULT_REKEY_COMMAND_SCHEMA) { return invalid('rekey command schema is invalid'); } const expectedRevision = integer( value.expectedRevision, 'expected rekey revision', ); const expectedOverlayDigest = nullableDigest( value.expectedOverlayDigest, 'expected overlay digest', ); const overlay = normalizeToolExecutionResultRekeyOverlay(value.overlay); if ( overlay.revision !== expectedRevision + 1 || overlay.previousOverlayDigest !== expectedOverlayDigest || (expectedRevision === 0) !== (expectedOverlayDigest === null) ) { return invalid('rekey command head fence is invalid'); } const unsigned = Object.freeze({ schema: TOOL_EXECUTION_RESULT_REKEY_COMMAND_SCHEMA, expectedRevision, expectedOverlayDigest, mutationId: identity(value.mutationId, 'rekey mutation id'), overlay, }); const commandDigest = digest(value.commandDigest, 'rekey command digest'); if (hash(COMMAND_DIGEST_DOMAIN, unsigned) !== commandDigest) { return invalid('rekey command digest does not match'); } return Object.freeze({ ...unsigned, commandDigest }); } export function createToolExecutionResultRekeyCommand( input: Readonly<{ readonly artifact: ToolExecutionResultArtifact; readonly binding: ToolExecutionResultKeyBinding; readonly previousOverlay: ToolExecutionResultRekeyOverlay | null; readonly overlayId: string; readonly mutationId: string; readonly targetCatalogFence: ToolResultKeyCatalogFence; readonly targetKey: Uint8Array; readonly output: ToolJsonValue; readonly rekeyedAtMs: number; readonly registry: ToolDefinitionRegistry; readonly nonceFactory?: () => Uint8Array; }>, ): Readonly { const artifact = normalizeToolExecutionResultArtifact(input.artifact); const binding = normalizeToolExecutionResultKeyBinding(input.binding); if ( binding.startId !== artifact.startId || binding.artifactId !== artifact.artifactId || binding.artifactDigest !== artifact.artifactDigest || binding.keyId !== artifact.keyId ) { throw new ToolExecutionResultRekeyConflictError(); } if (!(input.registry instanceof ToolDefinitionRegistry)) { return invalid('Tool registry is invalid'); } const output = input.registry.normalizeOutput( artifact.tool.name, artifact.tool.version, input.output, ); const plaintext = Buffer.from(JSON.stringify(output), 'utf8'); if ( plaintext.length > MAX_TOOL_OUTPUT_BYTES || hash(OUTPUT_DIGEST_DOMAIN, output) !== artifact.outputDigest ) { plaintext.fill(0); return invalid('rekey output does not match source Artifact'); } const previous = input.previousOverlay === null ? null : normalizeToolExecutionResultRekeyOverlay(input.previousOverlay); const source = sourceReference(artifact); if ( previous && (JSON.stringify(previous.sourceArtifact) !== JSON.stringify(source) || previous.sourceBindingDigest !== binding.bindingDigest) ) { plaintext.fill(0); throw new ToolExecutionResultRekeyConflictError(); } const targetCatalogFence = normalizeToolResultKeyCatalogFence( input.targetCatalogFence, ); const targetKey = ownedKey(input.targetKey); let nonce: Buffer | undefined; try { if ( toolResultKeyMaterialProof(targetCatalogFence.keyId, targetKey) !== targetCatalogFence.materialProof ) { return invalid('target key material does not match catalog fence'); } const fromKeyId = previous?.targetCatalogFence.keyId ?? binding.keyId; nonce = Buffer.from((input.nonceFactory ?? (() => randomBytes(12)))()); if (nonce.length !== 12) { throw new ToolExecutionResultRekeyUnavailableError(); } const metadata = overlayMetadata({ schema: TOOL_EXECUTION_RESULT_REKEY_OVERLAY_SCHEMA, overlayId: identity(input.overlayId, 'rekey overlay id'), sourceArtifact: source, sourceBindingDigest: binding.bindingDigest, revision: (previous?.revision ?? 0) + 1, previousOverlayDigest: previous?.overlayDigest ?? null, fromKeyId, targetCatalogFence, algorithm: TOOL_EXECUTION_RESULT_REKEY_ALGORITHM, plaintextBytes: plaintext.length, rekeyedAtMs: timestamp(input.rekeyedAtMs, 'rekey time'), }); const cipher = createCipheriv( TOOL_EXECUTION_RESULT_REKEY_ALGORITHM, targetKey, nonce, { authTagLength: 16 }, ); const aad = overlayAad(metadata); try { cipher.setAAD(aad); } finally { aad.fill(0); } const ciphertext = Buffer.concat([ cipher.update(plaintext), cipher.final(), ]); try { const unsigned = Object.freeze({ schema: metadata.schema, overlayId: metadata.overlayId, sourceArtifact: metadata.sourceArtifact, sourceBindingDigest: metadata.sourceBindingDigest, revision: metadata.revision, previousOverlayDigest: metadata.previousOverlayDigest, fromKeyId: metadata.fromKeyId, targetCatalogFence: metadata.targetCatalogFence, algorithm: metadata.algorithm, nonce: nonce.toString('base64url'), ciphertext: ciphertext.toString('base64url'), authTag: cipher.getAuthTag().toString('base64url'), plaintextBytes: metadata.plaintextBytes, rekeyedAtMs: metadata.rekeyedAtMs, }); const overlay = normalizeToolExecutionResultRekeyOverlay({ ...unsigned, overlayDigest: hash(OVERLAY_DIGEST_DOMAIN, unsigned), }); const commandUnsigned = Object.freeze({ schema: TOOL_EXECUTION_RESULT_REKEY_COMMAND_SCHEMA, expectedRevision: previous?.revision ?? 0, expectedOverlayDigest: previous?.overlayDigest ?? null, mutationId: identity(input.mutationId, 'rekey mutation id'), overlay, }); return normalizeToolExecutionResultRekeyCommand({ ...commandUnsigned, commandDigest: hash(COMMAND_DIGEST_DOMAIN, commandUnsigned), }); } finally { ciphertext.fill(0); } } catch (cause) { if ( cause instanceof InvalidToolExecutionResultRekeyError || cause instanceof ToolExecutionResultRekeyConflictError || cause instanceof ToolExecutionResultRekeyUnavailableError ) { throw cause; } throw new ToolExecutionResultRekeyUnavailableError({ cause: cause instanceof Error ? cause : undefined, }); } finally { targetKey.fill(0); plaintext.fill(0); nonce?.fill(0); } } export function openToolExecutionResultRekeyOverlay( value: ToolExecutionResultRekeyOverlay, keyValue: Uint8Array, registry: ToolDefinitionRegistry, sourceArtifactValue: ToolExecutionResultArtifact, ): ToolJsonValue { const overlay = normalizeToolExecutionResultRekeyOverlay(value); const sourceArtifact = normalizeToolExecutionResultArtifact(sourceArtifactValue); if ( JSON.stringify(overlay.sourceArtifact) !== JSON.stringify(sourceReference(sourceArtifact)) ) { throw new ToolExecutionResultRekeyConflictError(); } if (!(registry instanceof ToolDefinitionRegistry)) { return invalid('Tool registry is invalid'); } const key = ownedKey(keyValue); const nonce = base64url(overlay.nonce, 'rekey nonce', 12); const ciphertext = base64url(overlay.ciphertext, 'rekey ciphertext'); const authTag = base64url(overlay.authTag, 'rekey auth tag', 16); const aad = overlayAad( overlayMetadata({ schema: overlay.schema, overlayId: overlay.overlayId, sourceArtifact: overlay.sourceArtifact, sourceBindingDigest: overlay.sourceBindingDigest, revision: overlay.revision, previousOverlayDigest: overlay.previousOverlayDigest, fromKeyId: overlay.fromKeyId, targetCatalogFence: overlay.targetCatalogFence, algorithm: overlay.algorithm, plaintextBytes: overlay.plaintextBytes, rekeyedAtMs: overlay.rekeyedAtMs, }), ); let plaintext: Buffer | undefined; try { if ( toolResultKeyMaterialProof(overlay.targetCatalogFence.keyId, key) !== overlay.targetCatalogFence.materialProof ) { throw new ToolExecutionResultRekeyUnavailableError(); } const decipher = createDecipheriv( TOOL_EXECUTION_RESULT_REKEY_ALGORITHM, key, nonce, { authTagLength: 16 }, ); decipher.setAAD(aad); decipher.setAuthTag(authTag); plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]); if (plaintext.length !== overlay.plaintextBytes) { throw new ToolExecutionResultRekeyUnavailableError(); } const parsed = JSON.parse(plaintext.toString('utf8')) as unknown; const output = registry.normalizeOutput( sourceArtifact.tool.name, sourceArtifact.tool.version, parsed, ); if ( JSON.stringify(output) !== plaintext.toString('utf8') || hash(OUTPUT_DIGEST_DOMAIN, output) !== overlay.sourceArtifact.outputDigest ) { throw new ToolExecutionResultRekeyUnavailableError(); } return output; } catch (cause) { if (cause instanceof InvalidToolExecutionResultRekeyError) throw cause; throw new ToolExecutionResultRekeyUnavailableError({ cause: cause instanceof Error ? cause : undefined, }); } finally { key.fill(0); nonce.fill(0); ciphertext.fill(0); authTag.fill(0); aad.fill(0); plaintext?.fill(0); } } export function normalizeToolResultKeyRetirementReceipt( value: ToolResultKeyRetirementReceipt, ): Readonly { const candidate = record(value, 'retirement receipt'); exactKeys( candidate, [ 'bindingCount', 'catalogDigest', 'catalogGeneration', 'coverageDigest', 'createdAtMs', 'keyId', 'materialProof', 'mutationId', 'overlayHeadCount', 'receiptDigest', 'schema', 'uncoveredBindingCount', 'uncoveredOverlayHeadCount', ], 'retirement receipt', ); if ( value.schema !== TOOL_RESULT_KEY_RETIREMENT_RECEIPT_SCHEMA || value.uncoveredBindingCount !== 0 || value.uncoveredOverlayHeadCount !== 0 ) { return invalid('retirement receipt is not fully covered'); } const unsigned = Object.freeze({ schema: TOOL_RESULT_KEY_RETIREMENT_RECEIPT_SCHEMA, catalogGeneration: integer( value.catalogGeneration, 'retirement catalog generation', 1, ), catalogDigest: digest(value.catalogDigest, 'retirement catalog digest'), keyId: keyId(value.keyId), materialProof: digest(value.materialProof, 'retirement material proof'), mutationId: identity(value.mutationId, 'retirement mutation id'), bindingCount: integer(value.bindingCount, 'retirement binding count'), overlayHeadCount: integer( value.overlayHeadCount, 'retirement overlay head count', ), uncoveredBindingCount: 0 as const, uncoveredOverlayHeadCount: 0 as const, coverageDigest: digest(value.coverageDigest, 'retirement coverage digest'), createdAtMs: timestamp(value.createdAtMs, 'retirement receipt time'), }); const receiptDigest = digest( value.receiptDigest, 'retirement receipt digest', ); if (hash(RETIREMENT_RECEIPT_DIGEST_DOMAIN, unsigned) !== receiptDigest) { return invalid('retirement receipt digest does not match'); } return Object.freeze({ ...unsigned, receiptDigest }); } export function createToolResultKeyRetirementReceipt( input: Omit< ToolResultKeyRetirementReceipt, | 'receiptDigest' | 'schema' | 'uncoveredBindingCount' | 'uncoveredOverlayHeadCount' >, ): Readonly { const unsigned = Object.freeze({ schema: TOOL_RESULT_KEY_RETIREMENT_RECEIPT_SCHEMA, catalogGeneration: input.catalogGeneration, catalogDigest: input.catalogDigest, keyId: input.keyId, materialProof: input.materialProof, mutationId: input.mutationId, bindingCount: input.bindingCount, overlayHeadCount: input.overlayHeadCount, uncoveredBindingCount: 0 as const, uncoveredOverlayHeadCount: 0 as const, coverageDigest: input.coverageDigest, createdAtMs: input.createdAtMs, }); return normalizeToolResultKeyRetirementReceipt({ ...unsigned, receiptDigest: hash(RETIREMENT_RECEIPT_DIGEST_DOMAIN, unsigned), }); } export function normalizeToolResultKeyRetirementReceiptCommand( value: ToolResultKeyRetirementReceiptCommand, ): Readonly { const candidate = record(value, 'retirement receipt command'); exactKeys( candidate, [ 'commandDigest', 'expectedCatalogDigest', 'expectedCatalogGeneration', 'keyId', 'mutationId', 'schema', ], 'retirement receipt command', ); if (value.schema !== TOOL_RESULT_KEY_RETIREMENT_RECEIPT_COMMAND_SCHEMA) { return invalid('retirement receipt command schema is invalid'); } const unsigned = Object.freeze({ schema: TOOL_RESULT_KEY_RETIREMENT_RECEIPT_COMMAND_SCHEMA, expectedCatalogGeneration: integer( value.expectedCatalogGeneration, 'expected retirement catalog generation', 1, ), expectedCatalogDigest: digest( value.expectedCatalogDigest, 'expected retirement catalog digest', ), keyId: keyId(value.keyId), mutationId: identity(value.mutationId, 'retirement mutation id'), }); const commandDigest = digest( value.commandDigest, 'retirement receipt command digest', ); if ( hash(RETIREMENT_RECEIPT_COMMAND_DIGEST_DOMAIN, unsigned) !== commandDigest ) { return invalid('retirement receipt command digest does not match'); } return Object.freeze({ ...unsigned, commandDigest }); } export function createToolResultKeyRetirementReceiptCommand( input: Omit< ToolResultKeyRetirementReceiptCommand, 'commandDigest' | 'schema' >, ): Readonly { const unsigned = Object.freeze({ schema: TOOL_RESULT_KEY_RETIREMENT_RECEIPT_COMMAND_SCHEMA, expectedCatalogGeneration: input.expectedCatalogGeneration, expectedCatalogDigest: input.expectedCatalogDigest, keyId: input.keyId, mutationId: input.mutationId, }); return normalizeToolResultKeyRetirementReceiptCommand({ ...unsigned, commandDigest: hash(RETIREMENT_RECEIPT_COMMAND_DIGEST_DOMAIN, unsigned), }); } export class ToolResultKeyRetirementCoverageBuilder { readonly #catalogGeneration: number; readonly #catalogDigest: string; readonly #keyId: string; readonly #decryptableKeyIds!: ReadonlySet; readonly #facts = createHash('sha256'); #lastArtifactId: string | null = null; #bindingCount = 0; #overlayHeadCount = 0; #uncoveredBindingCount = 0; #uncoveredOverlayHeadCount = 0; #finished = false; constructor( input: Readonly<{ catalogGeneration: number; catalogDigest: string; keyId: string; decryptableKeyIds: readonly string[]; }>, ) { this.#catalogGeneration = integer( input.catalogGeneration, 'coverage catalog generation', 1, ); this.#catalogDigest = digest( input.catalogDigest, 'coverage catalog digest', ); this.#keyId = keyId(input.keyId); if ( !Array.isArray(input.decryptableKeyIds) || input.decryptableKeyIds.length < 1 || input.decryptableKeyIds.length > 16 ) { return invalid('coverage decryptable key set is invalid'); } const normalized = input.decryptableKeyIds.map((value) => keyId(value)); if ( normalized.some( (value, index) => index > 0 && value.localeCompare(normalized[index - 1]!) <= 0, ) || normalized.includes(this.#keyId) ) { return invalid('coverage decryptable key order is invalid'); } this.#decryptableKeyIds = new Set(normalized); this.#facts.update(RETIREMENT_COVERAGE_SET_DIGEST_DOMAIN); this.#facts.update( JSON.stringify({ catalogGeneration: this.#catalogGeneration, catalogDigest: this.#catalogDigest, keyId: this.#keyId, decryptableKeyIds: normalized, }), ); } add(value: ToolResultKeyRetirementCoverageFact): void { if (this.#finished) return invalid('coverage builder is already finished'); const candidate = record(value, 'retirement coverage fact'); exactKeys( candidate, [ 'artifactId', 'bindingDigest', 'bindingKeyId', 'headOverlayDigest', 'headTargetCatalogDigest', 'headTargetCatalogGeneration', 'headTargetKeyId', ], 'retirement coverage fact', ); const artifactId = identity(value.artifactId, 'coverage Artifact id'); if ( this.#lastArtifactId !== null && artifactId.localeCompare(this.#lastArtifactId) <= 0 ) { return invalid('coverage facts are not strictly ordered'); } const headTargetKeyId = value.headTargetKeyId === null ? null : keyId(value.headTargetKeyId); const headOverlayDigest = nullableDigest( value.headOverlayDigest, 'coverage head overlay digest', ); const headTargetCatalogGeneration = value.headTargetCatalogGeneration === null ? null : integer( value.headTargetCatalogGeneration, 'coverage head catalog generation', 1, ); const headTargetCatalogDigest = nullableDigest( value.headTargetCatalogDigest, 'coverage head catalog digest', ); if ( (headTargetKeyId !== null) !== (headOverlayDigest !== null && headTargetCatalogGeneration !== null && headTargetCatalogDigest !== null) ) { return invalid('coverage head shape is invalid'); } const fact = Object.freeze({ artifactId, bindingDigest: digest(value.bindingDigest, 'coverage binding digest'), bindingKeyId: keyId(value.bindingKeyId), headOverlayDigest, headTargetKeyId, headTargetCatalogGeneration, headTargetCatalogDigest, }); const bindingIsRetiring = fact.bindingKeyId === this.#keyId; const headTargetsRetiring = fact.headTargetKeyId === this.#keyId; if (!bindingIsRetiring && !headTargetsRetiring) { return invalid('coverage fact is unrelated to the retiring key'); } if (bindingIsRetiring) { this.#bindingCount += 1; if ( fact.headTargetKeyId === null || !this.#decryptableKeyIds.has(fact.headTargetKeyId) ) { this.#uncoveredBindingCount += 1; } } if (fact.headTargetKeyId !== null) this.#overlayHeadCount += 1; if (headTargetsRetiring) this.#uncoveredOverlayHeadCount += 1; this.#facts.update( Buffer.from(hash(RETIREMENT_COVERAGE_FACT_DIGEST_DOMAIN, fact), 'hex'), ); this.#lastArtifactId = artifactId; } finish(): Readonly { if (this.#finished) return invalid('coverage builder is already finished'); this.#finished = true; const entriesDigest = this.#facts.digest('hex'); const unsigned = Object.freeze({ catalogGeneration: this.#catalogGeneration, catalogDigest: this.#catalogDigest, keyId: this.#keyId, bindingCount: this.#bindingCount, overlayHeadCount: this.#overlayHeadCount, uncoveredBindingCount: this.#uncoveredBindingCount, uncoveredOverlayHeadCount: this.#uncoveredOverlayHeadCount, entriesDigest, }); return Object.freeze({ bindingCount: unsigned.bindingCount, overlayHeadCount: unsigned.overlayHeadCount, uncoveredBindingCount: unsigned.uncoveredBindingCount, uncoveredOverlayHeadCount: unsigned.uncoveredOverlayHeadCount, coverageDigest: hash(RETIREMENT_COVERAGE_FINAL_DIGEST_DOMAIN, unsigned), }); } }