proofbundle 1.2.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 (66) hide show
  1. {proofbundle-1.2.0/src/proofbundle.egg-info → proofbundle-1.3.0}/PKG-INFO +77 -10
  2. {proofbundle-1.2.0 → proofbundle-1.3.0}/README.md +72 -9
  3. {proofbundle-1.2.0 → proofbundle-1.3.0}/pyproject.toml +7 -1
  4. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/__init__.py +12 -1
  5. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/adapters/lm_eval.py +3 -1
  6. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/bundle.py +32 -14
  7. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/checkpoint.py +172 -31
  8. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/cli.py +59 -3
  9. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/kbjwt.py +9 -8
  10. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/sdjwt_issue.py +29 -3
  11. proofbundle-1.3.0/src/proofbundle/statuslist.py +194 -0
  12. proofbundle-1.3.0/src/proofbundle/tlogproof.py +175 -0
  13. {proofbundle-1.2.0 → proofbundle-1.3.0/src/proofbundle.egg-info}/PKG-INFO +77 -10
  14. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/SOURCES.txt +6 -1
  15. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/requires.txt +3 -0
  16. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_cosignature.py +15 -18
  17. proofbundle-1.3.0/tests/test_cosignature_mldsa.py +153 -0
  18. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_kbjwt.py +31 -5
  19. proofbundle-1.3.0/tests/test_statuslist.py +143 -0
  20. proofbundle-1.3.0/tests/test_tlogproof.py +163 -0
  21. {proofbundle-1.2.0 → proofbundle-1.3.0}/LICENSE +0 -0
  22. {proofbundle-1.2.0 → proofbundle-1.3.0}/setup.cfg +0 -0
  23. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/_inspect_registry.py +0 -0
  24. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/_integration.py +0 -0
  25. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/adapters/__init__.py +0 -0
  26. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/adapters/eee.py +0 -0
  27. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
  28. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/dsse.py +0 -0
  29. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/eee_eval_schema.json +0 -0
  30. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/emit.py +0 -0
  31. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/errors.py +0 -0
  32. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/evalclaim.py +0 -0
  33. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/inspect_hook.py +0 -0
  34. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/intoto.py +0 -0
  35. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/merkle.py +0 -0
  36. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/py.typed +0 -0
  37. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/pytest_plugin.py +0 -0
  38. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/sdjwt.py +0 -0
  39. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/signature.py +0 -0
  40. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
  41. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
  42. {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/top_level.txt +0 -0
  43. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_adapters.py +0 -0
  44. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_adversarial.py +0 -0
  45. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_bundle.py +0 -0
  46. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_bundle_robustness.py +0 -0
  47. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_checkpoint.py +0 -0
  48. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_cli.py +0 -0
  49. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_cli_eval.py +0 -0
  50. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_eee.py +0 -0
  51. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_emit.py +0 -0
  52. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_eval_claim_schema.py +0 -0
  53. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_evalclaim.py +0 -0
  54. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_examples.py +0 -0
  55. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_inspect_hook.py +0 -0
  56. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_intoto.py +0 -0
  57. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_intoto_dsse.py +0 -0
  58. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_merkle.py +0 -0
  59. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_merkle_property.py +0 -0
  60. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_pytest_plugin.py +0 -0
  61. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_rekor_interop.py +0 -0
  62. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_rfc6962_external_vectors.py +0 -0
  63. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_schema.py +0 -0
  64. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_sdjwt_issue.py +0 -0
  65. {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_sdjwt_reference.py +0 -0
  66. {proofbundle-1.2.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.2.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. 148 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. 148 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)
@@ -214,7 +225,7 @@ proofbundle verify examples/example_bundle.json
214
225
  ```
215
226
 
216
227
  <div align="center">
217
- <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">
218
229
  </div>
219
230
 
220
231
  Machine-readable output and a non-zero exit code on failure:
@@ -312,6 +323,41 @@ both log-signed **and** cosigned by a quorum of distinct witnesses, offline,
312
323
  which rules out a split view by the log operator. This is the same
313
324
  witnessed-checkpoint pattern Rekor v2 (GA October 2025) institutionalizes.
314
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
+
315
361
  ## Bundle format (`proofbundle/v0.1`)
316
362
 
317
363
  The format is specified normatively in [SPEC.md](SPEC.md) (fields, encodings,
@@ -359,6 +405,11 @@ The scope is deliberately narrow. It does exactly what it says and no more:
359
405
  - Witness cosignatures (v1.2) prove consistency *observations* by the listed
360
406
  witnesses; real split-view resistance additionally requires the witnesses to be
361
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.
362
413
  - The verifier does not fetch anything. Trust anchors (the signer key, the
363
414
  expected root, witness keys) are inputs you supply out of band.
364
415
  - No custom cryptography. Ed25519 comes from `cryptography`; Merkle hashing is
@@ -455,6 +506,16 @@ bundle-bound views.
455
506
  Every release ships **PEP 740 attestations** (Trusted Publishing) + an SLSA build-provenance
456
507
  attestation — see [SECURITY.md](SECURITY.md).
457
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
+
458
519
  ## Roadmap
459
520
 
460
521
  - **v0.1** — the offline verifier plus a real example bundle.
@@ -475,13 +536,19 @@ attestation — see [SECURITY.md](SECURITY.md).
475
536
  eval (end-of-task hook) or a pytest run (pytest11 plugin), plus a composite GitHub Action.
476
537
  - **v1.1** — trust hardening: a signed `assurance_level`, a THREAT_MODEL, a self_attested-
477
538
  without-prereg warning, model-swap + replay + withheld-field checks, and an adversarial No-Fake-PASS suite.
478
- - **v1.2 (current release)** — holder binding + witness quorum: **Key Binding JWT** verification
539
+ - **v1.2** — holder binding + witness quorum: **Key Binding JWT** verification
479
540
  (RFC 9901 §4.3, fail-closed; closes #1) with `cnf.jwk` issuance and holder presentation, **C2SP
480
541
  tlog-cosignature** verification (Ed25519 cosignature/v1, witness quorum, split-view resistance), and
481
542
  `verify --verbose` with the recomputed Merkle root (closes #2).
482
- - **Deferred** (explicitly not yet built) — SD-JWT VC conformance + `vct` metadata (not an RFC yet),
483
- Token Status List verification (draft in the RFC-Editor queue; planned as a bundled snapshot to stay
484
- offline), ML-DSA-44 cosignatures, an official in-toto PR, a full in-toto client.
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.
485
552
 
486
553
  ## Contributing
487
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. 148 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. 148 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)
@@ -169,7 +176,7 @@ proofbundle verify examples/example_bundle.json
169
176
  ```
170
177
 
171
178
  <div align="center">
172
- <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">
173
180
  </div>
174
181
 
175
182
  Machine-readable output and a non-zero exit code on failure:
@@ -267,6 +274,41 @@ both log-signed **and** cosigned by a quorum of distinct witnesses, offline,
267
274
  which rules out a split view by the log operator. This is the same
268
275
  witnessed-checkpoint pattern Rekor v2 (GA October 2025) institutionalizes.
269
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
+
270
312
  ## Bundle format (`proofbundle/v0.1`)
271
313
 
272
314
  The format is specified normatively in [SPEC.md](SPEC.md) (fields, encodings,
@@ -314,6 +356,11 @@ The scope is deliberately narrow. It does exactly what it says and no more:
314
356
  - Witness cosignatures (v1.2) prove consistency *observations* by the listed
315
357
  witnesses; real split-view resistance additionally requires the witnesses to be
316
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.
317
364
  - The verifier does not fetch anything. Trust anchors (the signer key, the
318
365
  expected root, witness keys) are inputs you supply out of band.
319
366
  - No custom cryptography. Ed25519 comes from `cryptography`; Merkle hashing is
@@ -410,6 +457,16 @@ bundle-bound views.
410
457
  Every release ships **PEP 740 attestations** (Trusted Publishing) + an SLSA build-provenance
411
458
  attestation — see [SECURITY.md](SECURITY.md).
412
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
+
413
470
  ## Roadmap
414
471
 
415
472
  - **v0.1** — the offline verifier plus a real example bundle.
@@ -430,13 +487,19 @@ attestation — see [SECURITY.md](SECURITY.md).
430
487
  eval (end-of-task hook) or a pytest run (pytest11 plugin), plus a composite GitHub Action.
431
488
  - **v1.1** — trust hardening: a signed `assurance_level`, a THREAT_MODEL, a self_attested-
432
489
  without-prereg warning, model-swap + replay + withheld-field checks, and an adversarial No-Fake-PASS suite.
433
- - **v1.2 (current release)** — holder binding + witness quorum: **Key Binding JWT** verification
490
+ - **v1.2** — holder binding + witness quorum: **Key Binding JWT** verification
434
491
  (RFC 9901 §4.3, fail-closed; closes #1) with `cnf.jwk` issuance and holder presentation, **C2SP
435
492
  tlog-cosignature** verification (Ed25519 cosignature/v1, witness quorum, split-view resistance), and
436
493
  `verify --verbose` with the recomputed Merkle root (closes #2).
437
- - **Deferred** (explicitly not yet built) — SD-JWT VC conformance + `vct` metadata (not an RFC yet),
438
- Token Status List verification (draft in the RFC-Editor queue; planned as a bundled snapshot to stay
439
- offline), ML-DSA-44 cosignatures, an official in-toto PR, a full in-toto client.
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.
440
503
 
441
504
  ## Contributing
442
505
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proofbundle"
7
- version = "1.2.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,7 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  from typing import TYPE_CHECKING
15
15
 
16
- __version__ = "1.2.0"
16
+ __version__ = "1.3.0"
17
17
 
18
18
  __all__ = [
19
19
  "__version__",
@@ -26,6 +26,10 @@ __all__ = [
26
26
  "verify_inclusion",
27
27
  "verify_consistency",
28
28
  "verify_key_binding",
29
+ "verify_tlog_proof",
30
+ "verify_cosignature",
31
+ "verify_witnessed_checkpoint",
32
+ "verify_status_snapshot",
29
33
  "VerificationResult",
30
34
  "Check",
31
35
  "ProofBundleError",
@@ -39,13 +43,20 @@ _LAZY = {
39
43
  "Check": ".errors", "ProofBundleError": ".errors", "VerificationResult": ".errors",
40
44
  "verify_consistency": ".merkle", "verify_inclusion": ".merkle",
41
45
  "verify_key_binding": ".kbjwt",
46
+ "verify_tlog_proof": ".tlogproof",
47
+ "verify_cosignature": ".checkpoint",
48
+ "verify_witnessed_checkpoint": ".checkpoint",
49
+ "verify_status_snapshot": ".statuslist",
42
50
  }
43
51
 
44
52
  if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime stays lazy
45
53
  from .bundle import SCHEMA, load_bundle, recompute_merkle_root_b64, verify_bundle
46
54
  from .emit import emit_bundle, generate_signer
47
55
  from .errors import Check, ProofBundleError, VerificationResult
56
+ from .checkpoint import verify_cosignature, verify_witnessed_checkpoint
48
57
  from .kbjwt import verify_key_binding
58
+ from .statuslist import verify_status_snapshot
59
+ from .tlogproof import verify_tlog_proof
49
60
  from .merkle import verify_consistency, verify_inclusion
50
61
 
51
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)
@@ -96,8 +96,15 @@ def load_bundle(path: str) -> dict:
96
96
  return json.load(handle)
97
97
 
98
98
 
99
- def verify_bundle(bundle: Union[dict, str]) -> VerificationResult:
100
- """Verify an evidence bundle (a dict or a path to a JSON file)."""
99
+ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce=None) -> VerificationResult:
100
+ """Verify an evidence bundle (a dict or a path to a JSON file).
101
+
102
+ ``expected_aud`` / ``expected_nonce`` (v1.3): when the bundle carries a Key Binding JWT, these enforce
103
+ RFC 9901 §7.3 replay/audience binding — the KB-JWT's ``aud`` MUST match ``expected_aud`` and its
104
+ ``nonce`` MUST match ``expected_nonce``. If omitted, the KB-JWT signature + disclosure binding are still
105
+ checked, but the relying party has NOT bound the presentation to itself/this transaction — a stale or
106
+ cross-audience replay would still verify. A relying party doing challenge-response MUST pass both.
107
+ """
101
108
  if isinstance(bundle, str):
102
109
  bundle = load_bundle(bundle)
103
110
  if not isinstance(bundle, dict):
@@ -148,6 +155,8 @@ def verify_bundle(bundle: Union[dict, str]) -> VerificationResult:
148
155
  sd = _require_dict(sd, "sd_jwt_vc")
149
156
  _reject_unknown(sd, _SD_KEYS, "sd_jwt_vc")
150
157
  compact = _require(sd, "compact", "sd_jwt_vc.compact")
158
+ if not isinstance(compact, str): # malformed input → BundleFormatError, never a raw traceback
159
+ raise BundleFormatError("field sd_jwt_vc.compact must be a string")
151
160
  issuer_pub = None
152
161
  if sd.get("issuer_public_key_b64"):
153
162
  issuer_pub = _b64d(sd["issuer_public_key_b64"], "sd_jwt_vc.issuer_public_key_b64")
@@ -159,18 +168,21 @@ def verify_bundle(bundle: Union[dict, str]) -> VerificationResult:
159
168
  sd_res["sig_ok"],
160
169
  "issuer signature valid" if sd_res["sig_ok"] else "issuer signature invalid",
161
170
  )
162
- # v1.2, fail-closed: a KB-JWT that is PRESENT must verify (RFC 9901 §4.3). Before
163
- # v1.2 a trailing KB-JWT was silently ignored a downgrade risk. Bundles without
164
- # a KB-JWT are untouched ONLY when the issuer did NOT bind a holder key: a v0.9/v1.0/
165
- # v1.1 bundle with no ``cnf`` verifies exactly as before.
166
- # CRITICAL fix (release review 2026-07-02): if the issuer-signed payload DOES carry a
167
- # ``cnf`` holder key (proof-of-possession REQUIRED by the issuer), a presentation with
168
- # NO Key Binding JWT is a bearer downgrade anyone who sees the disclosed SD-JWT could
169
- # replay it. That MUST fail, not silently pass.
170
- if isinstance(compact, str):
171
+ # v1.2/v1.3, fail-closed: a KB-JWT that is PRESENT must verify (RFC 9901 §4.3), AND if the issuer
172
+ # bound a `cnf` holder key (proof-of-possession REQUIRED) a presentation with NO KB-JWT is a bearer
173
+ # downgrade anyone who sees the disclosed SD-JWT could replay it so that MUST fail. expected_aud/
174
+ # expected_nonce (v1.3) enforce RFC 9901 §7.3 replay/audience binding when the relying party supplies
175
+ # them. Bundles issued without `cnf` and with no KB-JWT verify exactly as before (backward-compatible).
176
+ #
177
+ # The holder-binding check is only MEANINGFUL when the issuer signature was actually verified: the `cnf`
178
+ # holder key is declared INSIDE the issuer-signed JWT, so without a checked issuer signature an attacker
179
+ # could forge the whole SD-JWT (issuer JWT + cnf + matching KB-JWT). Only run it when the issuer
180
+ # public key was supplied AND its signature verified (release-review HIGH fix) — otherwise the SD-JWT is
181
+ # unauthenticated and no holder-binding claim over it can be trusted.
182
+ if sd_res.get("sig_checked") and sd_res.get("sig_ok"):
171
183
  sd_part, kb = split_key_binding(compact)
172
184
  if kb is not None:
173
- kb_res = verify_key_binding(compact)
185
+ kb_res = verify_key_binding(compact, expected_aud=expected_aud, expected_nonce=expected_nonce)
174
186
  result.add("sd-jwt-key-binding", kb_res["ok"], kb_res["detail"])
175
187
  elif _issuer_requires_holder_binding(sd_part):
176
188
  result.add(
@@ -196,14 +208,20 @@ def recompute_merkle_root_b64(bundle: Union[dict, str]) -> dict:
196
208
  raise BundleFormatError("bundle must be a JSON object")
197
209
  payload = _b64d(_require(bundle, "payload_b64", "payload_b64"), "payload_b64")
198
210
  mk = _require_dict(_require(bundle, "merkle", "merkle"), "merkle")
211
+ # Validate hash_alg the same way verify_bundle does — recompute must not apply SHA-256/RFC-6962 primitives
212
+ # to a bundle that declares a different algorithm (audit LOW #11/#14).
213
+ hash_alg = mk.get("hash_alg", "sha256-rfc6962")
214
+ if hash_alg != "sha256-rfc6962":
215
+ raise UnsupportedError(f"merkle hash_alg {hash_alg!r} not supported in v0.1")
199
216
  leaf_index = _require_int(mk, "leaf_index", "merkle.leaf_index")
200
217
  tree_size = _require_int(mk, "tree_size", "merkle.tree_size")
201
218
  proof_list = _require(mk, "inclusion_proof_b64", "merkle.inclusion_proof_b64")
202
219
  if not isinstance(proof_list, list):
203
220
  raise BundleFormatError("field merkle.inclusion_proof_b64 must be a list")
204
221
  proof = [_b64d(p, "merkle.inclusion_proof_b64[]") for p in proof_list]
205
- stated_b64 = _require(mk, "root_b64", "merkle.root_b64")
206
- _b64d(stated_b64, "merkle.root_b64") # validate encoding
222
+ # Display the stated root in CANONICAL base64 (re-encode from the decoded bytes), so a non-canonical but
223
+ # byte-equal stated root does not read as a spurious mismatch next to the canonical recomputed root (LOW #10/#15).
224
+ stated_b64 = base64.b64encode(_b64d(_require(mk, "root_b64", "merkle.root_b64"), "merkle.root_b64")).decode("ascii")
207
225
  try:
208
226
  recomputed = merkle.root_from_inclusion(
209
227
  leaf_index, tree_size, merkle.leaf_hash(payload), proof)