- Expanded database schema to include `leaf_hash` in `audit_records` and added `audit_sths` table. - Implemented `server/audit_merkle.ts` for native WebCrypto RFC 6962 tree computations and inclusion proofs. - Created asynchronous micro-batcher in `server/audit.ts` to compute STH, sign with SPIFFE key, save to DB, and broadcast via Valkey. - Refactored `auditLog` to compute leaf hashes synchronously before database inserts. - Added hermetic unit tests with mock fallback patterns for SPIFFE/FFI in `server/audit_merkle.test.ts`. Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
5.3 KiB
5.3 KiB
TASK METADATA
- Target Files:
server/audit.ts,server/audit_merkle.ts,server/valkey.ts,server/db.ts, and database schema/migration files. - Core Objective: Implement an append-only RFC 6962 Merkle Tree audit ledger in PostgreSQL with an in-process Deno micro-batcher for STH computation and Valkey pub/sub broadcast.
- Dependencies: Deno 2.x WebCrypto (Ed25519), Valkey connection, PostgreSQL schema updates, and SPIFFE workload identity key material.
- Additional Important Notes: Must use Deno's native WebCrypto (no external crypto npm packages). Valkey session keys should use raw strings without 'session:' prefix (not fully applicable here, but good standard). Ensure tests mock Valkey, PostgreSQL, and SPIFFE cleanly.
Architectural Considerations & Risks
Before implementing the RFC 6962 Merkle Tree Ledger, we must evaluate the current architecture and explicitly identify risks, regressions, and necessary constraints.
Risks & Tradeoffs:
- In-Process Micro-Batcher Memory & State: The micro-batcher runs
in-process inside Deno using asynchronous intervals (e.g.,
setIntervalor explicit flush). If the Deno process crashes abruptly, unflushed audit logs in memory could be lost before the batch is committed to PostgreSQL and the Merkle tree head is updated.- Mitigation: Ensure
auditLogstill performs an immediate async insert of the record to the database (as it currently does viaserver/audit.ts), but theleaf_hashand tree updates are computed dynamically or buffered for the batched STH computation. The database remains the source of truth; the batcher just finalizes the cryptographical proof.
- Mitigation: Ensure
- Database Schema Locking: Adding a
leaf_hashcolumn to theaudit_recordstable and creating theaudit_sthstable might cause locking if the table is large.- Mitigation: The
leaf_hashcan be computed prior to insert.
- Mitigation: The
- SPIFFE Identity Dependency: STH generation requires the Deno process to
sign using the SPIFFE workload identity/server signing key via WebCrypto. If
SPIFFE/FFI fails or the keys are rotating during a batch, the STH generation
might fail.
- Mitigation: Graceful error handling and retry mechanism for STH signing.
- No Third-Party Dependencies: Adhering to the memory context, we must
exclusively use Deno 2.x native
crypto.subtleAPI for SHA-256 and Ed25519/ECDSA, rather than any Node.js polyfills or npm packages.
Alternatives Evaluated:
- External Worker: Using a dedicated worker (e.g., a separate service or CRON) for computing the Merkle root. Decision: We rejected this to keep the application self-contained and zero-dependency, per user guidance.
- Synchronous Tree Updates: Recomputing the tree on every single write. Decision: Rejected. This would severely block relational write throughput. A micro-batcher (e.g., 30-60s or size threshold) is the optimal path for decoupling heavy crypto logic from the request hot-path.
Proposed Implementation
The execution of this feature will be broken down into the following structured phases:
Phase 1: Database Schema Expansion
- Update PostgreSQL schema to alter
audit_recordsand add aleaf_hash TEXTcolumn. - Create the new
audit_sthstable:CREATE TABLE audit_sths ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tree_size BIGINT NOT NULL, root_hash TEXT NOT NULL, signature TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() );
Phase 2: RFC 6962 Cryptographic Primitives (server/audit_merkle.ts)
- Create
server/audit_merkle.ts. - Implement RFC 6962 leaf hashing:
SHA-256(0x00 || entry_bytes). - Implement internal tree hashing:
SHA-256(0x01 || left_child || right_child). - Implement the logic to construct the Merkle Tree from an array of leaves and
generate an inclusion proof (
verifyInclusionProof). - Ensure all crypto utilizes
crypto.subtle.digest('SHA-256', ...).
Phase 3: The Micro-Batcher & STH Signing (server/audit.ts)
- Refactor
auditLogto compute theleaf_hashsynchronously before the async database insert. - Implement a background micro-batcher in
server/audit.ts(orserver/audit_merkle.tsif better decoupled) usingsetInterval(e.g., every 1000ms, or on explicit flush). - The batcher will:
- Query new
leaf_hashentries from the database since the last STHtree_size. - Compute the new Merkle Root.
- Access the SPIFFE server signing key.
- Sign the STH (tree size, root hash) using
crypto.subtle.sign(Ed25519). - Insert the new STH into the
audit_sthstable.
- Query new
Phase 4: Valkey STH Broadcast (server/valkey.ts)
- Once the STH is successfully saved to PostgreSQL, update the cache.
- Use
valkey.set('auth:audit:latest_sth', json_payload). - Broadcast the update to independent witness nodes via pub/sub:
valkey.publish('auth:audit:sth', json_payload).
Phase 5: Testing & Quality Gates
- Write hermetic tests in
server/audit_merkle.test.ts. - Use
@std/testing/mockto mockvalkey,sqlWrapper, and SPIFFE keys. - Validate leaf hashing correctness against RFC 6962 test vectors.
- Verify the Valkey
PUBLISHpayload formatting. - Execute standard quality gates:
deno fmt,deno task lint,deno task check, anddeno task test.