Skip to main content

trust-ledger-verifier

Offline verification of trust-ledger/v1 and trust-ledger/v2 evidence packs. Distribution name is trust-ledger-verifier (the import package and CLI stay verifier / trust-verify — the plain name verifier is already taken on PyPI by an unrelated project).

Install

pip install trust-ledger-verifier

Usage

trust-verify path/to/pack
trust-verify path/to/pack --reference-values values.json
trust-verify path/to/pack --tsa-trust-profile my-profile.json
trust-verify path/to/pack --audit-challenge challenge.json
trust-verify path/to/pack --evidence-profile profile.json --appraisal-context context.json
trust-verify path/to/pack --producer-registry registry.json
trust-verify path/to/pack --challenge-ledger my-challenges.json
trust-verify path/to/pack --json   # machine-readable, for CI

# Build the appraisal context (digests of your documents, as one bindable object).
trust-context --out context.json --schema v3 \
    --evidence-profile profile.json --reference-values values.json \
    --tsa-trust-profile-builtin --no-validator-set \
    --producer-registry registry.json --challenge-ledger my-challenges.json \
    --accepted-pack-schema trust-ledger/v2 --require-anchor --require-challenge

trust-challenge --relying-party my-org --valid-for 24h > challenge.json
trust-challenge --relying-party my-org --appraisal-context context.json > challenge.json

# The only command here that writes: record that you accepted this pack as the answer.
trust-redeem path/to/pack --ledger my-challenges.json --audit-challenge challenge.json \
    --init --ledger-id my-org-challenges/2026 --relying-party-id my-org \
    --authority "besluit DPO-2026-014"

Exit codes: 0 = PASS, 1 = not PASS (FAIL / PARTIAL / claimed-only / unknown version), 2 = the pack could not be loaded at all.

--reference-values

The trusted basis a session's claims are checked against, supplied by you, the relying party. Without it, the verifier falls back to the values the pack itself claims, and the verdict can never be a full PASS — at best claimed-only. This is intentional: a pack grading its own homework is not evidence.

--tsa-trust-profile

Same principle, applied to anchoring: the TSA trust profile always comes from the relying party, never from the pack. A pack can carry an RFC 3161 token, but it does not get to say which timestamp authorities you trust — that would let a producer anchor against a TSA it controls and call it proof.

Without --tsa-trust-profile, the verifier uses a bundled default (sigstore-public-tsa.json, shipped as package data) pointing at the public timestamp.sigstore.dev service. The report always names which profile judged the anchor, including a digest of the profile document, so a reader can tell "trusted by the default bundle" apart from "trusted by your own profile" without re-reading the code.

To supply your own: the bundled file is the format — copy it and adjust roots, intermediates, allowed_policy_oids, allowed_tsa_spki_sha256, allowed_hash_oids / allowed_signature_oids, and revocation.policy (required or if-available, depending on whether your TSA publishes a CRL).

--audit-challenge

Third input of the same kind, and the one that answers a question the other two cannot: is this the pack I asked for? You mint a challenge with trust-challenge before the execution, send it to the producer, and keep your copy. The producer's ledger binds it at genesis, so the whole chain hangs off it — a clean pack from last month cannot be handed over as an answer, and a challenge pasted into a manifest afterwards is a FAIL.

Without it, the pack is unchallenged and reaches at most PARTIAL: nothing then ties it to the execution you had in mind, and the producer picked both the session id and which sealed session to show you.

⚠️ What a match does and does not prove. It proves the declaration was made after your challenge existed (statement freshness). It does not prove the described execution happened after it: a producer can keep the measured values of an older run and wrap fresh, correctly bound records around them. RFC 9334 makes the same point about nonces. Proving execution freshness needs the challenge to be consumed by the execution or measurement layer itself, which is outside this project.

--mode period_epoch exists for continuously running systems that cannot fetch a challenge per session. It is an epoch identifier: it proves the declaration fell inside your window and nothing more — no coverage, no completeness — and it therefore reaches challenged but never a full PASS.

--evidence-profile and --appraisal-context

Fourth input of the same kind, answering the question the other three still leave open: did the producer prove the thing I asked about, or only the things that suited him? Every check before this one asks whether what is present is valid. None of them asks whether what had to be present is there — so a producer answering "did this workload leak to the cloud?" can emit one blameless inference.local record, emit no routing.decision at all, and pass without falsifying a single field.

The evidence profile states which claim types must be present, on whose authority (issuer) and under which norm (norm_reference, mandatory — also for a purely internal policy). Both sit inside the profile's digest: origin you can edit afterwards is not origin. A missing required claim type is a FAIL naming that type, not a downgrade — there was an explicit requirement and the evidence does not meet it.

Two norms at once is the normal case (an AI Act art. 12 obligation next to a GDPR art. 30 one), so a profile can be composite: components written out in full, composition: "union" as the only allowed value, and the written-out required_claim_types checked against the union of the components. An intersection of two norms would be a third, weaker norm.

The appraisal context binds the profile together with everything else that steers the verdict — reference-values, validator set, TSA trust profile, producer registry, require_anchor / require_challenge — as one object, each pin optional (null = do not pin). Mint your challenge with trust-challenge --appraisal-context context.json and its digest goes into the challenge and therefore into genesis: the producer knew up front which evidence he owed, and nobody can swap the norm afterwards. If your challenge binds a context and you do not supply it at verification time, coverage is unevaluated and the pack reaches at most PARTIAL — "no requirement" and "a requirement I cannot test" are different statements.

Without any profile the pack is uncovered, and that alone does not degrade the verdict: with no stated norm there is nothing to test coverage against.

⚠️ What covered does and does not prove. It counts claim types, not executions. It rules out "not one record of this type"; it does not rule out "one blameless record and the rest omitted". For a positive claim ("show that you inferred locally") it is strong; for a negative one ("show that nothing was transferred") it is nearly nothing. If your norm needs per-execution coverage, set requires_event_coverage: true and the verdict degrades with that reason rather than showing a green axis your norm did not ask for.

Writing the context: trust-context

The evidence profile you write by hand — it is a small document and the real question in it (required_claim_types) is yours to answer. The context is different: it is almost entirely digests over the JCS-canonical form of the other documents, so trust-context computes it for you, using the same code the verifier uses:

trust-context --out context.json --schema v3 \
    --evidence-profile profile.json \
    --reference-values values.json \
    --no-validator-set \
    --tsa-trust-profile-builtin \
    --producer-registry registry.json \
    --challenge-ledger-id my-org-challenges/2026 \
    --accepted-pack-schema trust-ledger/v2 \
    --require-anchor --require-challenge

Every pin is an explicit either/or — a path, or --no-<thing> — and leaving a group unanswered is refused. That is not ceremony: null here means "I chose not to pin this", and a flag you merely forgot would say the same thing without anyone having chosen it. --schema is stated for the same reason: a v2 document does not say "unpinned on purpose", it says "this schema had no such field", and only you can say which of the two you mean.

It creates a document; it never updates one. Overwriting is refused without --overwrite, because an outstanding challenge may already bind the digest of the file that is there — and replacing it does not make that challenge stricter, it makes it unjudgeable.

It will not write your evidence profile. Deciding which claim types your norm requires is the audit work, and a tool that guessed it would hand you a norm nobody wrote.

--producer-registry

Same principle again, on the axis that says who. The pack supplies its own public key, and key_id is provably derived from that key — but derived-from-the-key-the-pack-supplied is an internal consistency property, not an identity. Without a registry, a producer can re-sign an entire pack with a fresh key under any name he likes and everything still verifies.

The registry (trust-ledger/producer-registry/v1) says which keys you enrolled, under which name, and on what basis:

{
  "producer_registry_schema": "trust-ledger/producer-registry/v1",
  "registry_id": "toezichthouder-x-producenten/v3",
  "issuer": {"relying_party_id": "toezichthouder-x", "authority": "besluit DPO-2026-014",
             "issued_ts": "2026-07-28T00:00:00Z"},
  "entries": [
    {"producer_id": "gateway", "key_id": "ed25519-…",
     "enrollment_basis": "sleuteloverdracht ter plaatse, protocol 2026-03-11",
     "enrolled_ts": "2026-03-11T09:00:00Z",
     "status": "active", "status_since": null, "status_reason": null}
  ]
}

entries is a list, not a map, because a producer holds several keys over time and the historical entry has to stay readable to judge old packs. status is active, superseded (retired by rotation — still covers what it signed before status_since, judged against the anchor's genTime) or revoked (compromised — rejects regardless of date, because a compromise works backwards). Without a trusted anchor there is no genTime to judge a rotation against, and a superseded entry is then not talked free by a sealed_ts the producer wrote himself.

enrollment_basis is mandatory and checked by nothing. That is deliberate: the enrolment is where the trust comes from, so a registry that omits it records only its conclusion — but no field could verify it, and a field that looks like a control while enforcing nothing is worse than an honest note.

⚠️ Without --producer-registry the pack is unregistered and reaches at most PARTIAL. Note that this differs from uncovered, which does not degrade: with no evidence profile there is no requirement to test against, but a pack always claims authorship, so a full PASS would be attesting a claim nobody checked. There is deliberately no bundled default registry either, unlike the TSA trust profile: for timestamp authorities a publicly recognised party exists, for producer identity none does.

⚠️ What registered does and does not prove. It proves this is the key you enrolled under this name — not which legal person is behind it (the registration is exactly as strong as the enrolment, which happened outside this system), and not that nobody else holds a copy of the key. There is no HSM binding and no hardware attestation here: whoever holds the private key is the producer.

--challenge-ledger and trust-redeem

The format has said challenge_mode: "single_execution" since the challenge existed, and until now nothing tested the word single where it promises something: that one execution also yields one answer. A producer could hand over pack A, wait for a dispute, and then "find" pack B — same session, same challenge, same enrolled key, another story. Both passed.

The challenge ledger (trust-ledger/challenge-ledger/v1) is your own record of which challenge you accepted which pack as the answer to:

{
  "challenge_ledger_schema": "trust-ledger/challenge-ledger/v1",
  "ledger_id": "toezichthouder-x-challenges/2026",
  "issuer": {"relying_party_id": "toezichthouder-x", "authority": "besluit DPO-2026-014",
             "issued_ts": "2026-07-29T00:00:00Z"},
  "entries": [
    {"challenge_digest": "sha256:…", "anchor_digest": "sha256:…",
     "accepted_ts": "2026-07-29T11:04:00Z", "accepted_verdict": "PASS", "note": null}
  ]
}

A pack is identified by its anchor_digest, which covers the whole file inventory and is stable across re-anchoring — so verifying or redeeming the same pack twice is no contradiction. Unanchored packs have no such identity and are reported unevaluated rather than falling back to something weaker. Two entries with different anchor_digest under one challenge are contradicted: a FAIL, and it hits the first pack as hard as the second. Which of the two is the lie cannot be established; that one of them is untrue can.

trust-verify only ever reads the ledger. trust-redeem verifies a pack and appends an entry — never on a FAIL, never on a contradiction (that is already recorded), never for a pack without a stable identity — writing atomically. Splitting the two keeps verify_pack a pure function of (pack, documents): a verifier that wrote would make its verdict depend on how often it had run before.

⚠️ Without --challenge-ledger the axis is unchecked, and unlike unregistered this does not degrade the verdict. A pack never claims to be the only answer to its challenge, so nothing goes unchecked here — the absent thing is an administration you keep. This is the uncovered side of that asymmetry, not the unregistered side.

⚠️ This is not fork detection. It proves at most that you accepted no second, different pack under this challenge: non-repudiation, not uniqueness. A contradictory pack given to another party, or to nobody, stays invisible. And this is the one input that grows: pinning in the appraisal context uses challenge_ledger_id, an identity rather than a digest (the ledger grows, so a digest bound at mint time would be stale by definition), which means you can drop an entry from your own administration without the binding noticing. Lose the file and the guarantee goes with it.

What a verdict means

  • PASS — every universal check passed (schema → sequence → signatures → chain, in that fixed order), every payload validator against the trusted reference values passed, the pack is anchored (anchor_status == "anchored") against a profile you accepted, and it is bound to the challenge you issued (challenge_status == "challenged", challenge_mode == "single_execution"), and the signing key is enrolled in the registry you supplied (identity_status == "registered"), and — when your challenge binds a norm — that norm was supplied and met, and your challenge ledger, if you keep one, holds no other answer to this challenge (answer_status is answered-once or unchecked). Each axis is separately necessary: an unanchored pack is provably truncatable, an unchallenged one is provably interchangeable, and an uncovered one is provably too narrow.
  • PARTIAL (reported as claimed-only in the verdict when trust_basis is weak, or a PASS degraded by anchor or challenge status) — universally valid, but either no external reference values were supplied, or the pack isn't anchored, or anchoring couldn't be evaluated with the trust profile you gave it (unknown root, wrong policy, revoked signer), or it isn't bound to a challenge. This is not a bug in the pack; it's a correct, weaker conclusion.
  • FAIL — a named check failed: a broken hash-chain link, an invalid signature, a payload that violates the reference values, an anchor whose token doesn't match the pack's digest (post-anchoring tampering), or a pack that answers a different challenge than the one you supplied (challenge-mismatch) or falls outside its window (challenge-expired), or a required claim type that is simply absent (coverage-gap), or a supplied norm that is not the one your challenge bound (appraisal-context-mismatch — never merged, never resolved in favour of the stricter one), or a signing key your registry never enrolled (unknown-key, revoked), or a second, different pack under a challenge your ledger already recorded an answer to (contradicted), or a challenge ledger that is not the one your context pinned (wrong-ledger).
  • unknown version — the pack's schema version is one this verifier doesn't know. Treated as a separate outcome, not silently as FAIL or PASS.

Every text report ends with a mandatory proof-boundary paragraph naming what is and is not proven for that specific pack — completeness, identity, key custody, uniqueness, relevance, time. Read it; it is not boilerplate, and it changes depending on anchor, challenge, coverage and identity status. See the root README.md for the project-wide version of this list and for what is not yet built at all (fork detection, per-execution coverage).

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

trust_ledger_verifier-0.6.0.tar.gz (116.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

trust_ledger_verifier-0.6.0-py3-none-any.whl (87.7 kB view details)

Uploaded Python 3

File details

Details for the file trust_ledger_verifier-0.6.0.tar.gz.

File metadata

  • Download URL: trust_ledger_verifier-0.6.0.tar.gz
  • Upload date:
  • Size: 116.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for trust_ledger_verifier-0.6.0.tar.gz
Algorithm Hash digest
SHA256 11c9d1610b411c2ffe16916ecf5829a170d8929c4d9bdd4300552c796ad67df1
MD5 4c96677f3683a0835e8f489b83342abd
BLAKE2b-256 322e3cd1130d46fd302d828460e7a9842419034261556af7479dac1402297be6

See more details on using hashes here.

Provenance

The following attestation bundles were made for trust_ledger_verifier-0.6.0.tar.gz:

Publisher: publish-pypi.yml on duncanprins/trust-ledger

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file trust_ledger_verifier-0.6.0-py3-none-any.whl.

File metadata

File hashes

Hashes for trust_ledger_verifier-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 aa91777cc8d9802ad7cd3f8a402d96991a15cbefa5c1fdba1178d771d6d3a0eb
MD5 19152b6159be9cb2d2a2629cfeba6f7f
BLAKE2b-256 fe0b4da9d2f52649975b0513fbce922aafac8acf4aa47651c43009fa2d7e5a2f

See more details on using hashes here.

Provenance

The following attestation bundles were made for trust_ledger_verifier-0.6.0-py3-none-any.whl:

Publisher: publish-pypi.yml on duncanprins/trust-ledger

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page