Skip to content

Verifying an evidence pack

Who this guide is for

This guide walks an auditor, a counterparty or any third party through verifying an Exedra Gate evidence pack independently: no account, no network access to Exedra Gate, and no proprietary software. Everything in a pack uses standard formats, SHA-256 for hashes, Ed25519 for the signature and RFC 3161 for the timestamp token, so every check below runs with ordinary tooling: a shell, jq, openssl, and Python 3 with the cryptography package for the signature step.

Verification is offline by design. The pack is self-contained: the public key, the key id, the checksums and the timestamp token are inside it.

What is in a pack

An envelope evidence pack is a folder of five files:

FileWhat it is
evidence-pack.jsonThe complete signing record: envelope metadata, signer records, document hashes and the ordered audit trail
evidence-pack.pdfA human-readable rendering of the same record
compliance-summary.pdfA compliance summary
manifest.jsonChecksums of the other files, the pack hash, the Ed25519 signature and the timestamp result
tsa.tokenThe raw RFC 3161 timestamp token, present when timestamping succeeded

Case packs are a single JSON artefact with the same signature discipline; they carry a signing_provenance block naming which key signed and under whose custody.

Step 1: check the file hashes

Compute the SHA-256 hash of each file and compare it with the checksum recorded in the manifest. Run this in the pack folder:

Terminal window
jq -r '.checksums' manifest.json
shasum -a 256 evidence-pack.json evidence-pack.pdf compliance-summary.pdf

Each computed hash must exactly match the corresponding manifest value: json_hash for evidence-pack.json, pdf_hash for evidence-pack.pdf and compliance_pdf_hash for compliance-summary.pdf. If tsa.token is present, check it too:

Terminal window
jq -r '.checksums.tsa_token_hash' manifest.json
shasum -a 256 tsa.token

A mismatch on any file means the file has been altered or corrupted since the pack was generated, and the rest of the verification cannot be trusted for that file.

Step 2: check the signature

The pack hash is signed with Ed25519. The signature block appears identically in manifest.json and evidence-pack.json: signature.value is the signature, signature.signed_content is the pack hash that was signed, and signature.public_key is the 32-byte public key, all Base64URL encoded. This script verifies it:

Terminal window
python3 - <<'EOF'
import base64, json
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
def b64url(s):
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
sig = json.load(open("manifest.json"))["signature"]
key = Ed25519PublicKey.from_public_bytes(b64url(sig["public_key"]))
key.verify(b64url(sig["value"]), sig["signed_content"].encode())
print("Ed25519 signature verifies over pack hash", sig["signed_content"])
EOF

An exception means the signature does not verify. Also confirm that signature.signed_content equals the pack_hash field in both files.

Verifying against the embedded key proves internal consistency. To go further, pin the key: signature.key_id names the signing key record, and you can ask Exedra Gate, or the client for a delegated key, to confirm the public key for that key id out of band. For a pack signed under signing delegation, the key is the client's own: the pack still verifies the same way, but key custody is the client's, under their controls, and the platform's attestation covers the event chain, the pack hash and which key identifier signed, not how that key was managed.

Step 3: check the independent timestamp

When tsa.status is ok, the pack carries an RFC 3161 timestamp token from an independent timestamp authority. The token covers tsa.timestamped_hash:

Terminal window
TS_HASH=$(jq -r '.tsa.timestamped_hash' manifest.json)
openssl ts -verify -digest "$TS_HASH" -in tsa.token -CAfile tsa-ca.pem

Expected output: Verification: OK. The tsa-ca.pem file is the certificate chain of the timestamp authority; the token itself embeds the chain (it is requested with certReq), and you can inspect it with openssl ts -reply -in tsa.token -token_in -text. The reference verifier ships a trust-anchor bundle for the default authority.

Read this carefully before comparing hashes: tsa.timestamped_hash is a pre-manifest hash, taken before the PDFs and the TSA result were written into the manifest, so it does not equal the final checksums.manifest_hash, by construction. The binding of the timestamped hash to this pack rests on the fact that the pre-manifest already contained the pack hash, the Ed25519 signature block and the JSON hash.

What the timestamp establishes: the timestamped content existed at the token's time, on the authority of the timestamp authority's clock, independent of Exedra Gate's clock. The token stays verifiable even if the pack signing key were later compromised. A non-qualified timestamp is admissible evidence, not presumed accurate; under eIDAS the legal presumption attaches to a qualified timestamp only, and Exedra Gate does not claim one for the default token.

Step 4: walk the audit chain

The audit trail in evidence-pack.json is hash chained: every event carries an event_hash and the previous_hash of the event before it, with the first event at null or the literal GENESIS. Walk it:

Terminal window
python3 - <<'EOF'
import json
events = json.load(open("evidence-pack.json"))["audit_trail"]
prev = None
for e in events:
if prev is None:
assert e.get("previous_hash") in (None, "GENESIS"), "first event is not genesis"
else:
assert e.get("previous_hash") == prev, f"chain breaks at sequence {e['sequence_number']}"
prev = e["event_hash"]
print(f"hash chain intact across {len(events)} events")
EOF

A broken link dates and places the alteration: the chain fails at the exact event where an entry was edited, inserted or removed. The pack also carries the generator's own chain_verification summary; recompute rather than trust it.

One boundary to know: the pack records its own generation as a final evidence_pack_generated event, appended after signing, because its payload contains the pack hash and therefore cannot be inside the signed content. The signed prefix is declared in chain_snapshot.event_count: the rendered trail has one more event than the pack hash covers, and that is expected, not a defect.

Step 5: recompute the pack hash

The pack hash is a SHA-256 over a canonical JSON string with a fixed literal key order:

{ envelope_id, created_at, documents: [document hashes],
signers: [signer emails in sort order],
audit_events: chain_snapshot.event_count,
last_event_hash: hash of the last event inside that prefix }

Recompute it from the data in evidence-pack.json, using the chain_snapshot boundary from step 4, and compare with pack_hash in both files. Determinism comes from the fixed key order, not from key sorting. The pack's own verification.instructions field names the exact field list for its pack type, and the reference verifier implements the recompute for both envelope and case packs, so the practical path is to run it and read its per-check output. Packs generated from placeholder data carry test_mode: true inside the hashed content, so a stripped or edited flag breaks the manifest hash and the timestamp rather than passing quietly.

Verifying the access-event ledger

Client workspaces also keep a second hash-chained record, the access-event ledger: IP sign-in denials, network rule changes, enforcement toggles and sovereign module switches. It uses the same primitives as evidence packs. Given the rows for one workspace ordered by sequence_number ascending, the walk is:

  1. Check continuity: the first row's prev_record_hash is the literal GENESIS, and every later row's prev_record_hash equals the previous row's record_hash.
  2. Recompute each record_hash and compare. Two canonicalisation versions exist. v1, used by rows written before the 2026-08-25 cutover, hashes only the six identifier fields as compact JSON in fixed alphabetical order: client_id, event_type, occurred_at, prev_record_hash, ref_id, sequence_number. v2, used by every row from the cutover on, hashes the full evidentiary row: the six v1 fields plus event_data, actor_id, actor_email, ip_address and user_agent, as canonical JSON with recursively sorted keys, no insignificant whitespace, absent optional fields as explicit null, and the marker canon_version: 2 inside the hashed bytes. A verifier detects the version by recompute and reports which one matched.
  3. Apply the monotonic-version rule: once one row in a chain verifies as v2, every later row must verify as v2. This blocks the downgrade attack in which a tampered v2 payload is re-presented so that only its v1 fields verify.
  4. Where signature is present, verify it as Ed25519 over the record_hash string against the platform public key, exactly as in step 2.
  5. Where tsa_timestamp is present, decode the Base64 token and verify it as an RFC 3161 token over record_hash, exactly as in step 3.

A row with a null signature or tsa_timestamp is a flagged degraded record, not a chain break: the ledger write must never gate a security decision, so signing and timestamping on this rail are best effort and the hash chain is the load-bearing tamper evidence. A degraded row is visible as such; a rewritten row breaks the chain.

What verification establishes

EstablishedBy
Alteration of the pack after generation is detectableEd25519 signature and file checksums
The pack existed at a specific timeRFC 3161 timestamp, when tsa.status is ok
Alteration of audit events is detectableSHA-256 hash chain
Document content matches its recorded hashThe document hash fields
The signing sequence is complete and orderedSequence numbers in the chained trail
Which key identifier signed the packsignature.key_id plus the custody record

The honest summary: verification establishes existence and integrity at a point in time. The record as it stood is reconstructable, and a record altered afterwards stops verifying.

What verification does not establish

It does not establish that the contents of a signed document are true, that a signer understood what they signed, or that anyone was legally entitled to sign. Signer identity assurance depends on the identity checks that were configured, which the record describes but the mathematics does not prove. For a pack signed under delegated key custody, verification does not establish how the client generated, stored, rotated or destroyed their key. The pack is a record of what happened, not a judgement about it.

The reference verifier

Exedra Gate publishes a reference offline verifier, exedragate-verify, a Deno command line tool that runs eight independent checks with no network and no database: pack hash recompute, Ed25519 signature, RFC 3161 token, hash format and test_mode consistency, the self-attestation event, attribution blocks, a PDF cross-check and dual-key attestation re-verification. Exit code 0 is a pass, 1 is a failed check, 2 is a fail-closed condition or tool error. --keys-file pins verification to known key ids, and --ca-bundle pins the timestamp trust anchors. Using it is optional: every check it automates is one of the manual steps above.