proofbundle 1.1.0__tar.gz → 1.3.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 (68) hide show
  1. {proofbundle-1.1.0/src/proofbundle.egg-info → proofbundle-1.3.0}/PKG-INFO +133 -25
  2. {proofbundle-1.1.0 → proofbundle-1.3.0}/README.md +128 -24
  3. {proofbundle-1.1.0 → proofbundle-1.3.0}/pyproject.toml +7 -1
  4. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/__init__.py +18 -2
  5. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/adapters/lm_eval.py +3 -1
  6. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/bundle.py +85 -3
  7. proofbundle-1.3.0/src/proofbundle/checkpoint.py +466 -0
  8. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/cli.py +71 -5
  9. proofbundle-1.3.0/src/proofbundle/kbjwt.py +207 -0
  10. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/sdjwt.py +7 -3
  11. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/sdjwt_issue.py +62 -2
  12. proofbundle-1.3.0/src/proofbundle/statuslist.py +194 -0
  13. proofbundle-1.3.0/src/proofbundle/tlogproof.py +175 -0
  14. {proofbundle-1.1.0 → proofbundle-1.3.0/src/proofbundle.egg-info}/PKG-INFO +133 -25
  15. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/SOURCES.txt +9 -1
  16. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/requires.txt +3 -0
  17. proofbundle-1.3.0/tests/test_cli.py +91 -0
  18. proofbundle-1.3.0/tests/test_cosignature.py +159 -0
  19. proofbundle-1.3.0/tests/test_cosignature_mldsa.py +153 -0
  20. proofbundle-1.3.0/tests/test_kbjwt.py +289 -0
  21. proofbundle-1.3.0/tests/test_statuslist.py +143 -0
  22. proofbundle-1.3.0/tests/test_tlogproof.py +163 -0
  23. proofbundle-1.1.0/src/proofbundle/checkpoint.py +0 -157
  24. proofbundle-1.1.0/tests/test_cli.py +0 -40
  25. {proofbundle-1.1.0 → proofbundle-1.3.0}/LICENSE +0 -0
  26. {proofbundle-1.1.0 → proofbundle-1.3.0}/setup.cfg +0 -0
  27. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/_inspect_registry.py +0 -0
  28. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/_integration.py +0 -0
  29. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/adapters/__init__.py +0 -0
  30. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/adapters/eee.py +0 -0
  31. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
  32. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/dsse.py +0 -0
  33. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/eee_eval_schema.json +0 -0
  34. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/emit.py +0 -0
  35. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/errors.py +0 -0
  36. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/evalclaim.py +0 -0
  37. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/inspect_hook.py +0 -0
  38. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/intoto.py +0 -0
  39. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/merkle.py +0 -0
  40. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/py.typed +0 -0
  41. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/pytest_plugin.py +0 -0
  42. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle/signature.py +0 -0
  43. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
  44. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
  45. {proofbundle-1.1.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/top_level.txt +0 -0
  46. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_adapters.py +0 -0
  47. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_adversarial.py +0 -0
  48. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_bundle.py +0 -0
  49. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_bundle_robustness.py +0 -0
  50. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_checkpoint.py +0 -0
  51. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_cli_eval.py +0 -0
  52. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_eee.py +0 -0
  53. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_emit.py +0 -0
  54. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_eval_claim_schema.py +0 -0
  55. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_evalclaim.py +0 -0
  56. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_examples.py +0 -0
  57. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_inspect_hook.py +0 -0
  58. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_intoto.py +0 -0
  59. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_intoto_dsse.py +0 -0
  60. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_merkle.py +0 -0
  61. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_merkle_property.py +0 -0
  62. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_pytest_plugin.py +0 -0
  63. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_rekor_interop.py +0 -0
  64. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_rfc6962_external_vectors.py +0 -0
  65. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_schema.py +0 -0
  66. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_sdjwt_issue.py +0 -0
  67. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_sdjwt_reference.py +0 -0
  68. {proofbundle-1.1.0 → proofbundle-1.3.0}/tests/test_signature.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proofbundle
3
- Version: 1.1.0
3
+ Version: 1.3.0
4
4
  Summary: Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT.
5
5
  Author: Konrad Gruszka
6
6
  License: MIT
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3.9
18
18
  Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
21
23
  Classifier: Topic :: Security :: Cryptography
22
24
  Requires-Python: >=3.9
23
25
  Description-Content-Type: text/markdown
@@ -27,6 +29,8 @@ Provides-Extra: sdjwt
27
29
  Provides-Extra: eval
28
30
  Requires-Dist: rfc8785>=0.1.4; extra == "eval"
29
31
  Provides-Extra: adapters
32
+ Provides-Extra: pq
33
+ Requires-Dist: cryptography>=48; extra == "pq"
30
34
  Provides-Extra: pytest
31
35
  Requires-Dist: pytest>=7; extra == "pytest"
32
36
  Provides-Extra: inspect
@@ -53,9 +57,11 @@ Dynamic: license-file
53
57
  <h1>proofbundle</h1>
54
58
 
55
59
  **An offline verifier for AI eval receipts. Standards-native: Ed25519 signature,
56
- RFC 6962 transparency-log Merkle anchoring, optional SD-JWT (RFC 9901) selective
57
- disclosure, aligned to the in-toto test-result predicate. One portable JSON file,
58
- no server, no network.**
60
+ RFC 6962 transparency-log Merkle anchoring, C2SP witnessed checkpoints and
61
+ `.tlog-proof` transparent signatures (post-quantum-ready witnesses, ML-DSA-44),
62
+ SD-JWT (RFC 9901) selective disclosure with Key Binding, Token-Status-List
63
+ revocation snapshots, aligned to the in-toto test-result predicate. One portable
64
+ file, no server, no network.**
59
65
 
60
66
  [![CI](https://github.com/b7n0de/proofbundle/actions/workflows/ci.yml/badge.svg)](https://github.com/b7n0de/proofbundle/actions/workflows/ci.yml)
61
67
  [![PyPI](https://img.shields.io/pypi/v/proofbundle.svg?color=D6248A&cacheSeconds=3600)](https://pypi.org/project/proofbundle/)
@@ -65,6 +71,9 @@ no server, no network.**
65
71
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
66
72
  [![SLSA build provenance](https://img.shields.io/badge/SLSA-build_provenance-D6248A.svg)](https://slsa.dev)
67
73
  [![PyPI attestations](https://img.shields.io/badge/PyPI-attestations_(PEP_740)-D6248A.svg)](https://pypi.org/project/proofbundle/)
74
+ [![Post-quantum witnesses](https://img.shields.io/badge/PQ-ML--DSA--44_cosignatures-D6248A.svg)](https://github.com/C2SP/C2SP/blob/main/tlog-cosignature.md)
75
+ [![Mutation tested](https://img.shields.io/badge/tests-mutation_gated-D6248A.svg)](scripts/mutation_check.py)
76
+ [![C2SP tlog-proof](https://img.shields.io/badge/C2SP-tlog--proof-D6248A.svg)](https://github.com/C2SP/C2SP/blob/main/tlog-proof.md)
68
77
  <!-- DOI badge placeholder: enable Zenodo archiving for this repo, then add the Zenodo concept-DOI
69
78
  badge here (and the DOI to CITATION.cff) once Zenodo assigns one on the next release. No DOI has
70
79
  been assigned yet (no archived record exists at build time) — tracked in the human checklist. -->
@@ -73,11 +82,12 @@ no server, no network.**
73
82
 
74
83
  **At a glance:** `proofbundle emit` signs and anchors a payload; `proofbundle
75
84
  verify` checks one self-contained `bundle.json` with three offline cryptographic
76
- checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 102 tests.
85
+ checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 188 tests + a CI mutation gate.
77
86
 
78
87
  ## Contents
79
88
 
80
89
  - [Why](#why)
90
+ - [What a receipt proves](#what-a-receipt-proves-and-what-it-does-not)
81
91
  - [What it verifies](#what-it-verifies)
82
92
  - [How it fits together](#how-it-fits-together)
83
93
  - [Install](#install)
@@ -88,6 +98,7 @@ checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 102 tests.
88
98
  - [Bundle format](#bundle-format-proofbundlev01)
89
99
  - [Eval receipts](#eval-receipts)
90
100
  - [Security notes and scope](#security-notes-and-scope-stated-honestly)
101
+ - [Compliance](#compliance)
91
102
  - [Roadmap](#roadmap)
92
103
  - [Contributing](#contributing)
93
104
  - [License](#license)
@@ -148,6 +159,12 @@ A bundle is a single JSON document. `proofbundle` checks, offline:
148
159
  Certificate Transparency)
149
160
  3. **sd-jwt** (optional) — an embedded SD-JWT selective-disclosure credential is
150
161
  well formed, and if an issuer key is given, correctly issuer-signed
162
+ 4. **sd-jwt-key-binding** (optional, v1.2) — if the SD-JWT carries a Key Binding
163
+ JWT (RFC 9901 §4.3), it is verified **fail-closed**: `typ` is `kb+jwt`,
164
+ `iat`/`aud`/`nonce`/`sd_hash` are present, `sd_hash` binds the exact presented
165
+ disclosure set, and the signature verifies under the issuer-bound `cnf.jwk`
166
+ holder key. A present-but-broken KB-JWT fails the bundle — it is never
167
+ silently ignored.
151
168
 
152
169
  The verifier treats the payload as opaque bytes. It proves that these exact
153
170
  bytes were signed and anchored, not what they mean. That is on purpose: it keeps
@@ -160,16 +177,18 @@ flowchart LR
160
177
  P["payload bytes"]
161
178
  P -->|"Ed25519 sign"| S["signature"]
162
179
  P -->|"RFC 6962 anchor"| M["Merkle inclusion proof"]
163
- SD["SD-JWT VC (optional)"] -.-> B
180
+ SD["SD-JWT VC + KB-JWT (optional)"] -.-> B
164
181
  S --> B["bundle.json"]
165
182
  M --> B
166
183
  B --> V{{"proofbundle verify"}}
167
184
  V --> C1["ed25519-signature"]
168
185
  V --> C2["merkle-inclusion"]
169
186
  V --> C3["sd-jwt (optional)"]
187
+ V --> C4["key binding (optional)"]
170
188
  C1 --> R{"all checks pass?"}
171
189
  C2 --> R
172
190
  C3 --> R
191
+ C4 --> R
173
192
  R -->|yes| OK(["=> OK &nbsp; exit 0"])
174
193
  R -->|no| FAIL(["=> FAILED &nbsp; exit 1"])
175
194
 
@@ -206,7 +225,7 @@ proofbundle verify examples/example_bundle.json
206
225
  ```
207
226
 
208
227
  <div align="center">
209
- <img src="assets/demo.svg" alt="proofbundle verify output: four PASS checks and OK" width="680">
228
+ <img src="assets/demo.svg" alt="proofbundle verify output: PASS checks and OK" width="680">
210
229
  </div>
211
230
 
212
231
  Machine-readable output and a non-zero exit code on failure:
@@ -215,6 +234,14 @@ Machine-readable output and a non-zero exit code on failure:
215
234
  proofbundle verify --json bundle.json # exit 0 = ok, 1 = failed, 2 = malformed
216
235
  ```
217
236
 
237
+ Debugging an inclusion proof (v1.2): `--verbose` prints the recomputed Merkle
238
+ root next to the stated root, so a `FAIL` shows *which* root your payload
239
+ actually anchors to:
240
+
241
+ ```bash
242
+ proofbundle verify --verbose bundle.json
243
+ ```
244
+
218
245
  Emit a bundle of your own (v0.2): sign a payload with a fresh key and anchor it,
219
246
  then verify it anywhere, offline.
220
247
 
@@ -289,6 +316,48 @@ vectors vendored from
289
316
  [transparency-dev/merkle](https://github.com/transparency-dev/merkle) (see
290
317
  `tests/fixtures/`), plus Hypothesis property tests.
291
318
 
319
+ Since v1.2 proofbundle also speaks the witness layer:
320
+ [C2SP tlog-cosignature](https://github.com/C2SP/C2SP/blob/main/tlog-cosignature.md)
321
+ (Ed25519 cosignature/v1) — `verify_witnessed_checkpoint` checks a checkpoint is
322
+ both log-signed **and** cosigned by a quorum of distinct witnesses, offline,
323
+ which rules out a split view by the log operator. This is the same
324
+ witnessed-checkpoint pattern Rekor v2 (GA October 2025) institutionalizes.
325
+
326
+ Since v1.3 the witness layer is complete: **ML-DSA-44 cosignatures** (C2SP type
327
+ 0x06, FIPS 204 — post-quantum, the spec's SHOULD for new witness deployments;
328
+ optional `proofbundle[pq]` extra, Ed25519 stays the default) and the
329
+ **[C2SP tlog-proof](https://github.com/C2SP/C2SP/blob/main/tlog-proof.md)** file
330
+ format (`.tlog-proof`) — index + inclusion proof + (co)signed checkpoint in one
331
+ portable, offline-verifiable "transparent signature":
332
+
333
+ ```bash
334
+ proofbundle verify-proof receipt.tlog-proof --payload-file result.json \
335
+ --log-vkey <log-vkey> --witness-vkey <w1> --witness-vkey <w2> --threshold 2
336
+ ```
337
+
338
+ Revocation joins the offline model too: a receipt SD-JWT can carry a Token
339
+ Status List `status` claim, checked against a signed, **bundled list snapshot**
340
+ (`proofbundle.statuslist`) — staleness is reported explicitly instead of
341
+ assumed away.
342
+
343
+ ```mermaid
344
+ flowchart LR
345
+ B["bundle.json<br/>(signed + anchored receipt)"] --> CK["tlog-checkpoint<br/>(signed note over the root)"]
346
+ CK --> W1["witness cosignature<br/>Ed25519 · 0x04"]
347
+ CK --> W2["witness cosignature<br/>ML-DSA-44 · 0x06 · PQ"]
348
+ B -->|"index + inclusion proof"| TP[".tlog-proof<br/>transparent signature"]
349
+ W1 --> TP
350
+ W2 --> TP
351
+ TP --> V{{"proofbundle verify-proof<br/>offline · k-of-n policy"}}
352
+ V --> R1["log signature"]
353
+ V --> R2["witness quorum"]
354
+ V --> R3["merkle inclusion"]
355
+
356
+ style V fill:#D6248A,stroke:#D6248A,color:#fff
357
+ style W2 fill:#1e2327,stroke:#D6248A,color:#fff
358
+ style TP fill:#1e2327,stroke:#D6248A,color:#fff
359
+ ```
360
+
292
361
  ## Bundle format (`proofbundle/v0.1`)
293
362
 
294
363
  The format is specified normatively in [SPEC.md](SPEC.md) (fields, encodings,
@@ -312,23 +381,37 @@ RFC 6962 hashing, verification order) with a machine-readable JSON Schema at
312
381
  ```
313
382
 
314
383
  `sd_jwt_vc` is optional. Base64 fields are standard base64; the SD-JWT compact
315
- string uses base64url as per the spec.
384
+ string uses base64url as per the spec. The compact string MAY end in a Key
385
+ Binding JWT (instead of the trailing `~`), which is then verified (v1.2).
316
386
 
317
387
  ## Security notes and scope, stated honestly
318
388
 
319
389
  The scope is deliberately narrow. It does exactly what it says and no more:
320
390
 
321
- - Ed25519 signatures only, for both the payload and the optional SD-JWT issuer
322
- signature.
391
+ - Ed25519 signatures only, for the payload, the optional SD-JWT issuer
392
+ signature, and the optional KB-JWT holder signature.
323
393
  - SD-JWT: the SD-JWT core is now [RFC 9901](https://datatracker.ietf.org/doc/rfc9901/)
324
394
  (November 2025); this verifies that every presented disclosure is committed in the
325
- issuer-signed payload, and the issuer signature (EdDSA) if a key is supplied. It
326
- does **not** verify a Key Binding JWT, an X.509 or trust-list chain, status
327
- lists, or `vct` type metadata. **SD-JWT VC** (the credential-type profile) is
328
- still an IETF draft ([draft-ietf-oauth-sd-jwt-vc](https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/));
395
+ issuer-signed payload, the issuer signature (EdDSA) if a key is supplied, and — since
396
+ v1.2, fail-closed — a Key Binding JWT if one is attached (RFC 9901 §4.3: `kb+jwt`
397
+ typ, required `iat`/`aud`/`nonce`/`sd_hash`, `sd_hash` over the presented disclosure
398
+ set, holder signature under the issuer-bound `cnf.jwk`). `aud`/`nonce` *values* and
399
+ `iat` freshness are relying-party policy (an offline verifier has no trusted clock);
400
+ pass `expected_aud`/`expected_nonce` to `verify_key_binding` to enforce them. It does
401
+ **not** verify an X.509 or trust-list chain, status lists, or `vct` type metadata.
402
+ **SD-JWT VC** (the credential-type profile) is still an IETF draft
403
+ ([draft-ietf-oauth-sd-jwt-vc](https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/));
329
404
  full VC conformance is on the roadmap.
405
+ - Witness cosignatures (v1.2) prove consistency *observations* by the listed
406
+ witnesses; real split-view resistance additionally requires the witnesses to be
407
+ operationally independent — a deployment property no file format can supply.
408
+ - Post-quantum: witness **cosignatures** may be ML-DSA-44 (v1.3, `[pq]` extra) — the layer where
409
+ the ecosystem (C2SP, Sigstore) is deploying PQ first. Primary signatures stay Ed25519 until the
410
+ ecosystem moves; a configured ML-DSA witness on a build without PQ support fails closed.
411
+ - Revocation (v1.3) is checked against a SIGNED status-list snapshot the relying party supplies;
412
+ its freshness is reported and only judged against the caller's clock — never assumed.
330
413
  - The verifier does not fetch anything. Trust anchors (the signer key, the
331
- expected root) are inputs you supply out of band.
414
+ expected root, witness keys) are inputs you supply out of band.
332
415
  - No custom cryptography. Ed25519 comes from `cryptography`; Merkle hashing is
333
416
  RFC 6962.
334
417
 
@@ -404,13 +487,17 @@ threshold was met without revealing the model or the data. See [INTEROP.md](INTE
404
487
  in-toto verifier understands it. Alongside the self-hosted-predicate `to_intoto_statement` (see
405
488
  [PREDICATE.md](PREDICATE.md)). Metric details live in `annotations` (test-result has no native metric
406
489
  field); the model/dataset stay salted commitments, never `sha256`.
407
- - **C2SP tlog-checkpoint** (v0.9) — `proofbundle.checkpoint.sign_checkpoint(origin, tree_size, root, …)`
408
- emits a valid [C2SP](https://github.com/C2SP/C2SP/blob/main/tlog-checkpoint.md) signed note over the
409
- RFC 6962 Merkle root, making a receipt witness-network / transparency-log compatible. Pure serialization
410
- over the Ed25519 key already in use — no new crypto.
411
- - **SD-JWT issuance** (RFC 9901, verified Nov 2025) — `proofbundle.sdjwt_issue.issue_sd_jwt(claim, signer,
412
- root_b64=…, exact_score=…)` issues the receipt so a holder can disclose `passed` +
413
- `threshold` while **withholding the exact score** and the identifier openings. The digest mechanic is
490
+ - **C2SP tlog-checkpoint + cosignatures** (v0.9/v1.2) — `proofbundle.checkpoint.sign_checkpoint(origin,
491
+ tree_size, root, …)` emits a valid [C2SP](https://github.com/C2SP/C2SP/blob/main/tlog-checkpoint.md)
492
+ signed note over the RFC 6962 Merkle root; `cosign_checkpoint` / `verify_witnessed_checkpoint` add and
493
+ check Ed25519 cosignature/v1 witness cosignatures with a quorum threshold, making a receipt
494
+ witness-network / transparency-log compatible. Pure serialization over the Ed25519 keys already in use
495
+ — no new crypto.
496
+ - **SD-JWT issuance + Key Binding** (RFC 9901) — `proofbundle.sdjwt_issue.issue_sd_jwt(claim, signer,
497
+ root_b64=…, exact_score=…, holder_public_key=…)` issues the receipt so a holder can disclose `passed` +
498
+ `threshold` while **withholding the exact score** and the identifier openings, optionally binding a
499
+ holder key via `cnf.jwk`; `present_with_key_binding` builds the holder presentation and
500
+ `proofbundle.kbjwt.verify_key_binding` verifies it (v1.2). The digest mechanic is
414
501
  RFC 9901 §4.2.3 (base64url of SHA-256 over the base64url-encoded Disclosure), cross-checked against the
415
502
  `sd-jwt-python` reference.
416
503
  The signed bundle payload is always the source of truth; the SD-JWT and the in-toto export are derived,
@@ -419,6 +506,16 @@ bundle-bound views.
419
506
  Every release ships **PEP 740 attestations** (Trusted Publishing) + an SLSA build-provenance
420
507
  attestation — see [SECURITY.md](SECURITY.md).
421
508
 
509
+ ## Compliance
510
+
511
+ The EU AI Act's record-keeping duty for high-risk systems (Article 12 — automatic,
512
+ tamper-evident, independently verifiable logs) applies from **August 2, 2026**;
513
+ the GPAI Code of Practice expects documented, credible model evaluations.
514
+ **[COMPLIANCE.md](COMPLIANCE.md)** maps — honestly, and without claiming more
515
+ than the cryptography supports — what a verified receipt covers under Article 12,
516
+ NIST AI RMF MEASURE, and the logging standards in flight (prEN 18229-1,
517
+ ISO/IEC DIS 24970), including the anti-patterns no one should claim.
518
+
422
519
  ## Roadmap
423
520
 
424
521
  - **v0.1** — the offline verifier plus a real example bundle.
@@ -437,10 +534,21 @@ attestation — see [SECURITY.md](SECURITY.md).
437
534
  the RFC 6962 root, an Every Eval Ever converter, and standards-native repositioning.
438
535
  - **v1.0** — distribution: opt-in framework integrations that auto-emit a signed receipt of an inspect_ai
439
536
  eval (end-of-task hook) or a pytest run (pytest11 plugin), plus a composite GitHub Action.
440
- - **v1.1 (current release)** — trust hardening: a signed `assurance_level`, a THREAT_MODEL, a self_attested-
537
+ - **v1.1** — trust hardening: a signed `assurance_level`, a THREAT_MODEL, a self_attested-
441
538
  without-prereg warning, model-swap + replay + withheld-field checks, and an adversarial No-Fake-PASS suite.
442
- - **Deferred** (explicitly not yet built) — SD-JWT VC conformance + `vct` metadata,
443
- Key-Binding JWT, status lists / revocation, an official in-toto PR, DSSE / a full in-toto client.
539
+ - **v1.2** — holder binding + witness quorum: **Key Binding JWT** verification
540
+ (RFC 9901 §4.3, fail-closed; closes #1) with `cnf.jwk` issuance and holder presentation, **C2SP
541
+ tlog-cosignature** verification (Ed25519 cosignature/v1, witness quorum, split-view resistance), and
542
+ `verify --verbose` with the recomputed Merkle root (closes #2).
543
+ - **v1.3 (current release)** — the portable proof: **C2SP tlog-proof** emit/verify (+
544
+ `verify-proof` CLI), **ML-DSA-44** witness cosignatures (post-quantum, `[pq]` extra), **Token
545
+ Status List** snapshot verification (offline revocation), SD-JWT VC markers (`dc+sd-jwt`,
546
+ `vct`, `status`), COMPLIANCE.md (EU AI Act Art. 12 / NIST AI RMF mapping), and the mutation
547
+ suite as a CI gate.
548
+ - **Deferred** (explicitly not yet built) — full SD-JWT VC conformance + `vct` type-metadata
549
+ resolution (pre-IESG), per-sample Merkle receipts (the THREAT_MODEL's named gap; v2.0
550
+ direction), an official in-toto eval predicate (proposal path via OpenSSF/CoSAI), a full
551
+ in-toto client, a Python-3.10 floor.
444
552
 
445
553
  ## Contributing
446
554
 
@@ -8,9 +8,11 @@
8
8
  <h1>proofbundle</h1>
9
9
 
10
10
  **An offline verifier for AI eval receipts. Standards-native: Ed25519 signature,
11
- RFC 6962 transparency-log Merkle anchoring, optional SD-JWT (RFC 9901) selective
12
- disclosure, aligned to the in-toto test-result predicate. One portable JSON file,
13
- no server, no network.**
11
+ RFC 6962 transparency-log Merkle anchoring, C2SP witnessed checkpoints and
12
+ `.tlog-proof` transparent signatures (post-quantum-ready witnesses, ML-DSA-44),
13
+ SD-JWT (RFC 9901) selective disclosure with Key Binding, Token-Status-List
14
+ revocation snapshots, aligned to the in-toto test-result predicate. One portable
15
+ file, no server, no network.**
14
16
 
15
17
  [![CI](https://github.com/b7n0de/proofbundle/actions/workflows/ci.yml/badge.svg)](https://github.com/b7n0de/proofbundle/actions/workflows/ci.yml)
16
18
  [![PyPI](https://img.shields.io/pypi/v/proofbundle.svg?color=D6248A&cacheSeconds=3600)](https://pypi.org/project/proofbundle/)
@@ -20,6 +22,9 @@ no server, no network.**
20
22
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
21
23
  [![SLSA build provenance](https://img.shields.io/badge/SLSA-build_provenance-D6248A.svg)](https://slsa.dev)
22
24
  [![PyPI attestations](https://img.shields.io/badge/PyPI-attestations_(PEP_740)-D6248A.svg)](https://pypi.org/project/proofbundle/)
25
+ [![Post-quantum witnesses](https://img.shields.io/badge/PQ-ML--DSA--44_cosignatures-D6248A.svg)](https://github.com/C2SP/C2SP/blob/main/tlog-cosignature.md)
26
+ [![Mutation tested](https://img.shields.io/badge/tests-mutation_gated-D6248A.svg)](scripts/mutation_check.py)
27
+ [![C2SP tlog-proof](https://img.shields.io/badge/C2SP-tlog--proof-D6248A.svg)](https://github.com/C2SP/C2SP/blob/main/tlog-proof.md)
23
28
  <!-- DOI badge placeholder: enable Zenodo archiving for this repo, then add the Zenodo concept-DOI
24
29
  badge here (and the DOI to CITATION.cff) once Zenodo assigns one on the next release. No DOI has
25
30
  been assigned yet (no archived record exists at build time) — tracked in the human checklist. -->
@@ -28,11 +33,12 @@ no server, no network.**
28
33
 
29
34
  **At a glance:** `proofbundle emit` signs and anchors a payload; `proofbundle
30
35
  verify` checks one self-contained `bundle.json` with three offline cryptographic
31
- checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 102 tests.
36
+ checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 188 tests + a CI mutation gate.
32
37
 
33
38
  ## Contents
34
39
 
35
40
  - [Why](#why)
41
+ - [What a receipt proves](#what-a-receipt-proves-and-what-it-does-not)
36
42
  - [What it verifies](#what-it-verifies)
37
43
  - [How it fits together](#how-it-fits-together)
38
44
  - [Install](#install)
@@ -43,6 +49,7 @@ checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 102 tests.
43
49
  - [Bundle format](#bundle-format-proofbundlev01)
44
50
  - [Eval receipts](#eval-receipts)
45
51
  - [Security notes and scope](#security-notes-and-scope-stated-honestly)
52
+ - [Compliance](#compliance)
46
53
  - [Roadmap](#roadmap)
47
54
  - [Contributing](#contributing)
48
55
  - [License](#license)
@@ -103,6 +110,12 @@ A bundle is a single JSON document. `proofbundle` checks, offline:
103
110
  Certificate Transparency)
104
111
  3. **sd-jwt** (optional) — an embedded SD-JWT selective-disclosure credential is
105
112
  well formed, and if an issuer key is given, correctly issuer-signed
113
+ 4. **sd-jwt-key-binding** (optional, v1.2) — if the SD-JWT carries a Key Binding
114
+ JWT (RFC 9901 §4.3), it is verified **fail-closed**: `typ` is `kb+jwt`,
115
+ `iat`/`aud`/`nonce`/`sd_hash` are present, `sd_hash` binds the exact presented
116
+ disclosure set, and the signature verifies under the issuer-bound `cnf.jwk`
117
+ holder key. A present-but-broken KB-JWT fails the bundle — it is never
118
+ silently ignored.
106
119
 
107
120
  The verifier treats the payload as opaque bytes. It proves that these exact
108
121
  bytes were signed and anchored, not what they mean. That is on purpose: it keeps
@@ -115,16 +128,18 @@ flowchart LR
115
128
  P["payload bytes"]
116
129
  P -->|"Ed25519 sign"| S["signature"]
117
130
  P -->|"RFC 6962 anchor"| M["Merkle inclusion proof"]
118
- SD["SD-JWT VC (optional)"] -.-> B
131
+ SD["SD-JWT VC + KB-JWT (optional)"] -.-> B
119
132
  S --> B["bundle.json"]
120
133
  M --> B
121
134
  B --> V{{"proofbundle verify"}}
122
135
  V --> C1["ed25519-signature"]
123
136
  V --> C2["merkle-inclusion"]
124
137
  V --> C3["sd-jwt (optional)"]
138
+ V --> C4["key binding (optional)"]
125
139
  C1 --> R{"all checks pass?"}
126
140
  C2 --> R
127
141
  C3 --> R
142
+ C4 --> R
128
143
  R -->|yes| OK(["=> OK &nbsp; exit 0"])
129
144
  R -->|no| FAIL(["=> FAILED &nbsp; exit 1"])
130
145
 
@@ -161,7 +176,7 @@ proofbundle verify examples/example_bundle.json
161
176
  ```
162
177
 
163
178
  <div align="center">
164
- <img src="assets/demo.svg" alt="proofbundle verify output: four PASS checks and OK" width="680">
179
+ <img src="assets/demo.svg" alt="proofbundle verify output: PASS checks and OK" width="680">
165
180
  </div>
166
181
 
167
182
  Machine-readable output and a non-zero exit code on failure:
@@ -170,6 +185,14 @@ Machine-readable output and a non-zero exit code on failure:
170
185
  proofbundle verify --json bundle.json # exit 0 = ok, 1 = failed, 2 = malformed
171
186
  ```
172
187
 
188
+ Debugging an inclusion proof (v1.2): `--verbose` prints the recomputed Merkle
189
+ root next to the stated root, so a `FAIL` shows *which* root your payload
190
+ actually anchors to:
191
+
192
+ ```bash
193
+ proofbundle verify --verbose bundle.json
194
+ ```
195
+
173
196
  Emit a bundle of your own (v0.2): sign a payload with a fresh key and anchor it,
174
197
  then verify it anywhere, offline.
175
198
 
@@ -244,6 +267,48 @@ vectors vendored from
244
267
  [transparency-dev/merkle](https://github.com/transparency-dev/merkle) (see
245
268
  `tests/fixtures/`), plus Hypothesis property tests.
246
269
 
270
+ Since v1.2 proofbundle also speaks the witness layer:
271
+ [C2SP tlog-cosignature](https://github.com/C2SP/C2SP/blob/main/tlog-cosignature.md)
272
+ (Ed25519 cosignature/v1) — `verify_witnessed_checkpoint` checks a checkpoint is
273
+ both log-signed **and** cosigned by a quorum of distinct witnesses, offline,
274
+ which rules out a split view by the log operator. This is the same
275
+ witnessed-checkpoint pattern Rekor v2 (GA October 2025) institutionalizes.
276
+
277
+ Since v1.3 the witness layer is complete: **ML-DSA-44 cosignatures** (C2SP type
278
+ 0x06, FIPS 204 — post-quantum, the spec's SHOULD for new witness deployments;
279
+ optional `proofbundle[pq]` extra, Ed25519 stays the default) and the
280
+ **[C2SP tlog-proof](https://github.com/C2SP/C2SP/blob/main/tlog-proof.md)** file
281
+ format (`.tlog-proof`) — index + inclusion proof + (co)signed checkpoint in one
282
+ portable, offline-verifiable "transparent signature":
283
+
284
+ ```bash
285
+ proofbundle verify-proof receipt.tlog-proof --payload-file result.json \
286
+ --log-vkey <log-vkey> --witness-vkey <w1> --witness-vkey <w2> --threshold 2
287
+ ```
288
+
289
+ Revocation joins the offline model too: a receipt SD-JWT can carry a Token
290
+ Status List `status` claim, checked against a signed, **bundled list snapshot**
291
+ (`proofbundle.statuslist`) — staleness is reported explicitly instead of
292
+ assumed away.
293
+
294
+ ```mermaid
295
+ flowchart LR
296
+ B["bundle.json<br/>(signed + anchored receipt)"] --> CK["tlog-checkpoint<br/>(signed note over the root)"]
297
+ CK --> W1["witness cosignature<br/>Ed25519 · 0x04"]
298
+ CK --> W2["witness cosignature<br/>ML-DSA-44 · 0x06 · PQ"]
299
+ B -->|"index + inclusion proof"| TP[".tlog-proof<br/>transparent signature"]
300
+ W1 --> TP
301
+ W2 --> TP
302
+ TP --> V{{"proofbundle verify-proof<br/>offline · k-of-n policy"}}
303
+ V --> R1["log signature"]
304
+ V --> R2["witness quorum"]
305
+ V --> R3["merkle inclusion"]
306
+
307
+ style V fill:#D6248A,stroke:#D6248A,color:#fff
308
+ style W2 fill:#1e2327,stroke:#D6248A,color:#fff
309
+ style TP fill:#1e2327,stroke:#D6248A,color:#fff
310
+ ```
311
+
247
312
  ## Bundle format (`proofbundle/v0.1`)
248
313
 
249
314
  The format is specified normatively in [SPEC.md](SPEC.md) (fields, encodings,
@@ -267,23 +332,37 @@ RFC 6962 hashing, verification order) with a machine-readable JSON Schema at
267
332
  ```
268
333
 
269
334
  `sd_jwt_vc` is optional. Base64 fields are standard base64; the SD-JWT compact
270
- string uses base64url as per the spec.
335
+ string uses base64url as per the spec. The compact string MAY end in a Key
336
+ Binding JWT (instead of the trailing `~`), which is then verified (v1.2).
271
337
 
272
338
  ## Security notes and scope, stated honestly
273
339
 
274
340
  The scope is deliberately narrow. It does exactly what it says and no more:
275
341
 
276
- - Ed25519 signatures only, for both the payload and the optional SD-JWT issuer
277
- signature.
342
+ - Ed25519 signatures only, for the payload, the optional SD-JWT issuer
343
+ signature, and the optional KB-JWT holder signature.
278
344
  - SD-JWT: the SD-JWT core is now [RFC 9901](https://datatracker.ietf.org/doc/rfc9901/)
279
345
  (November 2025); this verifies that every presented disclosure is committed in the
280
- issuer-signed payload, and the issuer signature (EdDSA) if a key is supplied. It
281
- does **not** verify a Key Binding JWT, an X.509 or trust-list chain, status
282
- lists, or `vct` type metadata. **SD-JWT VC** (the credential-type profile) is
283
- still an IETF draft ([draft-ietf-oauth-sd-jwt-vc](https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/));
346
+ issuer-signed payload, the issuer signature (EdDSA) if a key is supplied, and — since
347
+ v1.2, fail-closed — a Key Binding JWT if one is attached (RFC 9901 §4.3: `kb+jwt`
348
+ typ, required `iat`/`aud`/`nonce`/`sd_hash`, `sd_hash` over the presented disclosure
349
+ set, holder signature under the issuer-bound `cnf.jwk`). `aud`/`nonce` *values* and
350
+ `iat` freshness are relying-party policy (an offline verifier has no trusted clock);
351
+ pass `expected_aud`/`expected_nonce` to `verify_key_binding` to enforce them. It does
352
+ **not** verify an X.509 or trust-list chain, status lists, or `vct` type metadata.
353
+ **SD-JWT VC** (the credential-type profile) is still an IETF draft
354
+ ([draft-ietf-oauth-sd-jwt-vc](https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/));
284
355
  full VC conformance is on the roadmap.
356
+ - Witness cosignatures (v1.2) prove consistency *observations* by the listed
357
+ witnesses; real split-view resistance additionally requires the witnesses to be
358
+ operationally independent — a deployment property no file format can supply.
359
+ - Post-quantum: witness **cosignatures** may be ML-DSA-44 (v1.3, `[pq]` extra) — the layer where
360
+ the ecosystem (C2SP, Sigstore) is deploying PQ first. Primary signatures stay Ed25519 until the
361
+ ecosystem moves; a configured ML-DSA witness on a build without PQ support fails closed.
362
+ - Revocation (v1.3) is checked against a SIGNED status-list snapshot the relying party supplies;
363
+ its freshness is reported and only judged against the caller's clock — never assumed.
285
364
  - The verifier does not fetch anything. Trust anchors (the signer key, the
286
- expected root) are inputs you supply out of band.
365
+ expected root, witness keys) are inputs you supply out of band.
287
366
  - No custom cryptography. Ed25519 comes from `cryptography`; Merkle hashing is
288
367
  RFC 6962.
289
368
 
@@ -359,13 +438,17 @@ threshold was met without revealing the model or the data. See [INTEROP.md](INTE
359
438
  in-toto verifier understands it. Alongside the self-hosted-predicate `to_intoto_statement` (see
360
439
  [PREDICATE.md](PREDICATE.md)). Metric details live in `annotations` (test-result has no native metric
361
440
  field); the model/dataset stay salted commitments, never `sha256`.
362
- - **C2SP tlog-checkpoint** (v0.9) — `proofbundle.checkpoint.sign_checkpoint(origin, tree_size, root, …)`
363
- emits a valid [C2SP](https://github.com/C2SP/C2SP/blob/main/tlog-checkpoint.md) signed note over the
364
- RFC 6962 Merkle root, making a receipt witness-network / transparency-log compatible. Pure serialization
365
- over the Ed25519 key already in use — no new crypto.
366
- - **SD-JWT issuance** (RFC 9901, verified Nov 2025) — `proofbundle.sdjwt_issue.issue_sd_jwt(claim, signer,
367
- root_b64=…, exact_score=…)` issues the receipt so a holder can disclose `passed` +
368
- `threshold` while **withholding the exact score** and the identifier openings. The digest mechanic is
441
+ - **C2SP tlog-checkpoint + cosignatures** (v0.9/v1.2) — `proofbundle.checkpoint.sign_checkpoint(origin,
442
+ tree_size, root, …)` emits a valid [C2SP](https://github.com/C2SP/C2SP/blob/main/tlog-checkpoint.md)
443
+ signed note over the RFC 6962 Merkle root; `cosign_checkpoint` / `verify_witnessed_checkpoint` add and
444
+ check Ed25519 cosignature/v1 witness cosignatures with a quorum threshold, making a receipt
445
+ witness-network / transparency-log compatible. Pure serialization over the Ed25519 keys already in use
446
+ — no new crypto.
447
+ - **SD-JWT issuance + Key Binding** (RFC 9901) — `proofbundle.sdjwt_issue.issue_sd_jwt(claim, signer,
448
+ root_b64=…, exact_score=…, holder_public_key=…)` issues the receipt so a holder can disclose `passed` +
449
+ `threshold` while **withholding the exact score** and the identifier openings, optionally binding a
450
+ holder key via `cnf.jwk`; `present_with_key_binding` builds the holder presentation and
451
+ `proofbundle.kbjwt.verify_key_binding` verifies it (v1.2). The digest mechanic is
369
452
  RFC 9901 §4.2.3 (base64url of SHA-256 over the base64url-encoded Disclosure), cross-checked against the
370
453
  `sd-jwt-python` reference.
371
454
  The signed bundle payload is always the source of truth; the SD-JWT and the in-toto export are derived,
@@ -374,6 +457,16 @@ bundle-bound views.
374
457
  Every release ships **PEP 740 attestations** (Trusted Publishing) + an SLSA build-provenance
375
458
  attestation — see [SECURITY.md](SECURITY.md).
376
459
 
460
+ ## Compliance
461
+
462
+ The EU AI Act's record-keeping duty for high-risk systems (Article 12 — automatic,
463
+ tamper-evident, independently verifiable logs) applies from **August 2, 2026**;
464
+ the GPAI Code of Practice expects documented, credible model evaluations.
465
+ **[COMPLIANCE.md](COMPLIANCE.md)** maps — honestly, and without claiming more
466
+ than the cryptography supports — what a verified receipt covers under Article 12,
467
+ NIST AI RMF MEASURE, and the logging standards in flight (prEN 18229-1,
468
+ ISO/IEC DIS 24970), including the anti-patterns no one should claim.
469
+
377
470
  ## Roadmap
378
471
 
379
472
  - **v0.1** — the offline verifier plus a real example bundle.
@@ -392,10 +485,21 @@ attestation — see [SECURITY.md](SECURITY.md).
392
485
  the RFC 6962 root, an Every Eval Ever converter, and standards-native repositioning.
393
486
  - **v1.0** — distribution: opt-in framework integrations that auto-emit a signed receipt of an inspect_ai
394
487
  eval (end-of-task hook) or a pytest run (pytest11 plugin), plus a composite GitHub Action.
395
- - **v1.1 (current release)** — trust hardening: a signed `assurance_level`, a THREAT_MODEL, a self_attested-
488
+ - **v1.1** — trust hardening: a signed `assurance_level`, a THREAT_MODEL, a self_attested-
396
489
  without-prereg warning, model-swap + replay + withheld-field checks, and an adversarial No-Fake-PASS suite.
397
- - **Deferred** (explicitly not yet built) — SD-JWT VC conformance + `vct` metadata,
398
- Key-Binding JWT, status lists / revocation, an official in-toto PR, DSSE / a full in-toto client.
490
+ - **v1.2** — holder binding + witness quorum: **Key Binding JWT** verification
491
+ (RFC 9901 §4.3, fail-closed; closes #1) with `cnf.jwk` issuance and holder presentation, **C2SP
492
+ tlog-cosignature** verification (Ed25519 cosignature/v1, witness quorum, split-view resistance), and
493
+ `verify --verbose` with the recomputed Merkle root (closes #2).
494
+ - **v1.3 (current release)** — the portable proof: **C2SP tlog-proof** emit/verify (+
495
+ `verify-proof` CLI), **ML-DSA-44** witness cosignatures (post-quantum, `[pq]` extra), **Token
496
+ Status List** snapshot verification (offline revocation), SD-JWT VC markers (`dc+sd-jwt`,
497
+ `vct`, `status`), COMPLIANCE.md (EU AI Act Art. 12 / NIST AI RMF mapping), and the mutation
498
+ suite as a CI gate.
499
+ - **Deferred** (explicitly not yet built) — full SD-JWT VC conformance + `vct` type-metadata
500
+ resolution (pre-IESG), per-sample Merkle receipts (the THREAT_MODEL's named gap; v2.0
501
+ direction), an official in-toto eval predicate (proposal path via OpenSSF/CoSAI), a full
502
+ in-toto client, a Python-3.10 floor.
399
503
 
400
504
  ## Contributing
401
505
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proofbundle"
7
- version = "1.1.0"
7
+ version = "1.3.0"
8
8
  description = "Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -30,6 +30,8 @@ classifiers = [
30
30
  "Programming Language :: Python :: 3.10",
31
31
  "Programming Language :: Python :: 3.11",
32
32
  "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: 3.14",
33
35
  "Topic :: Security :: Cryptography",
34
36
  ]
35
37
  dependencies = ["cryptography>=42"]
@@ -45,6 +47,10 @@ sdjwt = []
45
47
  eval = ["rfc8785>=0.1.4"]
46
48
  # The lm-eval adapter reads exported results.json (no import) → pure stdlib.
47
49
  adapters = []
50
+ # ML-DSA-44 (FIPS 204) cosignature verification (C2SP tlog-cosignature type 0x06). Needs a
51
+ # cryptography build with OpenSSL 3.5+ (default PyPI wheels since 48.0.0). Verify-only by default;
52
+ # the emit helper needs the same. Ed25519 paths never require this.
53
+ pq = ["cryptography>=48"]
48
54
  # The pytest plugin (opt-in test-run receipt via the pytest11 entry-point) — pytest is an optional extra,
49
55
  # never a core dependency. Floor 7.0: the hooks/stats API is signature-stable across pytest 7/8/9.
50
56
  pytest = ["pytest>=7"]
@@ -13,17 +13,23 @@ from __future__ import annotations
13
13
 
14
14
  from typing import TYPE_CHECKING
15
15
 
16
- __version__ = "1.1.0"
16
+ __version__ = "1.3.0"
17
17
 
18
18
  __all__ = [
19
19
  "__version__",
20
20
  "SCHEMA",
21
21
  "verify_bundle",
22
22
  "load_bundle",
23
+ "recompute_merkle_root_b64",
23
24
  "emit_bundle",
24
25
  "generate_signer",
25
26
  "verify_inclusion",
26
27
  "verify_consistency",
28
+ "verify_key_binding",
29
+ "verify_tlog_proof",
30
+ "verify_cosignature",
31
+ "verify_witnessed_checkpoint",
32
+ "verify_status_snapshot",
27
33
  "VerificationResult",
28
34
  "Check",
29
35
  "ProofBundleError",
@@ -32,15 +38,25 @@ __all__ = [
32
38
  # name → backing submodule (relative). Loaded on first attribute access.
33
39
  _LAZY = {
34
40
  "SCHEMA": ".bundle", "load_bundle": ".bundle", "verify_bundle": ".bundle",
41
+ "recompute_merkle_root_b64": ".bundle",
35
42
  "emit_bundle": ".emit", "generate_signer": ".emit",
36
43
  "Check": ".errors", "ProofBundleError": ".errors", "VerificationResult": ".errors",
37
44
  "verify_consistency": ".merkle", "verify_inclusion": ".merkle",
45
+ "verify_key_binding": ".kbjwt",
46
+ "verify_tlog_proof": ".tlogproof",
47
+ "verify_cosignature": ".checkpoint",
48
+ "verify_witnessed_checkpoint": ".checkpoint",
49
+ "verify_status_snapshot": ".statuslist",
38
50
  }
39
51
 
40
52
  if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime stays lazy
41
- from .bundle import SCHEMA, load_bundle, verify_bundle
53
+ from .bundle import SCHEMA, load_bundle, recompute_merkle_root_b64, verify_bundle
42
54
  from .emit import emit_bundle, generate_signer
43
55
  from .errors import Check, ProofBundleError, VerificationResult
56
+ from .checkpoint import verify_cosignature, verify_witnessed_checkpoint
57
+ from .kbjwt import verify_key_binding
58
+ from .statuslist import verify_status_snapshot
59
+ from .tlogproof import verify_tlog_proof
44
60
  from .merkle import verify_consistency, verify_inclusion
45
61
 
46
62
 
@@ -50,7 +50,9 @@ def from_lm_eval_results(path, task: str, metric: str, *, comparator: str, thres
50
50
  if value is None:
51
51
  raise ValueError(f"metric {metric!r} not found in results[{task!r}] "
52
52
  f"(available: {sorted(k for k in res if ',' in k)})")
53
- score = value if isinstance(value, str) else repr(value)
53
+ # Fixed-point format (never scientific notation, e.g. '1e-05') — build_eval_claim rejects scientific
54
+ # notation, so repr() silently dropped legitimate small scores (release-review fix, mirrors v0.8.1).
55
+ score = value if isinstance(value, str) else (format(float(value), ".12f").rstrip("0").rstrip(".") or "0")
54
56
 
55
57
  n_samples = data.get("n-samples", {}).get(task, {})
56
58
  n = int(n_samples.get("effective") or n_samples.get("original") or res.get("sample_len") or 0)