Skip to content

Engineering blog

Keyless witness logs with Merkle checkpoints and WebAuthn

A log signed by the party being audited is tamper-evident, not non-repudiable. Client-side signatures plus externally anchored Merkle roots close the gap.

When an auditor asks for proof that a critical action was authorized, most organizations produce a row from PostgreSQL or Elasticsearch. That row proves a record exists in a table. It does not prove the record is authentic, unaltered, or signed by an authorized human.

If a database administrator or an attacker with root gains access, they can update or delete log rows without leaving a trace. And even a perfectly maintained log has a structural weakness: the party attesting to its integrity is the party being audited.

The keyless witness pattern

To build an audit trail that is mathematically tamper-evident, the logging system must be unable to forge records even if it wants to. Three primitives compose to get there.

  1. Client-side WebAuthn signatures. Every authorization is signed directly by the approver's hardware authenticator using ECDSA P-256. INTYGA never possesses the private key and cannot generate a valid signature.
  2. Canonical payload hashing. The signature binds the exact hash of action, parameters, requester, nonce, and expiry — serialized deterministically — which defeats both replay and parameter tampering.
  3. External Merkle anchoring. Log ranges are hashed into a Merkle tree, and roots are anchored to independent transparency logs, freezing history against later rewriting.

Why the anchor has to be someone else's

A hash chain you sign yourself detects accidental corruption and third-party tampering. It does not constrain you. If the same organization holds the log and the signing key, then a sufficiently motivated version of that organization can produce a consistent alternative history.

This is why INTYGA anchors checkpoint roots to independent issuers — Sigstore Rekor and RFC 3161 timestamp authorities — and requires a quorum of at least two distinct external witnesses in production. An anchor INTYGA operates does not count toward that quorum, because an endpoint the vendor controls is not an independent witness. If two trusted anchors ever report divergent roots for the same checkpoint, verification returns INVALID rather than picking a winner.

Offline verification, without a vendor account

import { verifyApprovalReceipt } from "@intyga/verify";

// Fully offline. No network call, no INTYGA secret, no account.
const check = verifyApprovalReceipt(receipt, {
  target: "prod-db-cluster-01",                  // this execution environment
  actionType: "wipe_production",
  params: { target: "prod-db-1", region: "eu-north-1" },
  nonce,                                          // the challenge you issued
  approvers: { dids: TRUSTED_APPROVER_DIDS, resolveKey },
}, {
  expectedOrigin: process.env.INTYGA_WEBAUTHN_ORIGIN!,
  expectedRpId: process.env.INTYGA_WEBAUTHN_RP_ID!,
});

if (!check.ok) throw new Error(`Not a valid human approval: ${check.reason}`);

Note the shape of that API. It does not ask "is this signature well-formed?" — it asks "did a human sign exactly this instruction?" You supply the expected action; the library recomputes the canonical payload and rejects any mismatch. A signature you cannot bind to a specific instruction is just an expensive session token.

The approvers argument is the one with no default, and that is deliberate. Left out, the only key available to check the signature would be the one travelling inside the receipt — which proves the receipt agrees with itself and nothing more. Whoever hands you a receipt could have generated that keypair a second ago. The approver key has to come from your side: a pinned enrollment record, a directory lookup, an allowlist you control. The library refuses to verify without one rather than accepting a default that would quietly mean nothing.

Proving nothing was deleted

Signature verification proves a record is authentic. It says nothing about whether a record was silently removed. Completeness is a separate property, and it needs a separate mechanism: a gapless per-tenant sequence so a deletion leaves a detectable hole, and inclusion proofs checked against daily Merkle roots.

npx @intyga/sdk audit-verify ./intyga-audit-proof-seq-1234.json --roots roots.jsonl
# exit 0 = verified, 1 = not verified

Be precise about the boundary here, because it is easy to overclaim. This detects modification and deletion of events that were committed. It does not detect an event that was never committed in the first place. Withholding is a different threat, and the honest answer is that a witness log constrains what can be changed after the fact, not what can be omitted before it.

Why the verifier is open source

An offline witness guarantee is meaningless if the verification code is a proprietary black box. @intyga/verify is Apache-2.0 and depends on nothing but Node's built-in crypto — under 2,000 lines for receipt verification, under 4,000 including the ledger, anchor-quorum and evidence-bundle code. That is more than a single sitting, but it is auditable source with no transitive surface behind it, and you are being asked to trust it, so it must stay that way.

The dependency-free property has a limit worth stating: it uses node:crypto, which makes it Node-only rather than browser-portable. Better you learn that here than in a comment thread.

PropertyTraditional DB audit logKeyless witness log
Non-repudiationNone — an admin can insert rowsECDSA P-256 signature from the approver's own key
History rewritingTrivial via UPDATE / DELETEConstrained by externally anchored Merkle roots
Third-party verificationRequires database accessOffline, via an open-source verifier
Private key riskCentralized key storageApproval key is never exposed to INTYGA