agate-audit¶
The audit bounded context: an append-only transparency log modeled as an RFC 6962-style Merkle tree.
agate-audit records every (event, verdict) the proxy produces into a
tamper-evident, append-only log. Rather than a naive hash chain, it uses an
RFC 6962 Merkle tree, which supports
efficient inclusion and consistency proofs.
Responsibility¶
- Append entries and maintain a Merkle tree over them.
- Produce a signed tree head (the root, size, and algorithm epoch, signed).
- Answer inclusion proofs (entry i is in the tree of head H) and consistency proofs (head H₂ is an append-only extension of H₁).
- Stay verifiable across crypto epochs (see agate-crypto).
The Merkle transparency log¶
flowchart TD
root["root hash<br/>(signed tree head)"]
n01["hash(0,1)"]
n23["hash(2,3)"]
l0["leaf 0<br/>(event,verdict)"]
l1["leaf 1"]
l2["leaf 2"]
l3["leaf 3"]
root --> n01
root --> n23
n01 --> l0
n01 --> l1
n23 --> l2
n23 --> l3
Appending a leaf recomputes the path to the root; the new signed tree head commits to the whole history, so any retroactive edit of a past entry changes the root and breaks every later head.
Domain language¶
TransparencyLog— the aggregate root (embeds a domain-event collection).- Merkle values, entities, services (hashing), and factories
under
domain/merkle/. - Domain ports:
Clock,IdGenerator.
Layering¶
| Layer | Contents |
|---|---|
domain |
Pure entities, value objects, and domain services (Merkle hashing, the TransparencyLog aggregate, proofs). No I/O. |
application |
CQRS use cases (command/query handlers) over a mediator pipeline; pipeline behaviors (TransactionBehavior, MetricsBehavior); outbound ports (KeyStore, CheckpointAnchor, EventOutbox, TransactionManager, AuditMetrics, and the CQRS log gateways). |
infrastructure |
Concrete adapters: SystemClock, UuidLogIdGenerator, PostgresLog{Command,Query}Gateway, transaction management, migrations, Ed25519KeyStore, LoggingCheckpointAnchor. |
presentation |
HTTP handlers (health, versioned routes) and AuditError → HTTP mapping. |
setup |
Composition root: typed config from env, the froodi IoC container, HTTP bootstrap. |
Persistence is CQRS-split: a command gateway loads/saves the aggregate
(write side); a query gateway returns read models/DTOs (read side). The crate
depends inward on agate-crypto for hashing and signing.
Checkpoints (signed tree heads)¶
A checkpoint is a signed tree head — the Merkle root and size at an
instant, signed so anyone can verify the log's state and detect tampering.
POST /logs/{id}/checkpoint with {"key_id": "…"} runs the IssueCheckpoint
command: it snapshots the head, signs it with the configured Ed25519 key,
anchors it, persists the aggregate, and returns the signed tree head (size,
root, timestamp, key id, algorithm, signature — hex-encoded where binary).
The signing key is loaded from the environment — a 32-byte seed
AUDIT_CHECKPOINT_SEED (64 hex chars) under AUDIT_CHECKPOINT_KEY_ID (default
checkpoint-ed25519). With no seed configured, checkpoint requests fail with a
clear error instead of signing under an ephemeral key no verifier could trust
across restarts. The CheckpointAnchor port is the seam for an independent
witness (the defense against split-view / equivocation); the default adapter
logs the head for external collection.
Observability¶
Append metrics are application logic hidden behind a port, not counter!
calls scattered through the code. An AuditMetrics port is recorded by a
MetricsBehavior in the mediator pipeline, registered outermost on
AppendRecord so it counts the outcome after the transaction behavior commits
or rolls back: one agate_audit_records_appended_total on success, one
agate_audit_records_dropped_total on failure. The infrastructure adapter
writes through the metrics facade; unit tests drive the behavior with a fake.
Records dropped before they reach the pipeline (outbox scope-open failures,
sink backpressure) are counted through the same port at the server.
Invariants & testing¶
Merkle proof round-trips and tamper rejection are covered with proptest. Database-backed gateways are tested with testcontainers in the infrastructure layer.