Verify a Verdifax audit bundle

Every successful (OK) Verdifax run produces a sealed audit bundle, a structured JSON record of what was recorded about the run (actor, model, decision, classification, effects) alongside cryptographic seals (manifest hash, per-artifact hashes, seal references, public-log anchor). The bundle is designed so any third party can independently confirm that the recorded evidence is internally consistent and matches the public log, without contacting the API and without any credentials. Two layers are checked separately. Recomputing the execution record and its proof from the disclosed fields needs no key, no public log and no Verdifax service. The public-log checks are a second layer, which relies on the log's key and on Verdifax's published log-signing keys. Since verifier 0.12.0, a public-log entry counts only if it is signed with a published Verdifax log-signing key and the timestamp the log signs for it verifies, so a rewritten record that anyone else logs as a new entry fails. Verdifax holds that key. From run 240 on, each record seals a 600-second anchoring window, so a later re-log, even by Verdifax, fails or must carry a new sealed date; earlier records show their authenticated anchoring time but cannot fail on it.

What the public log binds depends on when the record was sealed:

  • Successful (OK) runs from run 240 on (leaf v7) carry everything run 239 carries, below, and also seal a 600-second anchoring window in the values the first entry anchors. Both entries must have been logged within that window after the run's sealed clock, so a later re-log of a rewritten record fails, even by Verdifax, which holds the signing key; moving the sealed clock to fit changes the record's own date.
  • Run 239 (leaf v6) carries a second public-log entry, the complete-record anchor, over a digest of every section of the record, including the decision record, request details, approval chain, real-world effects and system provenance. Rewriting any section no longer matches that entry. The first entry also states that the second is required, so deleting the second entry from the bundle fails verification.
  • Run 238 carries the same complete-record anchor, but its first entry does not state the requirement: deleting the second entry from its bundle lowers the verdict to PARTIAL COVERAGE instead of a failure.
  • Leaf v5 records sealed before run 238 (runs 236 and 237): the public-log entry binds the run's identity and its 33 pre-anchor values, which include the attested decision, the AIVP governance result, the payload hash, the actor and the scaffold flags. The descriptive sections are sealed by hashes inside the bundle but are not anchored, so they show the bundle is internally consistent, not that a party able to rewrite the whole bundle has not changed them.
  • Earlier records: the public-log entry binds the run's identity values only (v3 leaves also bind the actor section and the scaffold flags).

verdifax-verify is the open-source command-line tool that performs that check. It is the offline-verification path for Verdifax bundles: the binary recomputes every section hash, the bundle hash and the anchored public-log leaves from the bundle's own contents, and confirms every section is bound to the same manifest hash, without ever reaching the API. It does not re-derive the manifest hash itself, which takes the full stored manifest.

Install

Go (any platform)

go install github.com/Verdifax/verdifax-verify@latest

The binary lands at $(go env GOBIN)/verdifax-verify, usually ~/go/bin/verdifax-verify.

Prebuilt binaries (Cosign-signed)

Prebuilt binaries for macOS and Linux are published on the releases page with a SHA-256 checksum and a Sigstore Cosign keyless signature for each artifact. The signature is bound to the release workflow's GitHub identity and recorded on the public Sigstore transparency log, so there is no Verdifax-held signing key to trust or compromise.

From v0.2.0 each binary ships a Sigstore bundle (.bundle), the modern single-file format carrying the signature, certificate, and transparency-log entry. Verify before running (example for Apple Silicon):

cosign verify-blob \
  --bundle verdifax-verify-darwin-arm64.bundle \
  --certificate-identity-regexp '^https://github.com/Verdifax/verdifax-verify/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verdifax-verify-darwin-arm64

Verified OK means the binary was built by this repository's release workflow, unmodified. v0.1.9 carries a detached .sig + .pem pair instead of a bundle; verify it with --certificate <file>.pem --signature <file>.sig in place of --bundle. Releases before v0.1.9 carry checksums only.

Which version

Use v0.14.5 or later (verdifax-verify --version). v0.12.0 also accepts a bundle whose JSON carries a value twice under keys that differ only in letter case, which other readers such as jq, Python and browsers read differently; since v0.12.1 the verifier refuses it. Releases through v0.11.0 accept a record whose public-log entries were logged by any key, and do not recognize leaf v7, so they report records from run 240 on as FAILED; releases before v0.11.0 also reject run 239. Since v0.12.0 the verifier accepts an entry only if it is signed with a published Verdifax log-signing key (listed with fingerprints and custody in the verifier's README) and Rekor's signed entry timestamp verifies, which authenticates the anchoring time and entry number. Releases v0.1.4 through v0.9.0 also did not check that an anchor's recorded leaf hash matches the public-log entry its inclusion proof covers, so a skilled forger could rewrite anchored values together with a recomputed leaf hash; since v0.10.0 the verifier binds every anchor to its log entry. The orchestrator container image builds this public verifier at v0.14.5, at /app/verdifax-verify.

Verify in 60 seconds

Run the verifier over a published record, for example run 248:

verdifax-verify https://api.verdifax.com/public/runs/ffb0a8a43f55f044bdb5bedd96bb1bc3/bundle.json

For your own runs, save the bundle with the API key that created the run and verify the file:

curl -s -H "X-Verdifax-Key: $VERDIFAX_KEY" \
  https://api.verdifax.com/runs/63/artifacts -o bundle.json
verdifax-verify bundle.json

The report lists each hash it recomputed and ends with a verdict line. Exit code is 0 when every check passes.

  • VERIFIED AND ANCHORED, COMPLETE RECORD ANCHORED: both public-log entries verify; the first binds the run identity and the pre-anchor table, the second every section of the record. Runs 238, 239 and 240 verify this way; their complete-record entries are Rekor global indexes 3282398365, 3283087102 and 3284146825. The report's Logged by and Logged at lines name the signing key and the authenticated anchoring time; on run 240 and later, Anchoring window reads sealed at 600s and met.
  • VERIFIED AND ANCHORED, PARTIAL COVERAGE: the run identity and the 33-value pre-anchor table are anchored; the descriptive sections are sealed inside the bundle only. Runs 236 and 237 verify this way.
  • VERIFIED AND ANCHORED (PARTIAL COVERAGE: pre-v5 record ...): an earlier record whose anchor binds the run's identity values.
  • SELF-CONSISTENT ONLY, NOT ANCHORED: the bundle carries no verifiable public-log anchor; it fails unless --allow-unanchored is passed.
  • FAILED: a hash or an anchor did not verify (exit code 1).

Relying parties: from run 239 on, the verifier reports FAILED with exit code 1 for an altered complete-record anchor (since v0.10.0) and for a missing one (since v0.11.0), even with --allow-unanchored, because the record's first entry states that the anchor is required. For run 238, require the verdict COMPLETE RECORD ANCHORED (in --json output, "record_coverage": "complete_record_anchored"): deleting its complete-record anchor yields PARTIAL COVERAGE with exit code 0. From run 239 on, if the second public-log submission still fails after bounded retries, the run is recorded as failed at stage BUNDLE_ANCHOR and no audit bundle is stored for it; from run 240 on, the same happens if either entry lands outside the record's sealed anchoring window.

Verify deny and halt receipts

A refused or halted run produces a sealed receipt instead of an audit bundle: a DenyReceipt (pepg_deny), CCVHaltReceipt (ccv_halt), MACCHaltReceipt (macc_halt), AivpT4HaltReceipt (aivp_t4_halt) or AttestationGapReceipt (attestation_gap). Save the receipt document the API serves and pass it to verdifax-verify 0.14.5 or later:

curl -s -H "X-Verdifax-Key: $VERDIFAX_KEY" \
  https://api.verdifax.com/runs/{id}/deny-receipt -o receipt.json
verdifax-verify receipt.json

The other endpoints are ccv-halt-receipt, macc-halt-receipt, aivp-t4-halt-receipt and attestation-gap-receipt. The verifier recomputes the receipt's hash from its own fields and checks its receipt_anchor: a public-log entry whose leaf binds the receipt's kind, envelope id and hash, and, under receipt anchor v2, the run number. It verifies that entry exactly as it verifies a run's entries (inclusion proof, signed checkpoint, entry body and signed entry timestamp), requires a published Verdifax log-signing key to have logged it, and requires it to have been logged within 600 seconds after the receipt's own clock.

Receipts anchored under v2 also seal anchor_required in their own hash, so such a receipt fails if its entry is deleted, replaced with a v1 entry or moved off the public log. Receipts anchored under v1, issued from run 241 up to run 242, do not bind the run number: the verifier prints it as "from the document, not checked" and identifies the receipt by its envelope. Verifier 0.13.x cannot read receipts that carry anchor_required and reports them as FAILED, so use 0.14.5 or later.

  • RECEIPT VERIFIED AND ANCHORED, exit code 0.
  • RECEIPT SELF-CONSISTENT ONLY, NOT ANCHORED: the receipt carries no public-log entry, so its hash recomputing shows only that its fields agree with each other. Receipts issued before run 241, at 23:11 UTC on 2026-10-16, when receipt anchoring started, carry none. Exit code 1 unless --allow-unanchored is passed.
  • FAILED, exit code 1.

Verdifax holds the log-signing key, so an anchored receipt shows that Verdifax logged it at that time, not that the refusal was the right decision. Since verifier 0.14.1, a receipt dated at or after run 242's refusal (2026-10-16T23:48:35Z), from when every receipt the API issues seals anchor_required, fails if it lacks the field. Pass --self-hosted-receipt only for a receipt you know came from a test or self-hosted deployment that does not anchor receipts. Since 0.14.2, a receipt dated at or after run 241's refusal (2026-10-16T23:11:17Z), the first receipt the API anchored, also fails if it carries no public-log entry, under the same exemption. Any receipt dated before run 241's clock, real or invented, has nothing anchored that could contradict it, because genuine receipts from before receipt anchoring began carry no entry either: it is reported as self-consistent only and fails by default, passes with --allow-unanchored, and is only as trustworthy as whoever handed it to you. A stripped receipt cannot keep its real date. The receipt document's ok field is the API's response flag and is not checked.

What the verifier checks

For a completed run, the checks are the audit bundle checks described above. For a receipt, the verifier rebuilds the receipt's canonical preimage from its own fields, recomputes SHA-256 and compares it with the sealed hash, then checks the receipt's public-log entry. The preimages are:

For pepg_deny (DenyReceipt):

The verifier reconstructs the preimage from anchor_required (when the receipt seals it), envelope_id, policy_hash, evaluation_hash, fired_rule_id, decision_reason_code, mcd_finding_hash (an empty string when MCD did not match), deny_clock, evaluator_version, and version, then recomputes SHA-256 against the sealed hash.

For ccv_halt (CCVHaltReceipt):

The verifier reconstructs from anchor_required (when the receipt seals it), envelope_id, allow_token_hash, budget_hash, constraint_type, budget_limit, consumed_at_halt, partial_execution_hash, halt_reason_code, halt_clock, evaluator_version, version, then recomputes SHA-256.

For macc_halt (MACCHaltReceipt):

The verifier reconstructs from anchor_required (when the receipt seals it), envelope_id, tenant_id, allow_token_hash, budget_hash, constraint_type, budget_limit, cumulative_at_halt, per_run_consumption, window_start, halt_reason_code, halt_clock, evaluator_version, version, then recomputes SHA-256.

Machine-readable vs. human-readable output:

The verifier CLI outputs machine-readable JSON (--json flag) for programmatic consumption. The audit PDF renders a human-friendly presentation of the same verification outcome.

Strict mode

Some artifacts in the bundle honestly declare themselves as scaffold values, placeholders whose real cryptographic backing arrives in a later build phase. On run 238 they are transcript, hardware_attestation, leakage_bundle and zksp_binding.

A fifth artifact, pote_proof, was historically scaffolded too. Its scaffold flag is now cleared automatically when the run was sealed against a real Sigstore Rekor ledger (the default for production runs). Runs sealed in mock-ledger mode (dev/test) still carry the flag.

In default mode the verifier passes on scaffold artifacts: their hashes still recompute correctly because the scaffold value is part of the canonical content. The orchestrator is being honest about what is and isn't yet rooted in real hardware.

For high-trust environments where your own policy accepts nothing less than a cryptographic root of trust, pass --strict:

verdifax-verify --strict --show-evidence-summary bundle.json

This makes the verifier exit 1 if any artifact carries scaffold.is_scaffold = true. The --show-evidence-summary flag prints a one-glance maturity report alongside the cryptographic verification, including an explanatory verdict line so a strict-mode failure is never mistaken for a cryptographic failure. Use this combined form as the gate in your CI; the exit code drives the gate, the summary explains the result.

Use in CI

A typical pipeline that wants to enforce both internal consistency and real hardware root of trust would call:

verdifax-verify --strict --json "$BUNDLE" > verdict.json
if [ $? -ne 0 ]; then
  echo "Bundle did not verify under strict mode."
  exit 1
fi
# For records sealed from run 238 on, also require the complete record:
jq -e '.record_coverage == "complete_record_anchored"' verdict.json

What the verifier does NOT prove

To set expectations honestly:

The verifier confirms internal consistency of a received bundle. It does not by itself prove that the underlying execution actually happened on Verdifax hardware. A malicious orchestrator could fabricate a bundle with consistent internal hashes.

For a record that verifies as COMPLETE RECORD ANCHORED, the verifier also rules out a narrower threat: someone changing any section of a real bundle after anchoring, because the changed section no longer matches the digest bound to the complete-record entry. For a leaf v5 record without that entry (runs 236 and 237), it rules out changes to the 33 pre-anchor values (for example the attested actor or decision, the payload hash, or the AIVP governance result), but not to the descriptive sections (outcome summary, request substance, authorization and approval chain, real-world effects, data classification, system provenance), which are sealed only by section hashes and a bundle hash inside the bundle that anyone able to rewrite the whole bundle can recompute.

For protection against fabrication, you also want:

  • Public-log anchoring (Sigstore Rekor). āœ“ Live in production: every successful run is committed as a hashedrekord entry on rekor.sigstore.dev, a public transparency log that Verdifax does not operate. The leaf binds the run's identity values and, for records sealed under leaf v5, the digest of its 33 pre-anchor values. Records sealed from run 238 on add a second entry that binds every section.
  • Hardware-rooted attestation (TPM2 quote / AMD SEV-SNP report) bound to specific silicon. Real only on deployments with an AMD SEV-SNP attestation agent configured. On other deployments, including the one that sealed run 238, the hardware-attestation step emits a scaffold value. See /concepts/scaffold-gaps/ for the activation procedure.

The verifier reports honestly when scaffold values are in play; nothing claims to be more than it is.

Verifier version

The current verdifax-verify release is v0.14.5 (release notes). Use it or a later release for any record. Since v0.13.0 the same tool checks deny and halt receipts and their public-log entries; since v0.14.0 it also checks receipt anchor v2, which binds the run number.

Source code

The verifier is open source under the Clear BSD License (SPDX BSD-3-Clause-Clear) from v0.5.0, and lives in its own public repository. Releases through v0.4.0 were MIT:

github.com/Verdifax/verdifax-verify

You are encouraged to read the source, that is the entire point of an open-source verifier.