Audit-Pack Signing & Public Verification Protocol
A Govern audit pack is the artifact a firm hands to an insurer, external auditor, opposing counsel, or regulator to prove a particular AI-mediated decision was governed. Cryptographic signing makes audit packs tamper-evident: each firm holds an Ed25519 keypair, every audit pack carries a detached signature over the manifest hash, and the public half is published at a well-known URL the verifier can reach without Govern infrastructure access.
A recipient runs npx @govern/conformance verify <pack.zip> offline (modulo a single HTTPS fetch for the public key) and gets a yes/no answer.
Signature scheme
| Choice | Value |
|---|---|
| Algorithm | Ed25519 — small keys (32B public / 32B private) and signatures (64B), no parameter pitfalls, native Web Crypto support. |
| Hash | SHA-256 of the canonical manifest JSON. Signature is over that hash. |
| Encoding | Base64url in pack metadata; PEM at the well-known URL. |
| Detached vs. embedded | Detached — signature stored beside the manifest so the verifier can hash the canonical manifest without first stripping a signature field. |
Pack layout
A signed audit pack is a zip file containing:
pack.zip ├── events.csv ├── decisions.csv ├── chain-integrity.json ├── README.md ├── manifest.json ← canonical metadata, hashed + signed ├── manifest.sig ← Ed25519 signature over SHA-256(manifest.json) └── pubkey-fingerprint.txt ← short hex fingerprint for human visual confirmation
The verifier requires manifest.json + manifest.sig to be present; absence of either is a hard failure. pubkey-fingerprint.txt is for humans, not load-bearing.
manifest.json
Canonical JSON (RFC 8785 JCS — keys sorted, no whitespace, no trailing newline).
{
"spec_version": "v1",
"firm_id": "<firm-id>",
"pack_id": "<UUIDv7>",
"generated_at": "<ISO-8601 UTC>",
"period": {
"from": "<ISO-8601 UTC>",
"to": "<ISO-8601 UTC>"
},
"key_id": "<firm-key-id>",
"files": [
{ "path": "events.csv", "sha256": "<hex>", "row_count": 142 },
{ "path": "decisions.csv", "sha256": "<hex>", "row_count": 142 },
{ "path": "chain-integrity.json", "sha256": "<hex>" },
{ "path": "README.md", "sha256": "<hex>" }
],
"chain_tip": {
"row_hash": "<hex>",
"row_id": 12345,
"event_at": "<ISO-8601 UTC>"
}
}key_id identifies which of the firm’s signing keys produced manifest.sig — required for verifying historical packs after key rotation. chain_tip snapshots the head of the per-firm hash-chain at pack-generation time.
The signed object is SHA-256(canonical(manifest.json)). Tools producing or verifying audit packs MUST canonicalize identically — RFC 8785 JCS is the only conformant format.
manifest.sig
Raw 64-byte Ed25519 signature, base64url-encoded. No PEM wrapper, no leading metadata.
Key management
Key states
| State | Meaning | Operations allowed |
|---|---|---|
active | Currently signing new audit packs. Exactly one per firm. | sign + verify |
verified_only | Was active in the past; rotated out. Stays queryable indefinitely for historical packs. | verify only |
revoked | Compromised. Public key remains queryable but flagged as revoked. Verifiers reject signatures regardless of pack age. | none |
Rotation is the common path: a new key becomes active, the old key moves to verified_only. Existing packs remain verifiable. Revocation is the emergency path — all packs signed by a revoked key are no longer trustworthy.
Public-key publication
The public half of every signing key is published at:
https://mcp.cbtemp.com/.well-known/govern-firm-pubkey/<firm_id>
(Reference implementation host. Migrates to a spec-branded domain when the open spec domain is finalized.)
The endpoint returns application/json:
{
"spec_version": "v1",
"firm_id": "<firm-id>",
"keys": [
{
"key_id": "<UUIDv7>",
"algorithm": "ed25519",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
"public_key_b64u": "<32B base64url>",
"fingerprint_sha256_hex": "<hex>",
"state": "active|verified_only|revoked",
"created_at": "<ISO-8601 UTC>",
"rotated_at": "<ISO-8601 UTC | null>",
"revoked_at": "<ISO-8601 UTC | null>",
"revoke_reason": "<string | null>"
}
]
}Cache headers: Cache-Control: public, max-age=300, stale-while-revalidate=86400. Verifiers MUST honor Cache-Control but MUST NOT cache responses where state === ‘revoked’ for any duration.
Verifier protocol
1. Open pack.zip. Require:
- manifest.json
- manifest.sig
- every file listed in manifest.files
2. For each file in manifest.files:
- Compute SHA-256 of the in-zip bytes.
- Compare to manifest.files[i].sha256.
- Mismatch → fail with file_hash_mismatch.
3. Canonicalize manifest.json per RFC 8785 JCS. Compute SHA-256(canonical-bytes).
4. Decode manifest.sig (base64url → 64 bytes).
5. Fetch /.well-known/govern-firm-pubkey/<manifest.firm_id>.
- Find the key entry where key_id === manifest.key_id.
- If not found → fail with key_not_found.
- If state === 'revoked' → fail with key_revoked.
6. Verify the signature using the public key against the hash from step 3.
- Mismatch → fail with signature_invalid.
7. Open chain-integrity.json. Verify:
- Its computed chain-tip matches manifest.chain_tip.row_hash.
- The chain-integrity report itself reports ok: true.
- Mismatch → fail with chain_integrity_invalid.
8. Return success: { ok: true, key_id, state, chain_tip }.Structured error vocabulary
The error codes below are normative — verifiers in other languages MUST emit the same codes:
pack_malformed, file_missing, file_hash_mismatch, manifest_canonicalization_failed, pubkey_fetch_failed, key_not_found, key_revoked, signature_invalid, chain_integrity_invalid, unsupported_spec_version.
Threat model
The protocol defends against:
- Pack tampering in transit — file hashes catch byte-level changes; manifest signature catches metadata changes.
- Pack-recipient spoofing — well-known endpoint binds the public key to the firm; verifier composes the URL deterministically.
- Key compromise discovered after the fact —
revokedstate lets the firm flag historical packs as no longer trustworthy. - Rollback attacks —
chain_tipin the signed manifest pins a specific point in the firm’s hash-chain history.
The protocol does NOT defend against:
- Compromise of Govern’s signing infrastructure — HSM-backed signing keys are deferred to v1.5.
- A firm signing a pack that contains lies — the protocol proves the pack was authored by the firm and hasn’t been tampered with, not that the firm’s audit ledger reflects reality. Centerbase organizational counter-signature is a v1.5 addition.
Versioning
spec_version: "v1" covers the layout, signature scheme, key states, well-known endpoint shape, and verifier algorithm above. Breaking changes — adding required fields, changing canonicalization rules, switching signature algorithm — bump the major version. Additive changes (new optional manifest fields, new well-known fields) do not.