tanilo-receipt-verify 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. tanilo_receipt_verify-0.1.0/CHANGELOG.md +56 -0
  2. tanilo_receipt_verify-0.1.0/LICENSE +21 -0
  3. tanilo_receipt_verify-0.1.0/MANIFEST.in +5 -0
  4. tanilo_receipt_verify-0.1.0/PKG-INFO +118 -0
  5. tanilo_receipt_verify-0.1.0/README.md +104 -0
  6. tanilo_receipt_verify-0.1.0/pyproject.toml +20 -0
  7. tanilo_receipt_verify-0.1.0/setup.cfg +4 -0
  8. tanilo_receipt_verify-0.1.0/tanilo_receipt_verify/__init__.py +47 -0
  9. tanilo_receipt_verify-0.1.0/tanilo_receipt_verify/verify.py +570 -0
  10. tanilo_receipt_verify-0.1.0/tanilo_receipt_verify.egg-info/PKG-INFO +118 -0
  11. tanilo_receipt_verify-0.1.0/tanilo_receipt_verify.egg-info/SOURCES.txt +23 -0
  12. tanilo_receipt_verify-0.1.0/tanilo_receipt_verify.egg-info/dependency_links.txt +1 -0
  13. tanilo_receipt_verify-0.1.0/tanilo_receipt_verify.egg-info/requires.txt +1 -0
  14. tanilo_receipt_verify-0.1.0/tanilo_receipt_verify.egg-info/top_level.txt +1 -0
  15. tanilo_receipt_verify-0.1.0/tests/fixtures/ac11/envelope-three-signer.json +17 -0
  16. tanilo_receipt_verify-0.1.0/tests/fixtures/ac11/jwks-agentoracle.json +12 -0
  17. tanilo_receipt_verify-0.1.0/tests/fixtures/ac11/jwks-agenttrust.json +12 -0
  18. tanilo_receipt_verify-0.1.0/tests/fixtures/ac11/jwks-presidio.json +12 -0
  19. tanilo_receipt_verify-0.1.0/tests/fixtures/jwks_by_issuer.json +38 -0
  20. tanilo_receipt_verify-0.1.0/tests/fixtures/signed_envelope.json +1 -0
  21. tanilo_receipt_verify-0.1.0/tests/test_ac11_partial_jwks.py +255 -0
  22. tanilo_receipt_verify-0.1.0/tests/test_byte_identical.py +119 -0
  23. tanilo_receipt_verify-0.1.0/tests/test_defensive_and_es256.py +293 -0
  24. tanilo_receipt_verify-0.1.0/tests/test_indeterminate_default.py +176 -0
  25. tanilo_receipt_verify-0.1.0/tests/test_jwks_is_complete.py +213 -0
@@ -0,0 +1,56 @@
1
+ # Changelog
2
+
3
+ ## Unreleased — `jwks_is_complete` parameter
4
+
5
+ New keyword argument on `verify()`: `jwks_is_complete: bool = False`.
6
+
7
+ An unresolved `kid` is ambiguous on its own — it can mean "this is just whatever keys I fetched" (a gap) or "this IS my complete trust list" (a refusal). Previously the verifier always assumed the former. Now the caller says which:
8
+
9
+ - **`jwks_is_complete=False` (default, no behavior change):** an unresolved `kid` reports `verified: None`; overall status is `indeterminate` when nothing else fails. Identical to every prior release.
10
+ - **`jwks_is_complete=True`:** an unresolved `kid` reports `verified: False`, an error naming the `kid` is added to `.errors`, and overall status is `invalid` — a policy refusal, not a gap. Applies both to the partial-JWKS case (some issuers supplied, this one isn't) and to the fully-omitted case (no JWKS supplied at all, declared complete regardless).
11
+
12
+ Does not change how a signature that resolves and genuinely fails cryptographic verification is reported — that is `invalid` under either setting, unchanged.
13
+
14
+ Converged out of the tsc#4 GitHub thread (2026-09-24): [robertolocatelli81-dev](https://github.com/robertolocatelli81-dev)'s `keys_are_complete` and [babyblueviper1](https://github.com/babyblueviper1)'s `--referenced-set-is-complete` (shipped in [preaction-governance-conformance@3ffddb0](https://github.com/babyblueviper1/preaction-governance-conformance/commit/3ffddb0)) are the same split applied to different artifacts — a JWKS key set here, a referenced-proof set there. `jwks_is_complete` is that shape, credited to both, applied to this package's own JWKS lookup.
15
+
16
+ **Test coverage:** `tests/test_jwks_is_complete.py`, 7 tests — the default-unchanged case, the declared-complete refusal case (partial JWKS and fully-omitted JWKS), confirms a resolved signer's `verified: True` is untouched by the flag, and confirms the flag never softens a genuine cryptographic failure.
17
+
18
+ ## 0.1.0 — first release of `tanilo-receipt-verify`
19
+
20
+ Supersedes `agentoracle-receipt-verify` 0.1.0. Same verifier, same API, one defect correction (AC-11).
21
+
22
+ ### AC-11 — partial-JWKS case no longer reported as `invalid`
23
+
24
+ `agentoracle-receipt-verify` 0.1.0 handled the fully-omitted-JWKS case correctly (`indeterminate`, not `valid`). It did not handle the partial case: a composed, multi-signer envelope where the caller supplies JWKS for some but not all signers.
25
+
26
+ For a signer whose `kid` had no match in any supplied JWKS, 0.1.0 recorded `{"verified": False}` — indistinguishable from a genuine cryptographic signature failure. `all_signatures_verified` then read that as a real failure, and adjudication ranked it above "unevaluated," so the envelope came back `status: "invalid"` — a verdict about a signature that was never actually checked, not one that failed a check.
27
+
28
+ This release distinguishes the two cases:
29
+
30
+ - **Unresolved key lookup** (no JWK for that signer's `kid` in any supplied issuer's set) → `verified: None`, `issuer: "unresolved"`. Overall status is `indeterminate` when every other check passes, with `indeterminate_reason` naming the unresolved `kid`(s).
31
+ - **Cryptographic failure** (key found, signature checked, verification fails) → `verified: False`. Overall status is `invalid`.
32
+
33
+ Precedence rule: a real `False` outranks a `None` for the overall `all_signatures_verified` check. All-`None`-no-`False` means every signer that had key material verified, and at least one signer's key was never supplied — an honest "could not fully check," not a pass and not a fail.
34
+
35
+ **Upgrading:** if your code branches on `status == "invalid"` to detect a partial-JWKS call, that call now correctly returns `status == "indeterminate"` instead. `result.errors` no longer contains a "signature verify failed" line for a signer whose key was never supplied — check `result.indeterminate_reason` for that case instead.
36
+
37
+ **Test coverage:** `tests/test_ac11_partial_jwks.py` locks this in with a real multi-signer fixture (three genuine Ed25519 signers). Its core assertions were confirmed to fail against the actual published `agentoracle-receipt-verify==0.1.0` wheel (status came back `invalid` with the unresolved kid in `errors`) and pass against this release. The file also promotes five other hand-checked scenarios from the fix into the permanent suite: all-omitted and empty-dict JWKS on a multi-signer envelope, a genuine cryptographic failure with the key present, a mixed real-failure-plus-unresolved-key case (failure must still outrank unresolved), and a fully resolvable single- and three-signer happy path.
38
+
39
+ ### Also in this release
40
+
41
+ - Package renamed `agentoracle-receipt-verify` → `tanilo-receipt-verify`. Import path is now `from tanilo_receipt_verify import verify` (previously `agentoracle_receipt_verify`).
42
+ - No behavior changes beyond the AC-11 fix above. Everything in `agentoracle-receipt-verify` 0.1.0's own CHANGELOG entry (tri-state `status`/`valid`, `indeterminate_reason`, fail-closed `None`) carries forward unchanged.
43
+
44
+ ---
45
+
46
+ ## Prior history (as `agentoracle-receipt-verify`)
47
+
48
+ ### 0.1.0 — 2026-08-11
49
+
50
+ **Corrected a defect in 0.0.1 that could report a signed envelope as `valid` without checking any signature.**
51
+
52
+ `verify(envelope, jwks_by_issuer=None)` gated all signature verification behind the presence of key material. Called without `jwks_by_issuer` on a genuinely signed envelope, it returned `valid=True` with zero signatures checked. Fixed by introducing the tri-state `status` (`"valid"` / `"invalid"` / `"indeterminate"`) and making `valid` `Optional[bool]`, `None` when indeterminate.
53
+
54
+ ### 0.0.1 — 2026-06-01
55
+
56
+ Initial release. Superseded — see above.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TK Collective LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include README.md
2
+ include CHANGELOG.md
3
+ include LICENSE
4
+ recursive-include tests *.py
5
+ recursive-include tests/fixtures *.json
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: tanilo-receipt-verify
3
+ Version: 0.1.0
4
+ Summary: Verifier for composed verification-state envelopes (RFC 8785 JCS + EdDSA/ES256 JWS). Canonicalization is byte-identical to the production Node canonicalizer. Supersedes agentoracle-receipt-verify 0.1.0.
5
+ Author-email: Joe Krausz <joe@tanilo.io>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://tanilo.io
8
+ Project-URL: Repository, https://github.com/TKCollective/tanilo-receipt-verify
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: cryptography>=42.0.0
13
+ Dynamic: license-file
14
+
15
+ # tanilo-receipt-verify
16
+
17
+ 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.
18
+
19
+ **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.
20
+
21
+ **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.
22
+
23
+ ## Design goal
24
+
25
+ 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.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install tanilo-receipt-verify
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ **Key material is required to reach a verdict.** Pass `jwks_by_issuer`:
36
+
37
+ ```python
38
+ from tanilo_receipt_verify import verify
39
+
40
+ result = verify(envelope, jwks_by_issuer={
41
+ "https://agentoracle.co/.well-known/jwks.json": ao_jwks,
42
+ "https://agenttrust.uk/.well-known/jwks.json": at_jwks,
43
+ })
44
+
45
+ if result.status == "valid":
46
+ print("verified — canonical:", result.canonical_sha256)
47
+ ```
48
+
49
+ ### Three outcomes, not two
50
+
51
+ | `status` | `valid` | Meaning |
52
+ |---|---|---|
53
+ | `"valid"` | `True` | Every check ran and passed |
54
+ | `"invalid"` | `False` | A check ran and failed; see `.errors` |
55
+ | `"indeterminate"` | `None` | A check could not run; see `.indeterminate_reason` |
56
+
57
+ **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.
58
+
59
+ `None` is falsy, so `if result.valid:` fails closed. Branch on `.status` when you need to distinguish "failed" from "could not check".
60
+
61
+ ## What it checks
62
+
63
+ | Invariant | Description |
64
+ |---|---|
65
+ | `canonical_recomputes` | JCS(payload) → SHA-256 recomputes byte-identical to claimed |
66
+ | `decision_ref_recomputes` | `sha256(JCS(preimage))` matches published `decision_ref` (per invinoveritas/babyblueviper1 spec) |
67
+ | `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" |
68
+ | `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 |
69
+
70
+ ## Corrects a defect in `agentoracle-receipt-verify` 0.1.0 (AC-11)
71
+
72
+ "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](https://github.com/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.
73
+
74
+ `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.
75
+
76
+ 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.
77
+
78
+ This package resolves the two cases distinctly:
79
+
80
+ - **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.
81
+ - **Signer's `kid` found, signature verification actually run and fails** → that signer's entry reports `verified: False`. The envelope's overall status is `invalid`.
82
+
83
+ 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.
84
+
85
+ ## The two meanings of a missing key — `jwks_is_complete`
86
+
87
+ An unresolved `kid` can mean two different things, and this verifier cannot tell them apart on its own:
88
+
89
+ 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"`.
90
+ 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.
91
+
92
+ 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:
93
+
94
+ ```python
95
+ result = verify(
96
+ envelope,
97
+ jwks_by_issuer={"https://agentoracle.co/.well-known/jwks.json": ao_jwks},
98
+ jwks_is_complete=True, # this IS the whole trust list
99
+ )
100
+ # an unresolved kid now reports verified=False, is named in .errors,
101
+ # and the envelope's status is "invalid" — a policy refusal, not a gap
102
+ ```
103
+
104
+ `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.
105
+
106
+ Credit where it's due: this split converged out of a public thread rather than being invented here. [robertolocatelli81-dev](https://github.com/robertolocatelli81-dev) proposed the same distinction for JWKS lookups as `keys_are_complete` in `cryptovalid-opencore`, and [babyblueviper1](https://github.com/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](https://github.com/babyblueviper1/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.
107
+
108
+ ## Cross-language guarantees
109
+
110
+ The `tests/` suite includes byte-identical fixtures shared with the Node reference implementation:
111
+
112
+ - `test_jcs_byte_identical_to_node` — Python JCS output byte-matches Node output for a payload with nested objects, arrays, unicode, booleans, and integers.
113
+ - `test_decision_ref_recompute_babyblueviper1` — Python recomputes the shipped [invinoveritas fixture](https://github.com/babyblueviper1/preaction-governance-conformance/tree/3e54ee2/examples/decision-ref-recompute), byte-identical to her Python and our Node.
114
+ - `test_conformance_sample_canonical_hash` — reproduces the canonical hash from the production `/v1/conformance/sample` endpoint.
115
+
116
+ ## License
117
+
118
+ MIT — see `LICENSE`.
@@ -0,0 +1,104 @@
1
+ # tanilo-receipt-verify
2
+
3
+ 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.
4
+
5
+ **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.
6
+
7
+ **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.
8
+
9
+ ## Design goal
10
+
11
+ 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.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install tanilo-receipt-verify
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ **Key material is required to reach a verdict.** Pass `jwks_by_issuer`:
22
+
23
+ ```python
24
+ from tanilo_receipt_verify import verify
25
+
26
+ result = verify(envelope, jwks_by_issuer={
27
+ "https://agentoracle.co/.well-known/jwks.json": ao_jwks,
28
+ "https://agenttrust.uk/.well-known/jwks.json": at_jwks,
29
+ })
30
+
31
+ if result.status == "valid":
32
+ print("verified — canonical:", result.canonical_sha256)
33
+ ```
34
+
35
+ ### Three outcomes, not two
36
+
37
+ | `status` | `valid` | Meaning |
38
+ |---|---|---|
39
+ | `"valid"` | `True` | Every check ran and passed |
40
+ | `"invalid"` | `False` | A check ran and failed; see `.errors` |
41
+ | `"indeterminate"` | `None` | A check could not run; see `.indeterminate_reason` |
42
+
43
+ **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.
44
+
45
+ `None` is falsy, so `if result.valid:` fails closed. Branch on `.status` when you need to distinguish "failed" from "could not check".
46
+
47
+ ## What it checks
48
+
49
+ | Invariant | Description |
50
+ |---|---|
51
+ | `canonical_recomputes` | JCS(payload) → SHA-256 recomputes byte-identical to claimed |
52
+ | `decision_ref_recomputes` | `sha256(JCS(preimage))` matches published `decision_ref` (per invinoveritas/babyblueviper1 spec) |
53
+ | `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" |
54
+ | `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 |
55
+
56
+ ## Corrects a defect in `agentoracle-receipt-verify` 0.1.0 (AC-11)
57
+
58
+ "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](https://github.com/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.
59
+
60
+ `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.
61
+
62
+ 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.
63
+
64
+ This package resolves the two cases distinctly:
65
+
66
+ - **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.
67
+ - **Signer's `kid` found, signature verification actually run and fails** → that signer's entry reports `verified: False`. The envelope's overall status is `invalid`.
68
+
69
+ 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.
70
+
71
+ ## The two meanings of a missing key — `jwks_is_complete`
72
+
73
+ An unresolved `kid` can mean two different things, and this verifier cannot tell them apart on its own:
74
+
75
+ 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"`.
76
+ 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.
77
+
78
+ 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:
79
+
80
+ ```python
81
+ result = verify(
82
+ envelope,
83
+ jwks_by_issuer={"https://agentoracle.co/.well-known/jwks.json": ao_jwks},
84
+ jwks_is_complete=True, # this IS the whole trust list
85
+ )
86
+ # an unresolved kid now reports verified=False, is named in .errors,
87
+ # and the envelope's status is "invalid" — a policy refusal, not a gap
88
+ ```
89
+
90
+ `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.
91
+
92
+ Credit where it's due: this split converged out of a public thread rather than being invented here. [robertolocatelli81-dev](https://github.com/robertolocatelli81-dev) proposed the same distinction for JWKS lookups as `keys_are_complete` in `cryptovalid-opencore`, and [babyblueviper1](https://github.com/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](https://github.com/babyblueviper1/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.
93
+
94
+ ## Cross-language guarantees
95
+
96
+ The `tests/` suite includes byte-identical fixtures shared with the Node reference implementation:
97
+
98
+ - `test_jcs_byte_identical_to_node` — Python JCS output byte-matches Node output for a payload with nested objects, arrays, unicode, booleans, and integers.
99
+ - `test_decision_ref_recompute_babyblueviper1` — Python recomputes the shipped [invinoveritas fixture](https://github.com/babyblueviper1/preaction-governance-conformance/tree/3e54ee2/examples/decision-ref-recompute), byte-identical to her Python and our Node.
100
+ - `test_conformance_sample_canonical_hash` — reproduces the canonical hash from the production `/v1/conformance/sample` endpoint.
101
+
102
+ ## License
103
+
104
+ MIT — see `LICENSE`.
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "tanilo-receipt-verify"
3
+ version = "0.1.0"
4
+ description = "Verifier for composed verification-state envelopes (RFC 8785 JCS + EdDSA/ES256 JWS). Canonicalization is byte-identical to the production Node canonicalizer. Supersedes agentoracle-receipt-verify 0.1.0."
5
+ authors = [{ name = "Joe Krausz", email = "joe@tanilo.io" }]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ requires-python = ">=3.9"
10
+ dependencies = [
11
+ "cryptography>=42.0.0"
12
+ ]
13
+
14
+ [project.urls]
15
+ Homepage = "https://tanilo.io"
16
+ Repository = "https://github.com/TKCollective/tanilo-receipt-verify"
17
+
18
+ [build-system]
19
+ requires = ["setuptools>=77"]
20
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,47 @@
1
+ """Tanilo composed-envelope verifier (Python).
2
+
3
+ Canonicalizes with RFC 8785 JCS, verifies Ed25519 JWS signatures against
4
+ supplied JWKS, and checks recompute-invariants. JCS output and SHA-256 are
5
+ byte-identical to the production Node canonicalizer; see tests/.
6
+
7
+ IMPORTANT: `verify()` requires `jwks_by_issuer` to reach a valid/invalid
8
+ verdict. Called without key material on a signed envelope it returns
9
+ `status="indeterminate"` / `valid=None` -- never `valid=True`. Recompute
10
+ alone does not bind a payload to an issuer.
11
+
12
+ Also in this release: a signer whose key material could not be resolved
13
+ (no matching `kid` in the supplied JWKS for that issuer) now reports
14
+ `verified: None` on that signer specifically -- not `verified: False`.
15
+ A composed envelope with JWKS for some but not all signers previously
16
+ adjudicated the whole envelope `invalid`, collapsing "we never checked
17
+ this signer" into "this signer's signature failed." See CHANGELOG.
18
+
19
+ Public API:
20
+ verify(envelope: dict, jwks_by_issuer: dict | None) -> VerifyResult
21
+ jcs(v) -> str
22
+ sha256_hex(s: str) -> str
23
+ recompute_decision_ref(decision_ref_slot: dict) -> str
24
+ """
25
+
26
+ from .verify import (
27
+ verify,
28
+ jcs,
29
+ sha256_hex,
30
+ recompute_decision_ref,
31
+ VerifyResult,
32
+ STATUS_VALID,
33
+ STATUS_INVALID,
34
+ STATUS_INDETERMINATE,
35
+ )
36
+
37
+ __all__ = [
38
+ "verify",
39
+ "jcs",
40
+ "sha256_hex",
41
+ "recompute_decision_ref",
42
+ "VerifyResult",
43
+ "STATUS_VALID",
44
+ "STATUS_INVALID",
45
+ "STATUS_INDETERMINATE",
46
+ ]
47
+ __version__ = "0.1.0"