Back to Insights
Evidence Bundles Explained: Hash Chains, Signatures, and Offline Proof
AI Governance Code Governance GuardSpine Cryptography Evidence Bundles

Evidence Bundles Explained: Hash Chains, Signatures, and Offline Proof

An evidence bundle is a cryptographically sealed package that proves what changed, what rules fired, who approved, and why. Here is how it works, byte by byte.

An evidence bundle is a cryptographically sealed package that proves what changed, what rules fired, who approved, and why — and anyone can verify it without trusting us. Here is how it works, byte by byte.

I wrote the first version of the evidence bundle schema on a whiteboard in November 2025. The question was simple: if I hand an auditor a JSON file, what does it need to contain so that the auditor can independently verify every claim in it without calling any external service?

That question killed four designs before one survived. Here is the one that survived.

The Bundle Schema (v0.2.0)

An evidence bundle is a JSON object with five top-level fields:

{
  "bundle_id": "a3f8c2d1-7b4e-4f2a-9c1d-8e5f3a2b1c4d",
  "version": "0.2.0",
  "created_at": "2026-03-15T14:32:00Z",
  "items": [],
  "immutability_proof": {},
  "signatures": []
}

bundle_id is a UUID v4. Not sequential, not predictable. Each bundle gets a unique identifier at creation time.

version tracks the schema version. This matters for forward compatibility — a verifier built for v0.2.0 knows exactly what fields to expect and how to process them.

created_at is an ISO 8601 timestamp. UTC, always. No timezone ambiguity.

The three arrays — items, immutability_proof, and signatures — are where the work happens.

Items: The Evidence Itself

Each item in the items[] array represents one piece of evidence:

{
  "content_type": "guardspine/diff",
  "content": {
    "file": "src/auth/middleware.ts",
    "changes": [
      {
        "line": 42,
        "type": "modified",
        "before": "const maxAttempts = 5;",
        "after": "const maxAttempts = 10;"
      }
    ]
  },
  "content_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "sequence": 1
}

content_type tells the verifier what kind of evidence this is. The defined types are:

  • guardspine/diff — The actual change that was made.
  • guardspine/approval — A review decision (approved, rejected, approved with comments).
  • guardspine/policy-eval — The result of evaluating the change against a policy.
  • guardspine/rationale — Free-text explanation of why a decision was made.
  • guardspine/council-vote — A vote from a multi-model AI council review.

Each type has a defined schema for its content field. A diff item contains file paths and change details. An approval item contains the reviewer identity and decision. A policy-eval item contains the policy name, result, and risk tier.

content_hash is the SHA-256 hash of the content field, serialized using RFC 8785 canonical JSON. This is critical. I will explain why in a moment.

sequence is the item’s position in the bundle. Sequence numbers are 1-indexed and contiguous. This ordering matters for the hash chain.

Why RFC 8785 Canonical JSON

JSON serialization is not deterministic by default. The same JavaScript object can produce different JSON strings depending on property insertion order, whitespace, and Unicode escaping. If you hash {"a":1,"b":2} and I hash {"b":2,"a":1}, we get different hashes for the same logical content.

RFC 8785 (JSON Canonicalization Scheme, or JCS) solves this. It defines a single canonical serialization for any JSON value: sorted keys, no unnecessary whitespace, specific number formatting, specific Unicode escaping rules.

This means two things:

  1. Any implementation of RFC 8785 in any language produces the same bytes for the same JSON value.
  2. The content hash is deterministic. Recompute it yourself and compare. If it matches, the content has not been modified.

We chose RFC 8785 over alternatives (like CBOR or Protobuf) because the evidence bundles are JSON, the content is JSON, and adding a binary serialization format would introduce a dependency that makes offline verification harder. Keep the stack simple.

The Hash Chain

The immutability_proof object contains the hash chain that ties all items together:

{
  "immutability_proof": {
    "hash_chain": [
      {
        "index": 0,
        "content_hash": "0000000000000000000000000000000000000000000000000000000000000000",
        "chain_hash": "genesis"
      },
      {
        "index": 1,
        "content_hash": "e3b0c44298fc1c14...",
        "chain_hash": "a1b2c3d4e5f6..."
      },
      {
        "index": 2,
        "content_hash": "f7e6d5c4b3a2...",
        "chain_hash": "9f8e7d6c5b4a..."
      }
    ],
    "root_hash": "1a2b3c4d5e6f..."
  }
}

Genesis Sentinel

The hash chain starts with a genesis entry at index 0. Its content hash is all zeros. Its chain hash is the string "genesis". This is a sentinel value that anchors the chain.

Why a sentinel? Because the chain hash computation at index N depends on the chain hash at index N-1. Without the sentinel, index 1 has no predecessor. The sentinel removes a special case (R2: eliminate special cases).

Chain Hash Computation

For each subsequent entry (index 1 through N), the chain hash is computed as:

chain_hash[i] = SHA-256(chain_hash[i-1] + content_hash[i])

Where + is string concatenation of the hex-encoded hashes.

This is a Merkle-style chain. Each entry depends on all previous entries. Modify item 1’s content, and item 1’s content hash changes, which changes item 1’s chain hash, which changes item 2’s chain hash, which cascades through every subsequent entry.

You cannot tamper with any item without invalidating the chain from that point forward. And you cannot “fix” the chain by recomputing forward, because that would change the root hash.

Root Hash

The root hash commits the entire chain:

root_hash = SHA-256(chain_hash[N] + str(N))

Where N is the total number of items (not counting the genesis sentinel). Including the item count prevents a subtle attack: without it, an attacker could append items to the chain without changing the root hash of the truncated chain. The count pins the expected length.

The root hash is a single value that represents the integrity of every item in the bundle. Verify the root hash, and you know the entire chain is intact.

Signatures

The signatures[] array contains cryptographic signatures over the root hash:

{
  "signatures": [
    {
      "algorithm": "ed25519",
      "key_id": "guardspine-signer-prod-2026",
      "public_key": "MCowBQYDK2VwAyEA...",
      "signature": "MEUCIQD...",
      "signed_at": "2026-03-15T14:32:01Z"
    }
  ]
}

Four algorithms are supported:

Ed25519 — The default. Fast, small keys, small signatures, no configuration choices. Daniel Bernstein designed it to be hard to misuse. This is what you should use unless you have a specific reason not to.

RSA-SHA256 — For organizations that require RSA. Key sizes of 2048 bits or larger. Slower than Ed25519, larger signatures, but universally supported by HSMs and enterprise PKI.

ECDSA-P256 — For organizations that require NIST curves. Common in government and regulated environments where NIST approval matters.

HMAC-SHA256 — For symmetric signing. Both parties share the key. This is useful for internal-only bundles where you do not need public verifiability but do want tamper detection.

Each signature entry includes:

  • algorithm: Which of the four algorithms was used.
  • key_id: A human-readable identifier for the key (not the key itself).
  • public_key: The public key (or key ID for HMAC) needed to verify the signature.
  • signature: The signature value, base64-encoded.
  • signed_at: When the signature was created.

A bundle can have multiple signatures. Different signers can sign at different times. An internal signer might sign at creation time, and a compliance officer might countersign during audit. Each signature is independent — they all sign the same root hash.

The Verification Algorithm

Verification is five steps. No network calls. No API keys. No vendor dependencies.

Step 1: Verify content hashes. For each item in items[], serialize the content field using RFC 8785, compute SHA-256, and compare to the stored content_hash. If any mismatch, the content has been tampered with. Stop.

Step 2: Verify the hash chain. Starting from the genesis sentinel, recompute each chain hash using SHA-256(chain_hash[i-1] + content_hash[i]). Compare each computed chain hash to the stored value. If any mismatch, the chain has been tampered with. Stop.

Step 3: Verify the root hash. Compute SHA-256(chain_hash[N] + str(N)) where N is the item count. Compare to the stored root_hash. If mismatch, either the chain or the count has been tampered with. Stop.

Step 4: Verify signatures. For each entry in signatures[], verify the signature over the root hash using the specified algorithm and public key. Record which signatures are valid and which are invalid.

Step 5: Return the verification result. A VerificationResult contains: whether all content hashes are valid, whether the chain is valid, whether the root hash is valid, which signatures are valid, and an overall pass/fail.

interface VerificationResult {
  valid: boolean;
  content_hashes_valid: boolean;
  chain_valid: boolean;
  root_hash_valid: boolean;
  signatures: {
    key_id: string;
    algorithm: string;
    valid: boolean;
  }[];
  errors: string[];
}

The entire algorithm is deterministic. Given the same bundle, any correct implementation produces the same result. There is no judgment involved, no heuristics, no AI inference. The math either checks out or it does not.

What This Makes Impossible

The hash chain and signature design prevents specific attacks:

Content modification. Change any byte in any evidence item’s content, and the content hash changes, which breaks the chain, which changes the root hash, which invalidates all signatures. Detection: step 1.

Item reordering. Swap two items, and their chain hashes change (because each depends on the previous). Detection: step 2.

Item insertion. Insert a new item, and either the chain hashes after the insertion point change, or the item count changes, either of which changes the root hash. Detection: step 2 or 3.

Item deletion. Remove an item, and the chain hashes after the deletion point change, and the item count changes. Detection: step 2 or 3.

Selective disclosure attack. Present only some items from a bundle. The root hash includes the item count, so the verifier knows how many items to expect. A truncated bundle fails at step 3.

Signature stripping. Remove a signature. The remaining signatures still validate, but the verifier can check whether expected signers are present. The bundle format supports signature requirements as part of policy.

Why Not a Blockchain

I get this question every time I present the architecture. The answer is simple: a blockchain solves a problem we do not have.

Blockchains provide decentralized consensus among mutually distrusting parties. An evidence bundle has a single author (the GuardSpine instance that created it) and one or more signers. There is no consensus problem. There is a verification problem.

A hash chain gives us tamper evidence. Signatures give us authenticity. RFC 8785 gives us deterministic serialization. Together, they let any verifier independently confirm the bundle’s integrity. We do not need proof of work, proof of stake, or a distributed ledger to accomplish this.

The result is a bundle that fits in a single JSON file, verifies in milliseconds, and works offline. Try that with a blockchain.

Building Your Own Verifier

The verification algorithm is intentionally simple enough that you can implement it yourself. If you do not trust our guardspine-verify tool, write your own. The spec is open. The algorithm is documented here. The test vectors are in the guardspine-spec repository.

A minimal verifier in any language that has SHA-256, JSON parsing, and one of the four signature algorithms is roughly 200 lines of code. We have tested interoperability between the TypeScript kernel, the Python verifier, and independent implementations in Go and Rust.

That is the point. If verification depends on our code, you are trusting us. If you can write your own verifier in an afternoon, you are trusting math.

Book a call if you want to walk through the verification algorithm with your security team. I will bring test bundles.