tasq/node_modules/@claude-flow/guidance/dist/proof.js

238 lines
8.4 KiB
JavaScript

/**
* Proof Envelope - Cryptographic Evidence Trail
*
* Makes every run auditable and tamper-evident by producing a hash-chained,
* HMAC-signed envelope for each RunEvent. Each envelope captures:
*
* - SHA-256 content hash of the run event
* - Hash chain linking to the previous envelope (genesis = '0' x 64)
* - Individual tool call hashes
* - Memory lineage (reads/writes with value hashes)
* - HMAC-SHA256 signature over the entire envelope body
*
* @module @claude-flow/guidance/proof
*/
import { createHash, createHmac, randomUUID } from 'node:crypto';
import { timingSafeEqual } from './crypto-utils.js';
// ============================================================================
// Constants
// ============================================================================
const GENESIS_HASH = '0'.repeat(64);
const SERIALIZATION_VERSION = 1;
// ============================================================================
// ProofChain
// ============================================================================
/**
* A tamper-evident, hash-chained sequence of ProofEnvelopes.
*
* Each envelope links to the previous one via `previousHash`, forming
* a blockchain-like structure. Every envelope is HMAC-signed so any
* modification to the chain can be detected.
*/
export class ProofChain {
envelopes = [];
signingKey;
constructor(signingKey) {
if (!signingKey) {
throw new Error('ProofChain requires an explicit signingKey — hardcoded defaults are not secure');
}
this.signingKey = signingKey;
}
/**
* Append a new ProofEnvelope to the chain.
*
* @param runEvent - The RunEvent to wrap
* @param toolCalls - Tool call records from the run
* @param memoryOps - Memory operations from the run
* @param metadata - Optional metadata overrides
* @returns The newly created and signed ProofEnvelope
*/
append(runEvent, toolCalls = [], memoryOps = [], metadata) {
const previousHash = this.envelopes.length > 0
? this.envelopes[this.envelopes.length - 1].contentHash
: GENESIS_HASH;
const contentHash = this.computeContentHash(runEvent);
const toolCallHashes = {};
for (const call of toolCalls) {
toolCallHashes[call.callId] = this.computeToolCallHash(call);
}
const memoryLineage = memoryOps.map(op => ({
key: op.key,
namespace: op.namespace,
operation: op.operation,
hash: op.valueHash,
}));
const envelope = {
envelopeId: randomUUID(),
runEventId: runEvent.eventId,
timestamp: new Date().toISOString(),
contentHash,
previousHash,
toolCallHashes,
guidanceHash: runEvent.guidanceHash,
memoryLineage,
signature: '', // placeholder; signed below
metadata: {
agentId: metadata?.agentId ?? 'unknown',
sessionId: metadata?.sessionId ?? runEvent.sessionId ?? 'unknown',
parentEnvelopeId: metadata?.parentEnvelopeId,
},
};
envelope.signature = this.signEnvelope(envelope);
this.envelopes.push(envelope);
return envelope;
}
/**
* Verify a single envelope's HMAC signature and hash chain link.
*
* @returns true if the signature is valid and the previousHash is correct
*/
verify(envelope) {
// Verify HMAC signature
const expectedSignature = this.signEnvelope(envelope);
if (!timingSafeEqual(envelope.signature, expectedSignature)) {
return false;
}
// Verify hash chain linkage
const index = this.envelopes.findIndex(e => e.envelopeId === envelope.envelopeId);
if (index === -1) {
// Envelope not in this chain; verify signature only
return true;
}
if (index === 0) {
return envelope.previousHash === GENESIS_HASH;
}
return envelope.previousHash === this.envelopes[index - 1].contentHash;
}
/**
* Verify the entire chain from genesis to tip.
*
* Checks that every envelope:
* 1. Has a valid HMAC signature
* 2. Links correctly to the previous envelope's contentHash
*
* @returns true if the full chain is intact
*/
verifyChain() {
if (this.envelopes.length === 0) {
return true;
}
for (let i = 0; i < this.envelopes.length; i++) {
const envelope = this.envelopes[i];
// Verify signature (constant-time comparison)
const expectedSignature = this.signEnvelope(envelope);
if (!timingSafeEqual(envelope.signature, expectedSignature)) {
return false;
}
// Verify hash chain
if (i === 0) {
if (envelope.previousHash !== GENESIS_HASH) {
return false;
}
}
else {
if (envelope.previousHash !== this.envelopes[i - 1].contentHash) {
return false;
}
}
}
return true;
}
/**
* Retrieve an envelope by its ID.
*/
getEnvelope(id) {
return this.envelopes.find(e => e.envelopeId === id);
}
/**
* Get the most recent envelope in the chain.
*/
getChainTip() {
return this.envelopes.length > 0
? this.envelopes[this.envelopes.length - 1]
: undefined;
}
/**
* Get the number of envelopes in the chain.
*/
getChainLength() {
return this.envelopes.length;
}
/**
* Export the chain as a serializable object.
*/
export() {
return {
envelopes: this.envelopes.map(e => ({ ...e })),
createdAt: new Date().toISOString(),
version: SERIALIZATION_VERSION,
};
}
/**
* Restore the chain from a previously exported object.
*
* Replaces the current chain contents entirely.
*/
import(data) {
if (data.version !== SERIALIZATION_VERSION) {
throw new Error(`Unsupported proof chain version: ${data.version} (expected ${SERIALIZATION_VERSION})`);
}
this.envelopes = data.envelopes.map(e => ({ ...e }));
}
// ===========================================================================
// Private helpers
// ===========================================================================
/**
* Compute the SHA-256 content hash of a RunEvent.
*/
computeContentHash(event) {
const payload = JSON.stringify(event, Object.keys(event).sort());
return createHash('sha256').update(payload).digest('hex');
}
/**
* Compute the SHA-256 hash of a single tool call.
*
* Hash = SHA-256(toolName + JSON(params) + JSON(result))
*/
computeToolCallHash(call) {
const payload = call.toolName
+ JSON.stringify(call.params)
+ JSON.stringify(call.result);
return createHash('sha256').update(payload).digest('hex');
}
/**
* Produce the HMAC-SHA256 signature for an envelope.
*
* The signature covers every field except `signature` itself.
*/
signEnvelope(envelope) {
const body = {
envelopeId: envelope.envelopeId,
runEventId: envelope.runEventId,
timestamp: envelope.timestamp,
contentHash: envelope.contentHash,
previousHash: envelope.previousHash,
toolCallHashes: envelope.toolCallHashes,
guidanceHash: envelope.guidanceHash,
memoryLineage: envelope.memoryLineage,
metadata: envelope.metadata,
};
const payload = JSON.stringify(body);
return createHmac('sha256', this.signingKey).update(payload).digest('hex');
}
}
// ============================================================================
// Factory
// ============================================================================
/**
* Create a new ProofChain instance.
*
* @param config - Configuration with a required `signingKey` for HMAC signing.
* Callers that previously relied on the optional signature must now provide
* an explicit key (see ADR-G026).
* @returns A fresh ProofChain
*/
export function createProofChain(config) {
return new ProofChain(config.signingKey);
}
//# sourceMappingURL=proof.js.map