Signed statements & keys
Each action record is signed as an RFC 9943 (SCITT) Signed Statement: a COSE_Sign1 (RFC 9052) whose payload is not the record but its digest (RFC 9995 hash envelope). The JSON is stored once; the statement beside it is about 200 bytes. Any COSE library can check the signature, and recomputing the payload from the JSON is one hash.
The record statement
Section titled “The record statement”protected = { 1: -8, ; alg: EdDSA (Ed25519) 4: <raw 32-byte Ed25519 public key>, ; kid 15: { 1: <iss>, 2: <sub> }, ; CWT claims 258: -16, ; payload hash alg: SHA-256 259: "application/vnd.brutor.action-record+json" } ; preimage content typeunprotected = {} ; receipts are attached to heads, not recordspayload = <raw 32 bytes: SHA-256 of the JCS record bytes> ; == record_idsignature = Ed25519( cbor(["Signature1", protected_bstr, h'', payload]) )- No label 3 (content type) in hash-envelope mode; the preimage type is label 259.
payloadequals the record’srecord_iddecoded from hex. A verifier recomputes the record id from the JSON and compares —statement_payload_mismatchif it differs.kidis the raw public key itself, so a statement names the key it was signed with; a verifier decides separately whether it trusts that key.
| Claim | Value |
|---|---|
iss (CWT 1) |
BRUTOR_EVIDENCE_ISSUER when set (e.g. a did:web: identity), else urn:brutor:deployment:<BRUTOR_DEPLOYMENT_ID> |
sub (CWT 2) |
ai-system:<ai_system_id> for a governed record, tenant:<tenant_id> otherwise |
The tree-head statement
Section titled “The tree-head statement”A signed tree head is a Signed Statement in full-content mode: label 3 is
application/vnd.brutor.tree-head+cbor, the payload is a canonical CBOR map
{log_id, tree_size, root, prev_size, prev_root, issued_at, consistency_proof}, and the
CWT claims are iss = <log_id>, sub = "<log_id>#<tree_size>". It is signed with the
same key as the tenant’s records. See tenant logs.
The public key document
Section titled “The public key document”GET /.well-known/brutor-evidence-keys.json on the Core Proxy is public and read-only. It
lists every key this deployment signs or signed with:
{ "issuer": "urn:brutor:deployment:prod-eu-1", "record_schema": "ai.brutor/action-record/v1", "statement_content_type": "application/vnd.brutor.action-record+json", "tree_head_content_type": "application/vnd.brutor.tree-head+cbor", "keys": [ { "kid": "3b6a27bc…", "kty": "OKP", "crv": "Ed25519", "alg": "EdDSA", "x": "O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik", "use": "sig", "issuer": "urn:brutor:deployment:prod-eu-1", "status": "active", "scope": "deployment" }, { "kid": "9f1c…", "kty": "OKP", "crv": "Ed25519", "alg": "EdDSA", "x": "…", "use": "sig", "issuer": "urn:brutor:deployment:prod-eu-1", "status": "active", "scope": "tenant", "log_id": "5e0d…" } ]}kid is the lowercase hex of the raw public key; x is the same key base64url (a JWK).
Tenant keys are listed by log_id — an opaque hash — never by tenant id, because the
document is public. Retired keys stay listed with status: "retired" and stay trusted for
what they signed. Hand this document to a verifier as its trust anchor
(brutor-verify --keys); pin it out-of-band rather than fetching it from the party whose
evidence you are checking.
The deployment key (BRUTOR_EVIDENCE_SIGNING_KEY, an Ed25519 seed) is the default and
the master switch: without it nothing is sealed, the console says evidence is disabled, and
traffic is unaffected. It is deliberately separate from the audit key
(BRUTOR_AUDIT_SIGNING_KEY): each key signs a closed set of shapes (the audit key signs
row-chain checkpoints and manifests; the evidence key signs record statements and tree
heads), so neither is a signing oracle for the other. The key never leaves the Core
Proxy; the Control Plane holds no key and does no cryptography.
Per-tenant keys
Section titled “Per-tenant keys”A tenant can have its own key. Turn on Settings → Evidence → per-tenant key
(evidence_settings.per_tenant_key), then rotate once: the first rotation creates the
tenant key, stores its seed encrypted under ENCRYPTION_KEY (Fernet, the same envelope as
provider secrets) and seals a hand-over record signed by the deployment key. From then on
that tenant’s records and heads are signed with its own key.
Rotation
Section titled “Rotation”-
POST /v1/admin/evidence/keys/rotate(console: Settings → Evidence → Rotate key), which calls the core’sPOST /v1/evidence/keys/{tenant_id}/rotate. -
The core creates the new key and seals an
ai.brutor.key-rotationrecord signed by the old key, carryingold_kidandnew_kidin clear (public keys are clear-safe) and a SHA-256 reference to the new public key. -
The old key is marked
retired; it stays trusted for verification forever.
Returns 409 per_tenant_key_disabled when the setting is off. Key resolution is cached for
60 seconds per replica and invalidated in-process on rotation, so another replica may sign
with the just-retired key for up to a minute — harmless, because retired keys stay
trusted.
To rotate the deployment key, add the old public key to
BRUTOR_EVIDENCE_RETIRED_KEYS first, then set the new seed. Never edit the seed in place
without retiring the old public key: records it signed would no longer verify against this
deployment’s trust set.
Configuration
Section titled “Configuration”All on the Core Proxy.
| Variable | Example | Meaning |
|---|---|---|
BRUTOR_EVIDENCE_SIGNING_KEY |
9d61b19d…7f60 (64 hex) |
Ed25519 seed of the deployment evidence key. Empty or invalid ⇒ evidence off (no records minted; traffic unaffected; seal endpoints answer 503 evidence_disabled) |
BRUTOR_EVIDENCE_RETIRED_KEYS |
3b6a27bc…,d75a9801… |
Comma-separated raw public keys (hex) of retired deployment keys. Still trusted for verification; never signed with |
BRUTOR_DEPLOYMENT_ID |
prod-eu-1 |
Hashed into every tenant’s log_id and the default issuer. Default default. Set once per install, never change it — a new value cannot open the existing tenants’ logs and sealing fails until it is restored |
BRUTOR_EVIDENCE_ISSUER |
did:web:evidence.example.com |
CWT iss on every record statement. Default urn:brutor:deployment:<BRUTOR_DEPLOYMENT_ID> |
BRUTOR_EVIDENCE_HEAD_CADENCE_ENTRIES |
100 |
Issue a tree head after this many new entries |
BRUTOR_EVIDENCE_HEAD_CADENCE_SECONDS |
900 |
…or when the first uncovered entry is this old |
BRUTOR_EVIDENCE_WITNESS |
on |
off / 0 / false / no stops all witness registration (kill switch) |
BRUTOR_EVIDENCE_PROFILE_AAC |
0 |
1 enables the optional Agent Action Capsule export |
ENCRYPTION_KEY |
(shared with the Control Plane) | Encrypts per-tenant key seeds |
Control Plane variables for the trial bundle’s local witness:
| Variable | Example | Meaning |
|---|---|---|
BRUTOR_LOCAL_WITNESS_URL |
http://witness:8000 |
Registers the bundled brutor-witness as a platform witness flagged same_operator on first boot (create-once). Empty ⇒ not seeded |
BRUTOR_LOCAL_WITNESS_PINNED_KEY |
8a88e3dd… (64 hex) |
Pin for its Ed25519 key; when unset it is fetched once from the witness’s /anchor/authority-pubkey |
BRUTOR_LOCAL_WITNESS_OPERATOR |
self-hosted Brutor trial |
Operator label |
Generate a seed:
openssl rand -hex 32
