Govern Specv0.1
Spec v0.1 — draft

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

ChoiceValue
AlgorithmEd25519 — small keys (32B public / 32B private) and signatures (64B), no parameter pitfalls, native Web Crypto support.
HashSHA-256 of the canonical manifest JSON. Signature is over that hash.
EncodingBase64url in pack metadata; PEM at the well-known URL.
Detached vs. embeddedDetached — 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

StateMeaningOperations allowed
activeCurrently signing new audit packs. Exactly one per firm.sign + verify
verified_onlyWas active in the past; rotated out. Stays queryable indefinitely for historical packs.verify only
revokedCompromised. 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:

The protocol does NOT defend against:

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.