N° 0026 · Trust Architecture
How verification works.
KairoNull evidence records are verifiable without access to KairoNull systems. This page documents the exact methodology. Every step can be reproduced independently using standard cryptographic tools available on any operating system.
What a KairoNull record contains
Verification methodology
Every command and output below was run against the downloadable sample evidence package. The sample is synthetic: a fictional refund-handling agent, fictional orders and a fictional approver. Its timestamp was made by a local demo authority, not an independent one, so the sample demonstrates the method and shows nothing about when its data existed. A bundle exported from the live service is verified the same way against the real authority's certificate.
Obtain the evidence record
Every KairoNull audit export includes a JSON file containing the ledger entry for each decision event. Download the sample evidence package (synthetic) to follow along.
Recompute the SHA-256 hash
The hash of an entry is SHA-256 over its canonical JSON: the fields listed in hash_spec.fields_included (plus correlation_id only when it is set), with keys sorted alphabetically at every depth and no whitespace. Recompute it yourself and compare with entry_hash. If the record has been altered, it will not match.
# Node.js, no dependencies. Prints a MISMATCH line for any entry whose content does not match its hash.
node -e '
const c=require("crypto"),fs=require("fs");
const b=JSON.parse(fs.readFileSync("sample-evidence-package.json"));
const F=b.hash_spec.fields_included;
const s=o=>o&&typeof o=="object"?Array.isArray(o)?o.map(s):Object.fromEntries(Object.keys(o).sort().map(k=>[k,s(o[k])])):o;
let bad=0;
for(const e of b.entries){
const f=Object.fromEntries(F.map(k=>[k,e[k]??null]));
if(e.correlation_id)f.correlation_id=e.correlation_id;
if(c.createHash("sha256").update(JSON.stringify(s(f))).digest("hex")!==e.entry_hash){bad++;console.log("MISMATCH",e.id)}
}
console.log(bad?bad+" entries do not match":"all "+b.entries.length+" entry hashes match their content")'
all 10 entry hashes match their contentVerify the chain link
Each record's prev_hash must match the entry_hash of the preceding record, and the first must be GENESIS for a complete chain. A mismatch means an entry was inserted, removed, replaced or reordered.
# number of entries whose prev_hash is not their predecessor's entry_hash (0 = every link holds)
jq '[.entries as $e | range(1; $e|length) | select($e[.].prev_hash != $e[.-1].entry_hash)] | length' sample-evidence-package.json
0
jq -r '.entries[0].prev_hash' sample-evidence-package.json
GENESISVerify the RFC3161 timestamp
The timestamp token does not cover a single record. It covers the root hash: SHA-256 over all entry hashes in order. Build the timestamp query from that root hash (do not use -data with the raw bytes: that hashes them a second time and can never match), then verify the token against the authority's certificate.
jq -r '.anchor.anchor_token_b64' sample-evidence-package.json | base64 -d > anchor.tsr
ROOT=$(jq -r '.anchor.root_hash' sample-evidence-package.json)
openssl ts -query -digest "$ROOT" -sha256 -cert -no_nonce -out q.tsq
# For THIS sample only: the demo authority's own certificate, shipped inside the file.
jq -r '.tsa_ca_pem' sample-evidence-package.json > demo-ca.pem
openssl ts -verify -in anchor.tsr -queryfile q.tsq -CAfile demo-ca.pem
Verification: OK
openssl ts -reply -in anchor.tsr -text | grep -E "^TSA:|Time stamp"
Time stamp: Oct 3 09:33:18 2026 GMT
TSA: DirName:/CN=KairoNull DEMO TSA (local, not an independent authority)What "OK" means here. It shows the token is internally consistent with the certificate that came in the same file. For this sample that certificate belongs to a local demo authority, so OK is not independent evidence of time. For a bundle anchored by a real authority, use the authority's own certificate that you obtained from the authority, never one supplied by the bundle. A valid token from an authority you trust shows the anchored root existed no later than the token's time; it does not show when each entry was written.
What tamper detection looks like
Change one decision in one record (here: a policy decision from deny to allow) and run the step 02 command again. The altered entry no longer matches its hash, and the open verifier names it. Re-computing that entry's hash to hide the edit breaks the link to the next entry, and re-computing every later hash no longer reproduces the anchored root.
# step 02 command on the altered copy
MISMATCH 00000000-0000-4000-8000-000000000006
1 entries do not match
# the open verifier (scripts/umbra-demo/verify.ts) on the same altered copy
FAIL Every entry hash recomputed from content (10 entries)
-> 1 altered: 000000000006
VERDICT: FAILEDDetection depends on someone recomputing the hashes, and has limits: a verifier holding only one bundle cannot tell if the newest entries were dropped and the anchor re-created afterwards, and a timestamp is only as trustworthy as the authority's certificate you verify it against.
Try the verification tool on the synthetic sample evidence package. It recomputes each record's hash from its content in your browser. No account required.
Open verification tool →See verification applied to your AI systems.
Book a technical briefing. We walk through the architecture with your specific governance context, regulatory obligations, and existing AI stack.
Book a technical briefing →