Evidence you can actually verify: RFC 8785 canonicalization + Ed25519 signatures + per-check verify

2026-08-08

governancesecurityaudit-chain

LOCKSTEP TRACEABILITY MATRIX --- api_endpoints: [ "POST /v1/governance/audit/verify (rewritten)", "POST /v1/governance/audit/sign (DELETED)", "GET /.well-known/br-evidence-jwks.json (new)", ] sdk_methods_updated: [ "client.governance.verifyEvidence()", "client.governance.signAuditEntry() REMOVED", "governance.verify_evidence()", "governance.sign_audit_entry() REMOVED", ] mcp_tools_updated: ["br_get_governance_artifact"] ---

What We Built

BrainstormRouter's pitch is governance you can PROVE. Four defects meant the proof did not hold, and this change fixes the signing model end to end.

The signature over a Governance Artifact was computed with JSON.stringify(entry, Object.keys(entry).toSorted()). That second argument is a JSON.stringify REPLACER, and an array replacer is a property allowlist applied at every depth — not a top-level key ordering. Every nested field was therefore excluded from the signed bytes. On a real artifact, 76 of its leaves were unsigned: an auditor could rewrite verified.outcome, zero chainProof.eventHash, change tenant_seq, and rewrite the compliance claim from SOC2 CC7.2 to HIPAA 164.312(b), and verification still said valid. Canonicalization is now RFC 8785 (JCS), used by signing and verification alike.

Signing moved from a tenant-derived HKDF-HMAC to Ed25519. Public halves are published at GET /.well-known/br-evidence-jwks.json with real kids, so an auditor verifies OFFLINE with any standard library and never has to trust the system under audit. The HMAC survives as an internal integrity check and is never again surfaced as valid.

POST /v1/governance/audit/verify no longer takes a caller-supplied blob. It takes a request_id or artifact_id and returns four independent checks — signature, registry membership, chain linkage, provenance recompute — each pass, fail, or not_applicable with a reason. POST /v1/governance/audit/sign is deleted.

Why It Matters

A compliance officer's question is not "does this HMAC match" — it is "is this document evidence, and can I prove it to my regulator without asking you". The old answer was a bare {"valid": true} from the vendor's own endpoint, computed over bytes that excluded the fields that mattered. The new answer is a set of named checks, and a signature the regulator can verify on their own laptop.

To be precise about what was NOT broken, since overstating it is its own dishonesty: a forged blob was never retrievable evidence. GET /v1/governance/artifacts/{forged_id} 404s, the blob never joins the hash chain, and it carries no artifactId/tenant_seq/prev_hash/chainProof. /audit/sign was a detached HMAC utility, not evidence minting. The defect was that verify never SAID any of that.

How It Works

One canonical form. src/security/jcs.ts implements RFC 8785. For the FLAT audit-chain payload it emits bytes identical to the old shallow encoder, so not a single committed chain hash changes — jcs.test.ts pins that equivalence, because breaking it would render the whole fleet as chain-broken.

Deterministic signed body. generatedAt is deliberately excluded from the signature. Artifacts are derived on read, so a signed render timestamp would give every fetch different bytes and a different signature, and a held artifact could never be re-verified. With it outside, one committed decision has exactly one canonical signed form.

Keys. A 32-byte Ed25519 seed in BR_EVIDENCE_SIGNING_SEED, added as a key inside the EXISTING consolidated secret brainstormrouter-minimal-routing — no new infrastructure. A seed rather than a PEM because the kid is derived from the public key, so every task in the fleet publishes the same kid and an artifact signed by one task verifies against a JWKS fetched from another. Rotation moves the old seed to BR_EVIDENCE_PREVIOUS_SEEDS: it keeps verifying and stays in JWKS, but never signs.

Fail loudly. A prod-like process with no seed throws at ring load. The artifact then carries signature: null with signingStatus: {state: "unavailable", reason}, and the JWKS route 503s with the reason. Nothing is ever emitted unsigned while reporting success.

Migration window. verifyAuditEntry tries JCS first, then falls back to the pre-fix shallow encoder. A match under the old encoder returns legacy: true with canonicalization: "shallow-v0-legacy", so a pre-migration artifact keeps verifying instead of flipping to "tampered" — while the reader is told the old signature never covered nested content. Nothing is re-signed on read; an old artifact never silently acquires the new canonicalization or the new kid.

Honest not-applicables. chain/verify genuinely recomputes provenance for v2 rows; what was missing was a reason on the rows it skipped. Every non-computed state now names why — "chain payload v1, no provenance commitment to recompute", "bound lineage row is past the retention horizon", and so on. chain_length: 0 reports not_applicable("no chained events for tenant") and never a pass.

The Numbers

  • 76 previously-unsigned artifact leaves, now all covered. The centrepiece test enumerates

EVERY leaf of a real artifact and asserts each single mutation independently breaks verification; with the fix reverted it reports all 76 as undetected.

  • 4 named checks replace 1 misleading boolean.
  • 1 public signing oracle removed.
  • 0 chain hashes invalidated (byte-compatibility pin).

Competitive Edge

Credo AI's evidence is an uploaded document snapshot: you trust the platform because the platform says so. A Governance Artifact is a projection of a real inline decision, bound to the hash chain, and now signed with a key whose public half we publish. The verification does not require us to be online, honest, or even in business.

Lockstep Checklist

  • [x] API Routes: /v1/governance/audit/verify rewritten, /v1/governance/audit/sign

deleted, /.well-known/br-evidence-jwks.json added; routes.json + llms-full.txt regenerated.

  • [x] TS SDK: verifyEvidence() added, signAuditEntry() removed.
  • [x] Python SDK: verify_evidence() added (sync + async), sign_audit_entry() removed.
  • [x] MCP Schemas: br_get_governance_artifact description updated (HMAC → Ed25519 +

JWKS) in src/mcp/handlers/governance.ts, src/mcp/server.ts, and site/public/.well-known/agents.json.

  • [ ] Master Record: not updated — no new capability id was introduced (one removed,

one rewritten in place).

Known Gaps

  • GET /v1/audit/verify (security.audit.verifyverifyHashChain in

src/security/audit-store.ts) still returns valid: true over chain_length: 0. That file is owned by the concurrent single-authority-stores work and was left untouched; verifyTenantAuditChain gained a chainState field for the same purpose.

  • /auth/governance/audit/sign|verify survive because the dashboard integrity tool calls

them. They now carry scope: "detached_hmac", is_evidence: false, and an explanatory note so they cannot be read as evidence verification.