Skip to content

Tenant logs, tree heads & witnesses

A signature proves who signed a record. It does not prove the signer kept every record, or did not quietly rewrite history and re-sign it. That is what the log and its witnesses are for — and it is the gap the programme was built to close: a log is only as good as the party that keeps it, and the operator who ran the action is not a disinterested witness.

Each tenant has its own append-only RFC 6962 / RFC 9162 Merkle tree. One tenant’s records can therefore be proven to that tenant’s auditor without revealing a single field of another tenant’s.

log_id hex(SHA-256(BRUTOR_DEPLOYMENT_ID ‖ 0x00 ‖ tenant_id)) — never a name or a path
Leaf SHA-256(0x00 ‖ statement bytes) — the leaf commits to the Signed Statement
Node SHA-256(0x01 ‖ left ‖ right)
Entry kinds record, tree_head, disclosure, witness_backfill
Concurrency the flush transaction takes pg_advisory_xact_lock(hashtext('evidence_log:' ‖ tenant_id)), allocates the sequence, appends leaves and nodes — gap-free across replicas

Nodes are append-only (evidence_log_nodes); a node exists only for a full range, and the right-hand partial ranges are folded on demand exactly as RFC 6962’s recursion prescribes.

A signed tree head is issued when either:

  • BRUTOR_EVIDENCE_HEAD_CADENCE_ENTRIES (default 100) entries have been appended since the last head, or
  • the first entry after the last head is older than BRUTOR_EVIDENCE_HEAD_CADENCE_SECONDS (default 900, 15 minutes).

The cadence is evaluated in the same transaction as the appends that made a head due. An idle log is never re-signed — there is nothing new to attest, and re-signing the same root would only manufacture the appearance of activity.

A head is a Signed Statement (full-content, application/vnd.brutor.tree-head+cbor, iss = log_id, sub = "<log_id>#<tree_size>") over:

{ log_id, tree_size, root, prev_size, prev_root, issued_at, consistency_proof }

consistency_proof is the RFC 6962 proof from prev_size to tree_size, so each head proves it extends the previous one. After signing, the head is appended to its own log as a tree_head entry: head N is covered by head N+1’s root, so flipping a byte in a stored head breaks the next root.

Just before a head is signed, each audit row-chain writer that carried this tenant’s rows and advanced its checkpoint seals an fyi record ai.brutor.audit-chain-head (context: {chain_id, seq_end}, references to the chain head hash and the checkpoint signature). The head that follows covers it, so a relying party who trusts the witnessed tenant log also holds a witnessed commitment to the audit row chain up to seq_end — rewriting proxy_logs would mean rewriting the witnessed log too.

Force a head (e.g. before building a bundle — the bundle endpoint does this itself): POST /v1/evidence/logs/{tenant_id}/tree-head → {"issued": true, "head": {…}}, or {"issued": false, "head": null} when nothing is uncovered.

After commit, each head is registered with every enabled witness for the tenant — platform witnesses plus the tenant’s own (for example an auditor’s instance) — independently. Nothing here runs on the request path: fail-open for traffic, fail-loud for evidence.

Kind Protocol What a verified receipt shows
scitt RFC 9943 registration of the head statement (POST /transparency/register-statement) → an RFC 9942 COSE Receipt, attached to the head at unprotected label 394 to form a Transparent Statement the head is included, at a tree size, in the witness’s own append-only log
rfc3161 an RFC 3161 TimeStampReq over SHA-256 of the head statement → a TimeStampToken the head existed at the token’s genTime

Receipts are verified offline, under a pinned key — a key learned from the witness itself is display only, never a trust anchor:

  • scitt: vds read from the protected header only; the inclusion proof cbor([tree_size, leaf_index, path]) from unprotected[396]; the leaf recomputed from the head statement; the path length checked against the expected depth; the root folded and the receipt signature checked against it with the witness’s pinned Ed25519 key. Vendor-private labels are surfaced under vendor_ext and never trusted.
  • rfc3161: checked by brutor-tsp against the pinned TSA certificate — either the PEM, or the SHA-256 of its DER (64 hex), in which case the certificate is taken from the token and trusted only because its hash matches.

Per receipt the verdict is witnessed (scitt), timestamped (rfc3161), unverified (no pin — not evidence of forgery) or invalid.

Witness outages are retried durably from the log itself, oldest first, every 5 minutes; late receipts append as witness_backfill entries. Registration is idempotent per (head, witness). Witness URLs are subject to the outbound policy.

Each head gets a grade from its verified scitt receipts only:

Grade Condition
standalone no verified independent receipt
self_witnessed verified receipts only from witnesses flagged same_operator
witnessed one verified receipt from an independently operated witness
plural verified receipts from two or more distinct independent operators

Grades never go up. An unpinned witness cannot raise a grade; a same-operator witness can only make a head self_witnessed; a bundle’s grade is the minimum of its records’. A timestamp never raises the grade: an RFC 3161 token proves when a head existed, not that anyone else holds it in an append-only structure. A timestamp-only head stays standalone, with its timestamp shown alongside.

Settings → Evidence in the Admin UI, or the API:

Terminal window
curl -X POST http://localhost:5050/v1/admin/evidence/witnesses \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{
"scope": "tenant",
"kind": "scitt",
"url": "https://transparency.auditor.example",
"operator_label": "Example Audit LLP",
"pinned_key": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c",
"same_operator": false
}'

scope is tenant (yours) or platform (system admins; used for every tenant). kind is scitt or rfc3161. pinned_key is the witness’s raw Ed25519 public key in hex for scitt, or the SHA-256 hex of the TSA certificate DER (a PEM also works where it fits) for rfc3161. PATCH enables, relabels or re-pins; DELETE removes. Any RFC 9943 transparency service works — capsule-anchor, CCF-based ledgers, DataTrails-class services, or another organisation’s brutor-witness.

BRUTOR_EVIDENCE_WITNESS=off on the Core Proxy stops all witness registration (no egress). Records and heads are still sealed; heads stay standalone and are shown so.

Every 15 minutes the Control Plane scheduler raises an evidence_unwitnessed inbox item for a tenant that runs a high-risk AI System when its newest tree head, issued more than an hour ago, is still standalone or self_witnessed. The log is self-attested until a witness countersigns; for a high-risk system that is a finding, not a footnote. A head that is only timestamped still raises it — deliberately.

The trial bundle ships a local RFC 9943 transparency service as the witness container (ghcr.io/brutor-ai/brutor-witness): capsule-anchor (Apache-2.0), built unmodified from a pinned commit, keeping its own RFC 6962 log in a brutor_witness database on the bundle’s Postgres.

  1. docker-start.sh mints BRUTOR_WITNESS_SIGNING_KEY (the witness’s authority key) and BRUTOR_EVIDENCE_SIGNING_KEY once and keeps them in .env.

  2. On first boot the Control Plane registers the witness as a platform witness flagged same_operator (BRUTOR_LOCAL_WITNESS_URL=http://witness:8000) and pins its key from /anchor/authority-pubkey, or from BRUTOR_LOCAL_WITNESS_PINNED_KEY when set.

  3. Heads start receiving receipts and grade self_witnessed — ordering proven, independence not claimed.

  4. For witnessed, add an independent service (an auditor’s instance, or a public transparency service) under Settings → Evidence.

Its read-only log endpoints (/anchor/sth, /anchor/consistency-proof, /anchor/authority-pubkey) are published on 127.0.0.1:${WITNESS_PORT:-8300} for your own monitoring. The bundle keeps its public-log and TSA rails off (no external calls).

Prove the whole path end to end on a running bundle:

Terminal window
./scripts/evidence-smoke.sh

It seals a record, forces a head, waits for the witness receipt and checks the head grades self_witnessed under the pinned key, builds a bundle, verifies it (core verifier, and brutor-verify when it is on PATH), then flips one byte of the record statement and one of the receipt and checks that both fail.