Skip to main content

tanilo-receipt-verify

Verifier for composed verification-state envelopes: RFC 8785 JCS canonicalization plus EdDSA/ES256 JWS signature verification. Canonicalization output is byte-identical to the production Node canonicalizer.

JCS number formatting: this implementation's number serialization matches RFC 8785 for the value ranges receipt fields actually use (small integers and simple decimals) — it does not implement the full RFC 8785 §3.2.2.3 ECMAScript-compatible number-to-string algorithm across every double (extreme exponents, -0, etc.). If a future receipt field ever carries a number outside that range, re-verify canonicalization against the Node reference before trusting a byte-identical claim for it.

Supersedes agentoracle-receipt-verify 0.1.0. Same author, same verifier, corrected defect (AC-11) described below. If you have agentoracle-receipt-verify installed, switch to this package; agentoracle-receipt-verify will not receive further fixes.

Design goal

Three language bindings, one canonicalization. A receipt canonicalized in Node, Python, or the browser must produce the byte-identical string and byte-identical SHA-256. No language-specific behavior. No trusted issuer round-trip.

Install

pip install tanilo-receipt-verify

Usage

Key material is required to reach a verdict. Pass jwks_by_issuer:

from tanilo_receipt_verify import verify

result = verify(envelope, jwks_by_issuer={
    "https://agentoracle.co/.well-known/jwks.json": ao_jwks,
    "https://agenttrust.uk/.well-known/jwks.json": at_jwks,
})

if result.status == "valid":
    print("verified — canonical:", result.canonical_sha256)

Three outcomes, not two

status valid Meaning
"valid" True Every check ran and passed
"invalid" False A check ran and failed; see .errors
"indeterminate" None A check could not run; see .indeterminate_reason

Calling verify(envelope) without jwks_by_issuer on a signed envelope returns indeterminate, not valid. Canonicalization recompute proves the payload matches its claimed hash; it binds the payload to no issuer. Only the signature does that. A verifier that reported valid there would assert a property it never tested.

None is falsy, so if result.valid: fails closed. Branch on .status when you need to distinguish "failed" from "could not check".

What it checks

Invariant Description
canonical_recomputes JCS(payload) → SHA-256 recomputes byte-identical to claimed
decision_ref_recomputes sha256(JCS(preimage)) matches published decision_ref (per invinoveritas/babyblueviper1 spec)
decision_signer_ne_runtime Decision signer issuer ≠ runtime issuer (fail-closed: self-approval is void). None when either issuer is absent from the payload — the check could not run, which is not the same fact as "it ran and they matched"
all_signatures_verified Every JWS signature (EdDSA or ES256, per draft-krausz-verification-state-02 §5.1) verifies against a resolvable JWK by kid. None when a signer's key material could not be resolved from the supplied JWKS, its key material was malformed, or its algorithm isn't one of the two this verifier implements — unevaluated, not failed. False for an unresolved kid instead of None when jwks_is_complete=True — see below

Corrects a defect in agentoracle-receipt-verify 0.1.0 (AC-11)

"AC-11" names this bug class — a checker's own incomplete input reported as a finding about the artifact it's checking, rather than a fact about the checker — as case AC-11 in stillmarcus24/assurance-run, the independent conformance suite this class of defect was first named in. This package's own instance of it (below) is credited under that name, not coined here.

agentoracle-receipt-verify 0.1.0 correctly returned indeterminate when called with no jwks_by_issuer at all. It did not correctly handle the partial case: a composed, multi-signer envelope where the caller supplies JWKS for some but not all signers.

For a signer whose kid could not be matched against any supplied JWKS, 0.1.0 recorded {"verified": False} — the same value a genuine cryptographic signature failure produces. all_signatures_verified then read that as a real failure, and the adjudication logic ranked it above "unevaluated," returning status: "invalid" for an envelope that was never actually disproven — only partially checked.

This package resolves the two cases distinctly:

  • Signer's kid not found in any supplied JWKS → that signer's entry reports verified: None, issuer: "unresolved". The envelope's overall status becomes indeterminate (not invalid) when every other check passes, with indeterminate_reason naming which kid(s) could not be resolved.
  • Signer's kid found, signature verification actually run and fails → that signer's entry reports verified: False. The envelope's overall status is invalid.

An unresolved key lookup and a cryptographic failure are different facts. Reporting both as invalid collapsed "we never checked this" into "this was checked and failed" — the same failure class documented in the 0.1.0 CHANGELOG for the fully-omitted-JWKS case, here on the partial-JWKS path instead.

The two meanings of a missing key — jwks_is_complete

An unresolved kid can mean two different things, and this verifier cannot tell them apart on its own:

  1. "This is whatever keys I happened to have on hand." A missing kid is a gap in what the caller fetched, not a statement about the receipt. That's the default above: verified: None, status: "indeterminate".
  2. "This IS my complete trust list." The caller is asserting every issuer they will ever accept is already in jwks_by_issuer, so a kid that isn't there is a deliberate refusal, not a gap.

Both readings are legitimate; the verifier has no way to guess which one the caller means, so it's an explicit argument instead of a guess:

result = verify(
    envelope,
    jwks_by_issuer={"https://agentoracle.co/.well-known/jwks.json": ao_jwks},
    jwks_is_complete=True,  # this IS the whole trust list
)
# an unresolved kid now reports verified=False, is named in .errors,
# and the envelope's status is "invalid" — a policy refusal, not a gap

jwks_is_complete defaults to False, matching every example above with no behavior change. Setting it to True only changes what happens when a kid can't be resolved against the supplied JWKS — it does not change how a signature that resolves and genuinely fails cryptographic verification is reported; that's always invalid either way.

Credit where it's due: this split converged out of a public thread rather than being invented here. robertolocatelli81-dev proposed the same distinction for JWKS lookups as keys_are_complete in cryptovalid-opencore, and babyblueviper1 shipped the same distinction for a different artifact — a referenced-proof set, not a key set — as --referenced-set-is-complete in preaction-governance-conformance. Both land on: absent-by-default is an absence, not a judgment; the caller has to declare completeness before a missing entry becomes a refusal. jwks_is_complete here is that same shape applied to this package's own JWKS lookup.

Cross-language guarantees

The tests/ suite includes byte-identical fixtures shared with the Node reference implementation:

  • test_jcs_byte_identical_to_node — Python JCS output byte-matches Node output for a payload with nested objects, arrays, unicode, booleans, and integers.
  • test_decision_ref_recompute_babyblueviper1 — Python recomputes the shipped invinoveritas fixture, byte-identical to her Python and our Node.
  • test_conformance_sample_canonical_hash — reproduces the canonical hash from the production /v1/conformance/sample endpoint.

License

MIT — see LICENSE.

Release files for tanilo-receipt-verify 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tanilo-receipt-verify 0.1.0
File Size Uploaded
tanilo_receipt_verify-0.1.0.tar.gz 32.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tanilo-receipt-verify 0.1.0
File Interpreter ABI Platform
tanilo_receipt_verify-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 48.4 kB

Release files / tanilo_receipt_verify-0.1.0.tar.gz

Download URL tanilo_receipt_verify-0.1.0.tar.gz
Size 32.9 kB
Tags Source
SHA-256 checksum
How to use checksums
44dc542b6fc22baab45de1ce4e2ac07437ab51d6fb91c24fbd9feea5d0f4180e
BLAKE2b-256 checksum
How to use checksums
2801863889c690927fe7e6bcdc73acdb8927ec1806b79031da0bf3ff99b68998
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / tanilo_receipt_verify-0.1.0-py3-none-any.whl

Download URL tanilo_receipt_verify-0.1.0-py3-none-any.whl
Size 15.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7166053bb8777934d416cb2b20f68040b1745f363ddb84c1e79e9b204887ff5b
BLAKE2b-256 checksum
How to use checksums
8c4caec2c4c046b735f833cbe0d91927397d2e7feed16e63d5d832956bfb486b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page