Skip to content

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 ──▶ gateway

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.

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.

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.

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 CLI the auditor runs: offline, buildable from source, ~31 crates. See Verifying evidence.

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.