Audit log

Hash-chained, RFC 3161 trusted-timestamped, signed-receipt verifiable. Per-tenant. Offline-verifiable. Regulator-ready.

What it is

Every operation against your deny.sh tenant (encrypt, decrypt, vault store/retrieve, deadswitch state transition, key issuance) is appended to a per-tenant hash chain. Each entry commits to the previous entry's hash, so any tampering breaks the chain at the mutated row and every subsequent row.

Each entry is independently witnessed by one or more RFC 3161 Trusted Timestamp Authorities (TSAs). The TSA signs a token over the entry's hash plus the TSA's clock time. We persist that token. You can extract it later as a signed receipt to prove that the entry existed at or before the timestamped moment, without needing to trust deny.sh as the witness.

The construction

For each operation we record:

entry_hash = sha256(
  prev_hash       ||  // chain link
  tenant_id       ||  // your api-key hash, not the raw key
  op_type         ||  // 'vault.store', 'encrypt', etc.
  canonical(payload) ||  // sorted-keys JSON
  created_at         // unix epoch seconds
)
prev_hash for entry 1 = sha256(tenant_id || "GENESIS")

Then a background worker sweeps unsigned entries every 60 seconds and asks one or more TSAs to sign the entry_hash. The signed token is persisted alongside the entry. Multiple TSAs are supported for redundancy (FreeTSA primary, DigiCert paid backup) so the chain remains verifiable even if one TSA later goes offline.

The receipt artefact

A receipt is the customer-handable proof for a single entry. Fetch one over the API:

GET /v1/audit/receipt/<entry_id>        # JSON
GET /v1/audit/receipt/<entry_id>.pdf    # branded PDF
Authorization: Bearer <your api key>

The receipt is the entry's metadata plus all its TSA tokens. It deliberately does not include the operation's payload, only sha256(canonical(payload)), so you can hand a receipt to a regulator or auditor without leaking what the operation actually did.

Tenant id is one-way redacted in the receipt: tenant_id_hash_redacted = sha256(tenant_id || "receipt-v1"). Stable per tenant, opaque to readers.

Verifying a receipt offline

The deny CLI ships a verifier. It walks the embedded RFC 3161 token, recomputes the TSA's declared digest of TSTInfo (sha256, sha384, or sha512 per the SignerInfo digest algorithm), checks it matches the signed messageDigest attribute, and verifies the SignerInfo signature against the TSA signing certificate embedded in the token. No network required.

$ deny verify-receipt receipt.json

  entry id           1042
  op type            vault.store
  entry hash         8e34b91d…
  created at         2026-05-18T17:33:07.000Z
  chain length       1
  TSA tokens         1

  freetsa  (granted)
    ✓ messageImprint matches entry_hash
    ✓ signedAttrs.messageDigest matches sha256(TSTInfo)
    ✓ SignerInfo signature verifies
    ✓ genTime in plausible range

  OK — receipt verifies against at least one granted TSA token.

Exit code 0 on OK, 1 on TAMPERED, 2 on bad input. The --json flag emits a machine-readable result for piping into compliance dashboards.

What this proves, and what it does not

The chain proves that entries cannot be silently mutated after the fact, and the TSA timestamps prove an external party (the TSA, not deny.sh) saw each entry's hash at or before the recorded time. A receipt with a granted token from a public TSA like FreeTSA or DigiCert is third-party verifiable: the regulator does not need to trust us as the witness, they trust the TSA's published cert and signature.

What it does not prove: that the operation's payload was what you say it was. The chain commits to sha256(canonical(payload)), not the payload itself; we never see the plaintext of your secrets. If you need stronger evidence of the operation's contents, the customer-side application should record the canonical payload locally and prove it matches the receipt's op_payload_hash field.

Regulator-grade verification via openssl

The CLI's bundled verifier is the convenience path. For regulator-grade verification against an externally trusted PKI, the deny CLI can export the receipt as a bundle that openssl ts -verify reads natively:

deny verify-receipt receipt.json --export-openssl ./bundle
bash ./bundle/verify.sh

The bundle contains the raw .tsr TimeStampResp bytes per TSA, the message-imprint hex, the FreeTSA CA chain (bundled with the CLI; pinned to a known root fingerprint), and a one-liner verify.sh that runs openssl ts -verify for every token. Exit 0 = standard RFC 3161 PKI validation succeeded against the bundled root. The regulator can substitute their own trusted CA bundle by setting the relevant *_CAFILE env var or replacing freetsa-cacert.pem with their own.

Limits, retention, and trust model

Chain entries are retained indefinitely. TSA tokens carry their own genTime + signed certs and remain verifiable as long as the TSA's signing certificate chain remains accepted by your local trust store. The deny CLI ships with the FreeTSA root CA bundled (pinned at install time) so the --export-openssl flow works out of the box; the convenience verifier deliberately stops at "signature valid against the cert embedded in the token" and defers full PKI-root validation to the openssl path so a single trust decision (which root CAs to honour) lives in one place: the regulator's own trust store.

Signed receipts are a paid-tier feature (Dev / Pro / Lifetime / Enterprise). The hash chain itself is recorded for every tenant from day one; the API surface that issues regulator-handable receipts is gated.

Get started

The audit chain is recorded automatically on every paid-tier operation. To pull a receipt:

curl -H "Authorization: Bearer $DENY_API_KEY" \
  https://deny.sh/v1/audit/receipt/<entry_id> > receipt.json

deny verify-receipt receipt.json

Questions, integrations, custom retention SLAs, or auditor walkthroughs: hello@deny.sh.