qodiqa
SpecificationRun a simulation

Product · Anchors

History that outlives the host.

An anchor pins a signed checkpoint of the gateway's audit chain in a place the gateway host does not control. Rewrite the past afterwards, and the pinned copy no longer matches.

This holds when the witness and the anchor copy are kept by someone other than whoever runs the gateway.

  1. The host appends entries to its log and computes a root over them at each interval.
  2. A signed checkpoint is copied to independent witnesses and to anchors outside the host.
  3. If the host later rewrites its history, a verifier compares the log with the kept copies and reports a mismatch. An honest history is reported as consistent.

How it works

From a log entry to a pinned checkpoint.

The audit chain and the signing keys live on the gateway host. On their own they cannot show that the host did not rewrite its own past. Anchoring moves a proof of the past outside the host.

  1. The gateway appends each decision to its audit chain and to the transparency log.
  2. It signs a checkpoint of the chain at regular positions.
  3. Witnesses co-sign a statement about the checkpoint with their own keys.
  4. The statement is appended to a git repository as a commit; OpenTimestamps and RFC 3161 are optional extras.
  5. A verifier checks inclusion and consistency against that outside copy.

Witnesses

A witness is a separate signer with its own key and its own append-only log. It refuses a lower position, a different head at a position it has already seen, and a forward jump that does not chain to the last checkpoint it signed.

Off by default. A production boot is refused without an anchor export, a witness and the witness public keys.

External anchors

The anchor statement is appended to anchors/<UTC date>.jsonl in a git working tree outside the gateway's control. The gateway never pushes: publishing the repository is the operator's job.

OpenTimestamps and RFC 3161 receipts come from executables the operator provides. All three kinds are off unless configured.

Never in the way

Anchoring runs in the background. A failed anchor is retried every minute, counted and logged, and marked failed after ten attempts. It never refuses a request or a checkpoint.

At most one checkpoint per 24 hours is anchored externally by default; the shutdown checkpoint always is.

What an anchor proves

A checkpoint with this hash existed no later than the commit, and it has not been silently replaced since.

What it does not prove

That earlier entries were honest, or anything written after the latest checkpoint. A witness on the gateway's own host adds nothing against that host. The commit time is the committer's claim.

Anatomy

What an anchor statement contains.

One short line per anchored checkpoint. It is hashed, not signed: it is bound to a signed checkpoint through that checkpoint's own hash. Pick a field to read what it does.

digest
SHA-256 over the tag QODIQA::EXTERNAL_ANCHOR::V1 and the canonical statement.
checkpointId, payloadHash, chainLength, headHash, createdAt
Identify the signed checkpoint, its position and its newest entry.

Consistency

Why tampering becomes detectable.

A log only ever grows. A consistency proof shows that a newer signed root contains the older one unchanged. Edit one old entry and no such proof exists.

A consistency proof (RFC 6962) is a short list of hashes showing that the tree behind an older root is a prefix of the tree behind a newer root. If an old entry changed, the proof cannot be built and the check fails.

Verify

How to verify an anchor.

The verifiers import nothing from the gateway. Give the proof verifier a clone of the anchors repository and it takes its keys and anchored checkpoints from there instead of from the proof.

Three commands, six checks.

Run them from a clone of qodiqa-verify or qodiqa-anchors.

Show the commands
Terminal
node bin/qodiqa-proof-verify.js --proof examples/proof/proof.example.json
node bin/qodiqa-proof-verify.js --proof examples/proof/proof.tampered.example.json
node bin/qodiqa-proof-verify.js --proof proof.json --anchors-repo <clone of qodiqa-anchors>
Terminal
node scripts/audit-repro/verify/qodiqa-audit-verify.js \
  --audit audit.log --checkpoint-keys checkpoint-keys.json \
  --anchors anchors.jsonl --witness-keys witness-keys.json \
  --checkpoints audit.log.checkpoints.jsonl
Terminal
npm test

The first two lines run on the example proofs in that repository; the third uses your own clone of the anchors repository.

Validates the structure of every file under anchors/ and keys/ and that positions never go backwards.

What is checked

  1. The repository. Clean git tree, linear history, no merge commits.
  2. Append-only. No file deleted or renamed; keys never change.
  3. Signed commits. Optional, with --require-signed.
  4. Each line. Exactly the statement fields, and a matching digest.
  5. Order. Positions and times never go back; no forks.
  6. The checkpoint. A valid Ed25519 signature, and an anchor that covers it.
  • 0 VALID
  • 3 VALID_PARTIAL
  • 1 INVALID
  • 2 usage or input error

Partial result. OpenTimestamps and RFC 3161 receipts are reported as not evaluated.

Specification

Formats, algorithms, defaults.

Everything here is optional unless the row says it is required. Names in code are the gateway's configuration variables.

Formats
Anchor lineanchors/YYYY-MM-DD.jsonl, one line per anchor: { "digest", "statement" }, appended and never edited
Statement{ v: 1, checkpointId, payloadHash, chainLength, headHash, createdAt }, with createdAt in UTC ISO 8601
Public keyskeys/checkpoint-keys.json and keys/transparency-keys.json, each { v: 1, keys: [ { keyId, algorithm, publicKey, status } ] }; append-only
Algorithms
SignaturesEd25519 over a 32-byte digest, base64 of 64 bytes, public keys as SPKI
HashingSHA-256 with a domain tag in front: QODIQA::AUDIT_CHECKPOINT::V1, QODIQA::EXTERNAL_ANCHOR::V1, QODIQA::WITNESS_COSIGN::V1
Transparency logRFC 6962 Merkle tree, with inclusion proofs and consistency proofs between two signed roots
Anchor types
Git commitSet QODIQA_ANCHOR_GIT_REPO to a working tree outside the gateway's control. Off when unset. QODIQA_ANCHOR_GIT_SIGN=true signs the commit.
OpenTimestampsSet QODIQA_ANCHOR_OTS_EXEC to an executable that stamps the digest. Off when unset. The gateway contacts no network itself.
RFC 3161Set QODIQA_ANCHOR_TSA_EXEC to an executable that returns a timestamp token. Off when unset.
IntervalQODIQA_ANCHOR_EXTERNAL_INTERVAL_MS, default 86400000 (24 hours), allowed 1000 to 604800000
Checkpoints and witnesses
Checkpoint cadenceDefaults: every 1,000 entries or 15 minutes (QODIQA_AUDIT_CHECKPOINT_EVERY, QODIQA_AUDIT_CHECKPOINT_MAX_AGE_MS)
Witness modeQODIQA_WITNESS_MODE: none (default), local or exec. Several witnesses can be listed in a file named by QODIQA_WITNESSES_CONFIG.
QuorumQODIQA_ANCHOR_MIN_WITNESSES: 2 when two or more witnesses are configured, otherwise 1. Below it the checkpoint is held and exported later, in order.
Customer-held exportQODIQA_ANCHOR_EXPORT_PATH, an append-only JSONL file of checkpoints with their cosignatures
ProductionRequired: an anchor export, a witness and the trusted witness public keys. A production boot without them is refused. Git, OpenTimestamps and RFC 3161 are not required.
Limits stated by the repositories
IndependenceWhether a witness is independent of the operator is a deployment fact the software cannot verify. In local mode the gateway host holds the witness key.
ReceiptsThe proof verifier does not evaluate OpenTimestamps or RFC 3161 receipts.
Consistency proofsServed by the gateway at GET /transparency/consistency/:fromRootId/:toRootId, capped at 250,000 entries by default

How anchoring worksConfiguration referenceVerifiersqodiqa-anchorsqodiqa-verifyVerify a receipt in the browser