How we make Jira history independently verifiable
"Independently verifiable" is an easy claim to make and a hard one to deliver, so this post walks through exactly how it works in the Tamper-Evident Audit Log for Jira — every primitive, every design choice, and the one file that makes the whole thing portable. None of it is exotic; the point is the combination.
Step 1 — capture: one event, one record
Every Jira workflow event — issue created, transitioned, edited, commented, attachment added, link created, worklog logged — is forwarded by Atlassian Forge to our backend at the moment it happens. The event payload (event type, issue key, changed fields, acting identity, timestamp) is canonicalized with RFC 8785 (JSON Canonicalization Scheme), so the exact byte sequence that gets signed is deterministic and reproducible by any verifier later.
Step 2 — sign: ECDSA P-256, per workspace
Each canonical payload is signed with ECDSA over the NIST P-256 curve — the same curve carrying most of TLS and SSH. Signing keys are per-workspace and live inside a KMS; the private key never leaves it. Signatures are deterministic (RFC 6979), so there's no nonce-reuse failure mode and the same input always produces the same signature.
A signature proves two things: the record's content hasn't changed since signing, and it was signed by the holder of that workspace's key. What it doesn't prove on its own is order and completeness — you could delete a whole signed record without breaking any signature. That's what the chain is for.
Step 3 — chain: SHA-256 linkage, append-only
Every entry embeds the SHA-256 hash of the previous entry's signed payload. The first entry chains to a 32-byte zero sentinel; everything after links to its predecessor.
The consequences fall out mechanically:
- Edit any past entry → its payload no longer matches its signature.
- Delete any entry → the next entry's
prev_hashpoints at nothing. - Reorder or insert → the linkage breaks at the seam.
The chain head therefore commits to the entire history — every verification of the latest entry implicitly checks the shape of everything before it. The table is append-only by design; even legally required redactions are appended as marked entries rather than edits, so the chain never silently rewrites.
Step 4 — timestamp: an external witness for "when"
"When" is the field people argue about after an incident, and the operator's own clock is the wrong witness. So every entry is sent (as a hash — the TSA never sees content) to a public RFC 3161 Timestamp Authority pool (DigiCert primary, FreeTSA failover), which returns a signed timestamp token binding this record existed in exactly this form at this time to an independent third party's clock and key.
These per-event tokens are non-qualified, and we label them as such everywhere — an honest distinction that matters to some buyers and not to others. The TSA's certificate chain is captured at signing time, so tokens stay verifiable years later, after certificates rotate.
Step 5 — verify: one HTML file, zero trust in us
Everything above would still require trusting our servers at verification time — unless verification leaves the building. So every export bundle contains:
- the signed entries and their canonical payloads,
- the signature and timestamp tokens, with certificate material,
- a manifest, and
verify.html— a single self-contained HTML+JavaScript file.
Open verify.html in any modern browser — on a laptop with Wi-Fi off, if you like — drop
the bundle in, and it re-runs the whole pipeline: recomputes canonical payloads, checks
every ECDSA signature via WebCrypto, re-walks the hash chain link by link, validates the
RFC 3161 tokens. Green means the math checked out. It makes zero network requests,
and the verification logic is plain readable JavaScript, because "trust me, the verifier
says it's fine" would defeat the entire point.
You don't have to take that on faith either: there's a live verifier and a real sample bundle on our site — a 12-entry signed chain you can verify, then corrupt a byte and watch fail.
Why the combination matters more than any piece
Each primitive alone has a known gap: signatures don't prove completeness, chains without signatures can be regenerated wholesale by whoever holds the store, timestamps without chaining prove a record existed but not that it's all the records, and all three together still require trusting the vendor's verification service — unless the verifier is portable and offline.
Signing + chaining + external time + portable verification close each other's gaps. That's the whole trick — decades-old cryptography, assembled so that the person relying on the record needs to trust nobody, including us. The free tier ships all of it intact: we limit retention and export counts, never the cryptography.