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 COVERAGEinstead 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'sLogged byandLogged atlines name the signing key and the authenticated anchoring time; on run 240 and later,Anchoring windowreadssealed 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-unanchoredis passed.FAILED: a hash or an anchor did not verify (exit code1).
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 code0.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 code1unless--allow-unanchoredis passed.FAILED, exit code1.
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.
