WardenAuthAgent Security
PricingDocsCompareBlogLearnChangelog
Sign inGet started free
All posts
How We Built
July 19, 2026
13 min read

How We Built Cryptographic Audit Receipts for Non-Repudiable Access Control

Every authorization decision produces a signed proof token. Here's the merkle-tree structure, the signing key rotation strategy, offline verification, and why we chose Ed25519 over RSA.


Every authorization decision in WardenAuth — allow or deny — produces a signed, cryptographic receipt. This is not a log line in a database. It is a portable proof token: a small, self-contained, signed message that anyone can verify offline without trusting us. The receipts make our audit trail non-repudiable — neither the system operator nor a user can deny that a specific decision was made at a specific time. This is how we built it.

Why a database audit log is not enough

A standard audit trail stores decisions in a database. The database operator — your infrastructure team, your cloud provider, an attacker who compromised an admin credential — can modify, delete, or fabricate entries. Even with write-once append-only semantics (which DynamoDB does not natively provide), a sufficiently privileged actor can alter the log. The fix is cryptographic signatures: each receipt is signed with a key the operator does not control, and the receipts are chained so insertion or deletion is detectable.

Receipt structure

Each receipt is a small JSON document containing the decision context and a signature:

json
{
  "v": 1,
  "ts": "2026-07-19T14:31:22.123Z",
  "sub": "user_abc123",
  "scope": "acme-corp",
  "res": "invoice",
  "act": "delete",
  "dec": "deny",
  "rsn": "permission invoice:delete → deny (role: restricted)",
  "prev": "sha256:abc123...",
  "sig": "ed25519:def456..."
}
  • v: schema version — receipts are forward-compatible
  • ts: ISO 8601 timestamp with millisecond precision
  • sub, scope, res, act: the access check parameters — who asked, in what tenant, for what
  • dec, rsn: the decision (allow/deny) and reasoning — which permissions matched or why none did
  • prev: SHA-256 hash of the previous receipt in the chain — creates a Merkle-tree-like integrity chain
  • sig: Ed25519 signature over the entire receipt (excluding the sig field itself)

Why Ed25519 over RSA

We chose Ed25519 for three reasons. First, signing speed: Ed25519 signs a 256-bit message hash in under 100 microseconds — critical because signing happens on the hot path of every authorization decision. RSA-2048 signing is 20-50x slower. Second, signature size: Ed25519 produces 64-byte signatures; RSA-2048 produces 256 bytes. Each receipt is already ~300 bytes; keeping signatures small matters at scale. Third, security: Ed25519 has no known timing side-channel attacks, making it safe to implement in a multi-tenant environment where signing keys are segregated by scope.

The Merkle chain: preventing insertion and deletion

Each receipt contains prev — the SHA-256 hash of the previous receipt in the chain for that scope. This creates an append-only, tamper-evident chain:

text
Receipt 1 → prev: null
Receipt 2 → prev: sha256(Receipt 1)
Receipt 3 → prev: sha256(Receipt 2)
...

To verify the chain, you recompute each hash and check that it matches the next receipt's prev. A missing receipt breaks the chain. An inserted receipt will not match its successor. A modified receipt changes its hash, breaking the next link. This is the same integrity mechanism that powers Git commits and blockchain — just applied to authorization decisions.

Key management: signing and rotation

Each scope gets its own Ed25519 signing key pair, generated at scope creation time. The private key never leaves the Lambda execution environment — it is stored in AWS Secrets Manager with automatic rotation every 90 days. The public verification key is published at a well-known endpoint for offline verification.

typescript
// Key rotation: new key signs new receipts; old key verifies old receipts.
// Receipts include a key ID so verifiers know which public key to use.

interface ReceiptHeader {
  v: number       // schema version
  kid: string     // key ID → which public key verifies this receipt
  alg: 'ed25519'  // algorithm identifier
}

Offline verification

The entire point of cryptographic receipts is offline verification — an auditor should be able to validate the audit trail without accessing the live system. The verification flow:

  • Fetch the public verification key for the scope (published at a known URL)
  • For each receipt, recompute the hash chain: does sha256(prev_receipt) === this_receipt.prev?
  • Verify the Ed25519 signature: does the signature validate against the public key and the receipt payload?
  • If both checks pass, the receipt is authentic and the chain is intact — no trust in the operator required

What we learned

The cryptographic signing itself was straightforward — Ed25519 libraries are mature and fast. The hard part was the chain management: what happens when the hash chain for a scope grows to millions of receipts? We added periodic checkpointing — every 10,000 receipts, a checkpoint receipt is generated that summarizes the previous 10,000. Verifiers can start from any checkpoint rather than walking the entire chain from genesis.


Back to blogTry WardenAuth free →
© 2026 ecarrizo. All rights reserved.
PricingDocsCompareBlogLearnChangelogStatusGlossaryContactTermsPrivacy