Architecture
Obsign is four separated roles around one frozen record format. The separations are not packaging choices; each one closes a specific attack.
agent ──MCP──▶ obsign-proxy ──MCP──▶ tool server │ │ writes, fsync-before-forward ▼ WAL ◀──read-only── obsign-ledger ──▶ checkpoint store │ │ └── evidence packs ┴─▶ obsign verify (offline)
git repo ──▶ obsign-control ──signed releases──▶ policy + identity bundles ──▶ gatewayThe gateway (obsign-proxy)
Section titled “The gateway (obsign-proxy)”Sits on the MCP path, with stdio for a single agent process and Streamable
HTTP for a fleet. Every act is arbitrated: identity verified on every call,
Cedar policy evaluated, record written and fsync’d before the call is
forwarded. If the process dies in between, the trace shows an act that did
not happen, which is awkward but defensible. The other way round would be an
act with no trace, which ruins the product.
The method space is default-deny in both directions. Discovery
(tools/list, resources/list, prompts/list) is filtered so an invisible
tool is never attempted. Read channels (resources/read, prompts/get),
completions, and server-initiated sampling/elicitation all pass the same
gate. Anything unknown (a vendor extension, a future protocol revision) is
refused and recorded.
The gateway holds no signing key. Its only output is the WAL.
The WAL (obsign-wal)
Section titled “The WAL (obsign-wal)”A durable, append-only local log, replayed and origin-verified on startup.
One <chain>.jsonl file per audit chain; with the HTTP transport, one chain
per session. Records store only args_hash; the argument contents are never
written.
The ledger (obsign-ledger)
Section titled “The ledger (obsign-ledger)”As long as sealing happens inside the gateway, the signing key and the log cohabit on one host: whoever compromises it can rewrite the log and re-seal it. The ledger runs elsewhere, on another machine or as a cron under another identity. It reads the WAL without ever writing to it, and seals with a key the gateway never holds. In production that key lives in an HSM behind PKCS#11 and never enters the ledger process either.
Before sealing anything new, the ledger re-hashes the record at the sealed
boundary: a rewritten WAL, even one recomputed to be internally consistent,
is refused with DivergedLog, and divergence never self-heals by design.
See Sealing & the ledger.
The control plane (obsign-control)
Section titled “The control plane (obsign-control)”Everything the gateway trusts arrives as a signed file, and this is where those files come from: policies, tool catalogue, identity bundle (JWKS included) are compiled from a git checkout into immutable signed releases named by commit sha. There is no UI to click; a rule change is a dated, reviewed pull request. See The control plane.
The verifier (obsign)
Section titled “The verifier (obsign)”The CLI the auditor runs: offline, buildable from source, ~31 crates. See Verifying evidence.
One implementation of the cryptography
Section titled “One implementation of the cryptography”obsign-audit-core is the only place a hash is computed. The gateway, the
ledger, the control plane and the verifier all depend on it. Two
implementations would diverge, and the day the export says “valid” while the
verifier says “tampered”, the product is worth nothing.
Other core decisions, in brief:
- Hand-rolled canonical encoding, no JSON for hashes. Every field is length-prefixed and concatenation is injective. JSON remains the transport format and plays no part in the computation.
- Hashes, not contents. Prompts and tool arguments contain personal data.
The log keeps the hash; retained content is encrypted with a customer-held
key (
SealedRef). - Merkle with promotion. Duplication (CVE-2012-2459) lets you build two different batches with the same root.
- Domain separation. Records, leaves, internal nodes and checkpoints hash with distinct prefixes, so none can be presented as another.