Skip to main content

Cryptographic Verification

This page explains exactly how the Tamper-Evident Audit Log signs, timestamps, and hash-chains audit entries — and how to verify them independently.

This is the differentiating section of Attestsys documentation. The combination of per-entry signing, hash chaining, RFC 3161 timestamping, and portable offline verification is unusual on the Atlassian Marketplace — most "audit log" apps stop at displaying history.


Overview of the pipeline

When a Jira event is captured, the following pipeline runs:

Jira event
→ Canonical JSON serialisation (RFC 8785 JCS)
→ RFC 3161 timestamp request (tier-routed TSA)
→ Chain head lock (per-tenant, serialised)
→ Signed payload assembly
→ ECDSA P-256 signature
→ Persist to hash-chained audit table

Each step is deterministic and auditable. The signed payload is what gets hashed into the next entry's prev_hash field, creating an unbroken chain.


Canonical JSON (RFC 8785 JCS)

Before signing, the event payload is serialised to Canonical JSON per RFC 8785 JSON Canonicalization Scheme (JCS). This ensures that the same logical payload always produces the same byte sequence, regardless of key ordering or whitespace — making signatures deterministic and reproducible.

The implementation uses io.github.erdtman:java-json-canonicalization:1.1 (pinned). Changing this library version would be chain-breaking — it would invalidate verification of all prior entries.


ECDSA P-256 signing

Each audit entry is signed using ECDSA on the NIST P-256 elliptic curve (also known as secp256r1 / prime256v1) — the standard curve used in TLS, SSH, and EU eIDAS qualified electronic signatures.

Key properties:

  • RFC 6979 deterministic signing — given the same key and message, the signature is always the same. This eliminates the risk of nonce reuse that affects standard randomised ECDSA
  • DER-encoded signatures — stored as standard ASN.1 DER for interoperability with third-party verification tools
  • Per-tenant key pairs — each Jira workspace (tenant) has its own ECDSA key pair. Keys are generated on tenant provisioning and stored with AES-256-GCM envelope encryption at rest
  • GDPR key erasure — if a tenant requests erasure, the private key is tombstoned (set to NULL). The chain entries remain but can no longer be signed with new entries under that key. A tombstone audit entry is appended to preserve chain continuity before key erasure

The public key for each tenant is included in every export bundle in PEM format, enabling independent verification without contacting Attestsys.


RFC 3161 trusted timestamping

Timestamping has two layers. Every audit entry is timestamped at ingest by an independent Timestamp Authority (TSA); paid tiers additionally anchor the whole audit chain to a qualified TSA once per day.

LayerTierTSAStatus
Per-event (every entry, at ingest)All tiersPublic RFC 3161 TSA pool (FreeTSA, DigiCert, DFN-Verein)Non-qualified. Not listed on any EU national Trusted List. Cryptographically sound but not QTSP-backed. Clearly labelled NON_QUALIFIED in every entry and export.
Daily qualified checkpoint (one per day over the chain head)Standard, AdvancedEU Trusted List QTSP (e.g. QuoVadis Trustlink B.V. NL, GlobalSign nv-sa)QTSP-backed qualified timestamp anchoring every entry up to the checkpoint. Listed on EU national Trusted Lists.
Per-event qualifiedEnterpriseTenant-selectable EU Trusted List QTSPEach entry individually qualified-timestamped, with choice of QTSP and jurisdiction.

Because each entry's prev_hash folds the entire history into the chain head, one qualified timestamp on the head proves every prior entry existed and is unaltered as of the checkpoint — the hash chain itself is the inclusion proof. The TSA response (a DER-encoded TimeStampToken) is stored alongside the signed entry; checkpoint tokens are stored in the bundle's checkpoints/ folder. Verification tools can check every TSA certificate chain independently using the tsa-certs/ and checkpoints/ folders in the bundle.

Pending TSA marker

If the TSA is temporarily unreachable, the entry is stored with a PENDING_TSA_MARKER sentinel in place of the timestamp token. A background retry job resolves pending entries asynchronously. Pending entries show as ⚠️ Pending in the issue panel until resolved. The chain is not broken by pending entries — the sentinel value is a fixed 32-byte SHA-256 hash of the ASCII string "ATTESTSYS_PENDING_TSA_V1", which is deterministic and documented.


Hash chain

Each audit entry contains a prev_hash field — the SHA-256 hash of the previous entry's signed payload (the canonical JSON bytes that were ECDSA-signed). The genesis entry (the first entry in a tenant's chain) uses a fixed 32-byte all-zeros genesis value.

This creates a linked chain: modifying any past entry changes its signed payload, which changes its SHA-256 hash, which breaks the prev_hash of the next entry, propagating a detectable break through every subsequent entry to the chain head.

Chain verification:

  1. Reads all entries in sequence order
  2. Recomputes the SHA-256 hash of each entry's signed payload
  3. Compares it to the prev_hash stored in the next entry
  4. Verifies the ECDSA signature of each entry's signed payload against the tenant's public key
  5. Accumulates all failures — does not short-circuit at the first broken link

A chain that is fully intact with no failures is reported as INTACT. Any failure is reported with the entry ID, position in the chain, and the type of failure (hash mismatch, signature invalid, or both).


What happens to purged entries

On the free tier, audit entries older than 30 days have their original event payload bytes removed by an hourly backend retention scheduler. The cryptographic envelope of each entry — ECDSA signature, hash-chain prev_hash, RFC 3161 timestamp token, signing-certificate snapshot, and the to_sign_digest (the 32-byte SHA-256 the tenant key actually signed) — is retained forever. A signed retention-marker entry is appended to the chain on every non-empty purge run, recording the cutoff timestamp and count of entries whose payloads were removed.

Chain verification still succeeds for purged entries:

  • prev_hash continuity is unaffected — the audit-chain row itself is never mutated, only its sibling payload row is deleted.
  • ECDSA signature verification uses the stored to_sign_digest directly, so no payload bytes are needed.
  • TSA imprint cross-check is skipped for purged entries (the imprint compares against SHA-256(payload) which is gone), but the TSA token itself remains in the bundle for external inspection.

In export bundles, purged entries appear in manifest.json with payloadPurged: true and no entries/<id>.payload file. The included verify.html recognises these and shows them with a "🚫 Payload purged · retention policy" badge while still verifying their signature and chain linkage.


Offline verification with verify.html

Every export bundle includes a verify.html file — a self-contained verifier that runs entirely in the browser.

To verify:

  1. Unzip the bundle to a local folder
  2. Open verify.html in any modern browser
  3. No network requests are made — the verifier reads only the local bundle files
  4. The verifier performs the same chain and signature checks as the backend verifier

What the verifier checks:

  • ECDSA P-256 signature validity for each entry (using the included public key)
  • SHA-256 hash chain linkage for each entry
  • Presence and structure of the RFC 3161 timestamp token for each entry

What the verifier does not check offline:

  • TSA certificate revocation (requires network access to OCSP/CRL endpoints)
  • TSA Trusted List status (requires network access to EU Trusted List)

For a full legal authentication context, TSA revocation and Trusted List status should be checked by a qualified professional at the time of verification.


CLI verification (planned)

:::note Roadmap — not yet available Today, offline verification ships as the self-contained verify.html above, which already runs in CI (headless browser) as well as interactively. A dedicated command-line verifier is planned for fully scripted pipelines; the interface below is indicative and may change. :::

The planned command-line verifier:

# Verify a specific entry by ID
attestsys verify --entry-id <entry-id>

# Verify an entire exported bundle
attestsys verify --bundle path/to/bundle.zip

It will perform the same verification steps as verify.html and suit scripted verification in CI/CD pipelines.


Security properties summary

PropertyValue
Signing algorithmECDSA P-256 (NIST secp256r1), RFC 6979 deterministic
Signature encodingDER (ASN.1)
Canonical JSONRFC 8785 JCS
Hash algorithm (chain)SHA-256
Timestamp protocolRFC 3161
Per-event TSA (all tiers)Public RFC 3161 TSA pool — FreeTSA / DigiCert / DFN (non-qualified)
Paid tier TSA (primary)QuoVadis Trustlink B.V. NL (QTSP, EU Trusted List)
Paid tier TSA (secondary)GlobalSign nv-sa BE (QTSP, EU Trusted List)
Key storageAES-256-GCM envelope encryption at rest
InfrastructureHetzner Cloud, Nuremberg DE (EU only)