proofbundle 1.2.0__tar.gz → 1.4.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 (70) hide show
  1. {proofbundle-1.2.0/src/proofbundle.egg-info → proofbundle-1.4.0}/PKG-INFO +103 -22
  2. {proofbundle-1.2.0 → proofbundle-1.4.0}/README.md +95 -17
  3. {proofbundle-1.2.0 → proofbundle-1.4.0}/pyproject.toml +12 -7
  4. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/__init__.py +17 -1
  5. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/adapters/__init__.py +3 -1
  6. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/adapters/lm_eval.py +3 -1
  7. proofbundle-1.4.0/src/proofbundle/adapters/promptfoo.py +131 -0
  8. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/bundle.py +32 -14
  9. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/checkpoint.py +172 -31
  10. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/cli.py +90 -3
  11. proofbundle-1.4.0/src/proofbundle/hf_evals.py +185 -0
  12. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/kbjwt.py +9 -8
  13. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/sdjwt.py +5 -0
  14. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/sdjwt_issue.py +29 -3
  15. proofbundle-1.4.0/src/proofbundle/statuslist.py +194 -0
  16. proofbundle-1.4.0/src/proofbundle/tlogproof.py +175 -0
  17. {proofbundle-1.2.0 → proofbundle-1.4.0/src/proofbundle.egg-info}/PKG-INFO +103 -22
  18. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle.egg-info/SOURCES.txt +10 -1
  19. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle.egg-info/requires.txt +3 -4
  20. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_cosignature.py +15 -18
  21. proofbundle-1.4.0/tests/test_cosignature_mldsa.py +153 -0
  22. proofbundle-1.4.0/tests/test_hf_evals.py +152 -0
  23. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_kbjwt.py +31 -5
  24. proofbundle-1.4.0/tests/test_promptfoo.py +177 -0
  25. proofbundle-1.4.0/tests/test_statuslist.py +143 -0
  26. proofbundle-1.4.0/tests/test_tlogproof.py +163 -0
  27. {proofbundle-1.2.0 → proofbundle-1.4.0}/LICENSE +0 -0
  28. {proofbundle-1.2.0 → proofbundle-1.4.0}/setup.cfg +0 -0
  29. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/_inspect_registry.py +0 -0
  30. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/_integration.py +0 -0
  31. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/adapters/eee.py +0 -0
  32. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
  33. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/dsse.py +0 -0
  34. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/eee_eval_schema.json +0 -0
  35. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/emit.py +0 -0
  36. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/errors.py +0 -0
  37. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/evalclaim.py +0 -0
  38. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/inspect_hook.py +0 -0
  39. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/intoto.py +0 -0
  40. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/merkle.py +0 -0
  41. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/py.typed +0 -0
  42. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/pytest_plugin.py +0 -0
  43. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle/signature.py +0 -0
  44. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
  45. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
  46. {proofbundle-1.2.0 → proofbundle-1.4.0}/src/proofbundle.egg-info/top_level.txt +0 -0
  47. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_adapters.py +0 -0
  48. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_adversarial.py +0 -0
  49. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_bundle.py +0 -0
  50. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_bundle_robustness.py +0 -0
  51. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_checkpoint.py +0 -0
  52. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_cli.py +0 -0
  53. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_cli_eval.py +0 -0
  54. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_eee.py +0 -0
  55. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_emit.py +0 -0
  56. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_eval_claim_schema.py +0 -0
  57. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_evalclaim.py +0 -0
  58. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_examples.py +0 -0
  59. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_inspect_hook.py +0 -0
  60. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_intoto.py +0 -0
  61. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_intoto_dsse.py +0 -0
  62. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_merkle.py +0 -0
  63. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_merkle_property.py +0 -0
  64. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_pytest_plugin.py +0 -0
  65. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_rekor_interop.py +0 -0
  66. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_rfc6962_external_vectors.py +0 -0
  67. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_schema.py +0 -0
  68. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_sdjwt_issue.py +0 -0
  69. {proofbundle-1.2.0 → proofbundle-1.4.0}/tests/test_sdjwt_reference.py +0 -0
  70. {proofbundle-1.2.0 → proofbundle-1.4.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.4.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
@@ -14,12 +14,13 @@ Classifier: Development Status :: 3 - Alpha
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.9
18
17
  Classifier: Programming Language :: Python :: 3.10
19
18
  Classifier: Programming Language :: Python :: 3.11
20
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
21
22
  Classifier: Topic :: Security :: Cryptography
22
- Requires-Python: >=3.9
23
+ Requires-Python: >=3.10
23
24
  Description-Content-Type: text/markdown
24
25
  License-File: LICENSE
25
26
  Requires-Dist: cryptography>=42
@@ -27,10 +28,12 @@ Provides-Extra: sdjwt
27
28
  Provides-Extra: eval
28
29
  Requires-Dist: rfc8785>=0.1.4; extra == "eval"
29
30
  Provides-Extra: adapters
31
+ Provides-Extra: pq
32
+ Requires-Dist: cryptography>=48; extra == "pq"
30
33
  Provides-Extra: pytest
31
34
  Requires-Dist: pytest>=7; extra == "pytest"
32
35
  Provides-Extra: inspect
33
- Requires-Dist: inspect_ai<0.4,>=0.3.112; python_version >= "3.10" and extra == "inspect"
36
+ Requires-Dist: inspect_ai<0.4,>=0.3.112; extra == "inspect"
34
37
  Provides-Extra: dev
35
38
  Requires-Dist: pytest>=7; extra == "dev"
36
39
  Requires-Dist: ruff>=0.5; extra == "dev"
@@ -40,7 +43,7 @@ Requires-Dist: build>=1; extra == "dev"
40
43
  Requires-Dist: hypothesis>=6; extra == "dev"
41
44
  Requires-Dist: rfc8785>=0.1.4; extra == "dev"
42
45
  Requires-Dist: sd-jwt>=0.10; extra == "dev"
43
- Requires-Dist: inspect_ai<0.4,>=0.3.112; python_version >= "3.10" and extra == "dev"
46
+ Requires-Dist: inspect_ai<0.4,>=0.3.112; extra == "dev"
44
47
  Dynamic: license-file
45
48
 
46
49
  <div align="center">
@@ -53,9 +56,11 @@ Dynamic: license-file
53
56
  <h1>proofbundle</h1>
54
57
 
55
58
  **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.**
59
+ RFC 6962 transparency-log Merkle anchoring, C2SP witnessed checkpoints and
60
+ `.tlog-proof` transparent signatures (post-quantum-ready witnesses, ML-DSA-44),
61
+ SD-JWT (RFC 9901) selective disclosure with Key Binding, Token-Status-List
62
+ revocation snapshots, aligned to the in-toto test-result predicate. One portable
63
+ file, no server, no network.**
59
64
 
60
65
  [![CI](https://github.com/b7n0de/proofbundle/actions/workflows/ci.yml/badge.svg)](https://github.com/b7n0de/proofbundle/actions/workflows/ci.yml)
61
66
  [![PyPI](https://img.shields.io/pypi/v/proofbundle.svg?color=D6248A&cacheSeconds=3600)](https://pypi.org/project/proofbundle/)
@@ -65,6 +70,9 @@ no server, no network.**
65
70
  [![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
71
  [![SLSA build provenance](https://img.shields.io/badge/SLSA-build_provenance-D6248A.svg)](https://slsa.dev)
67
72
  [![PyPI attestations](https://img.shields.io/badge/PyPI-attestations_(PEP_740)-D6248A.svg)](https://pypi.org/project/proofbundle/)
73
+ [![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)
74
+ [![Mutation tested](https://img.shields.io/badge/tests-mutation_gated-D6248A.svg)](scripts/mutation_check.py)
75
+ [![C2SP tlog-proof](https://img.shields.io/badge/C2SP-tlog--proof-D6248A.svg)](https://github.com/C2SP/C2SP/blob/main/tlog-proof.md)
68
76
  <!-- DOI badge placeholder: enable Zenodo archiving for this repo, then add the Zenodo concept-DOI
69
77
  badge here (and the DOI to CITATION.cff) once Zenodo assigns one on the next release. No DOI has
70
78
  been assigned yet (no archived record exists at build time) — tracked in the human checklist. -->
@@ -73,11 +81,12 @@ no server, no network.**
73
81
 
74
82
  **At a glance:** `proofbundle emit` signs and anchors a payload; `proofbundle
75
83
  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.
84
+ checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 209 tests + a CI mutation gate.
77
85
 
78
86
  ## Contents
79
87
 
80
88
  - [Why](#why)
89
+ - [What a receipt proves](#what-a-receipt-proves-and-what-it-does-not)
81
90
  - [What it verifies](#what-it-verifies)
82
91
  - [How it fits together](#how-it-fits-together)
83
92
  - [Install](#install)
@@ -88,6 +97,7 @@ checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 148 tests.
88
97
  - [Bundle format](#bundle-format-proofbundlev01)
89
98
  - [Eval receipts](#eval-receipts)
90
99
  - [Security notes and scope](#security-notes-and-scope-stated-honestly)
100
+ - [Compliance](#compliance)
91
101
  - [Roadmap](#roadmap)
92
102
  - [Contributing](#contributing)
93
103
  - [License](#license)
@@ -148,12 +158,19 @@ A bundle is a single JSON document. `proofbundle` checks, offline:
148
158
  Certificate Transparency)
149
159
  3. **sd-jwt** (optional) — an embedded SD-JWT selective-disclosure credential is
150
160
  well formed, and if an issuer key is given, correctly issuer-signed
151
- 4. **sd-jwt-key-binding** (optional, v1.2) — if the SD-JWT carries a Key Binding
152
- JWT (RFC 9901 §4.3), it is verified **fail-closed**: `typ` is `kb+jwt`,
153
- `iat`/`aud`/`nonce`/`sd_hash` are present, `sd_hash` binds the exact presented
154
- disclosure set, and the signature verifies under the issuer-bound `cnf.jwk`
155
- holder key. A present-but-broken KB-JWT fails the bundle it is never
156
- silently ignored.
161
+ 4. **sd-jwt-key-binding** (optional, v1.2; hardened v1.3) — proof-of-possession, **fail-closed**.
162
+ The check runs only once the issuer signature itself verified (an unauthenticated SD-JWT can
163
+ never report a valid holder binding). Then: `typ` is `kb+jwt`, `iat`/`aud`/`nonce`/`sd_hash` are
164
+ present, `sd_hash` binds the exact presented disclosure set, and the signature verifies under the
165
+ issuer-bound `cnf.jwk` holder key. A present-but-broken KB-JWT fails the bundle; and if the issuer
166
+ **bound** a `cnf` holder key but the presentation carries **no** KB-JWT, that fails too — stripping
167
+ the binding is a bearer downgrade, not a valid receipt. `verify --aud/--nonce` enforce RFC 9901
168
+ §7.3 audience/replay binding when the relying party supplies them.
169
+
170
+ Beyond the single-file bundle, the library also verifies, all offline: a **witnessed C2SP checkpoint**
171
+ (`verify_witnessed_checkpoint`, Ed25519 + post-quantum ML-DSA-44 cosignatures — the quorum counts distinct
172
+ witness **keys**, not names, so one key under many names can never stuff a threshold), a **C2SP
173
+ `.tlog-proof`** (`proofbundle verify-proof`), and a **Token Status List** revocation snapshot.
157
174
 
158
175
  The verifier treats the payload as opaque bytes. It proves that these exact
159
176
  bytes were signed and anchored, not what they mean. That is on purpose: it keeps
@@ -192,7 +209,7 @@ flowchart LR
192
209
  pip install proofbundle
193
210
  ```
194
211
 
195
- Requires Python 3.9+ and [`cryptography`](https://cryptography.io). Signature
212
+ Requires Python 3.10+ and [`cryptography`](https://cryptography.io). Signature
196
213
  math is delegated to `cryptography`; this project never rolls its own crypto.
197
214
  The Merkle and SD-JWT logic is pure standard library.
198
215
 
@@ -214,7 +231,7 @@ proofbundle verify examples/example_bundle.json
214
231
  ```
215
232
 
216
233
  <div align="center">
217
- <img src="assets/demo.svg" alt="proofbundle verify output: four PASS checks and OK" width="680">
234
+ <img src="assets/demo.svg" alt="proofbundle verify output: PASS checks and OK" width="680">
218
235
  </div>
219
236
 
220
237
  Machine-readable output and a non-zero exit code on failure:
@@ -312,6 +329,41 @@ both log-signed **and** cosigned by a quorum of distinct witnesses, offline,
312
329
  which rules out a split view by the log operator. This is the same
313
330
  witnessed-checkpoint pattern Rekor v2 (GA October 2025) institutionalizes.
314
331
 
332
+ Since v1.3 the witness layer is complete: **ML-DSA-44 cosignatures** (C2SP type
333
+ 0x06, FIPS 204 — post-quantum, the spec's SHOULD for new witness deployments;
334
+ optional `proofbundle[pq]` extra, Ed25519 stays the default) and the
335
+ **[C2SP tlog-proof](https://github.com/C2SP/C2SP/blob/main/tlog-proof.md)** file
336
+ format (`.tlog-proof`) — index + inclusion proof + (co)signed checkpoint in one
337
+ portable, offline-verifiable "transparent signature":
338
+
339
+ ```bash
340
+ proofbundle verify-proof receipt.tlog-proof --payload-file result.json \
341
+ --log-vkey <log-vkey> --witness-vkey <w1> --witness-vkey <w2> --threshold 2
342
+ ```
343
+
344
+ Revocation joins the offline model too: a receipt SD-JWT can carry a Token
345
+ Status List `status` claim, checked against a signed, **bundled list snapshot**
346
+ (`proofbundle.statuslist`) — staleness is reported explicitly instead of
347
+ assumed away.
348
+
349
+ ```mermaid
350
+ flowchart LR
351
+ B["bundle.json<br/>(signed + anchored receipt)"] --> CK["tlog-checkpoint<br/>(signed note over the root)"]
352
+ CK --> W1["witness cosignature<br/>Ed25519 · 0x04"]
353
+ CK --> W2["witness cosignature<br/>ML-DSA-44 · 0x06 · PQ"]
354
+ B -->|"index + inclusion proof"| TP[".tlog-proof<br/>transparent signature"]
355
+ W1 --> TP
356
+ W2 --> TP
357
+ TP --> V{{"proofbundle verify-proof<br/>offline · k-of-n policy"}}
358
+ V --> R1["log signature"]
359
+ V --> R2["witness quorum"]
360
+ V --> R3["merkle inclusion"]
361
+
362
+ style V fill:#D6248A,stroke:#D6248A,color:#fff
363
+ style W2 fill:#1e2327,stroke:#D6248A,color:#fff
364
+ style TP fill:#1e2327,stroke:#D6248A,color:#fff
365
+ ```
366
+
315
367
  ## Bundle format (`proofbundle/v0.1`)
316
368
 
317
369
  The format is specified normatively in [SPEC.md](SPEC.md) (fields, encodings,
@@ -359,6 +411,11 @@ The scope is deliberately narrow. It does exactly what it says and no more:
359
411
  - Witness cosignatures (v1.2) prove consistency *observations* by the listed
360
412
  witnesses; real split-view resistance additionally requires the witnesses to be
361
413
  operationally independent — a deployment property no file format can supply.
414
+ - Post-quantum: witness **cosignatures** may be ML-DSA-44 (v1.3, `[pq]` extra) — the layer where
415
+ the ecosystem (C2SP, Sigstore) is deploying PQ first. Primary signatures stay Ed25519 until the
416
+ ecosystem moves; a configured ML-DSA witness on a build without PQ support fails closed.
417
+ - Revocation (v1.3) is checked against a SIGNED status-list snapshot the relying party supplies;
418
+ its freshness is reported and only judged against the caller's clock — never assumed.
362
419
  - The verifier does not fetch anything. Trust anchors (the signer key, the
363
420
  expected root, witness keys) are inputs you supply out of band.
364
421
  - No custom cryptography. Ed25519 comes from `cryptography`; Merkle hashing is
@@ -429,7 +486,7 @@ threshold was met without revealing the model or the data. See [INTEROP.md](INTE
429
486
  [lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) `results_*.json` (the
430
487
  genuine `acc,none` filter-suffix format). **`proofbundle.adapters.from_eee_dataset`** (v0.9) reads an
431
488
  Every Eval Ever v0.2.2 aggregate JSON and builds a signed receipt — validated against the vendored EEE
432
- schema, with **no runtime import** of `every_eval_ever` (it needs Python 3.12; proofbundle stays 3.9+).
489
+ schema, with **no runtime import** of `every_eval_ever` (it needs Python 3.12; proofbundle stays 3.10+).
433
490
  - **in-toto test-result export, DSSE-signed** (v0.9) — `proofbundle.intoto.export_intoto_dsse(claim,
434
491
  signer)` emits the receipt as a DSSE-signed in-toto Statement v1 with the **generic
435
492
  `test-result/v0.1` predicate** (result PASSED/FAILED, `configuration` ResourceDescriptors), so a generic
@@ -455,6 +512,16 @@ bundle-bound views.
455
512
  Every release ships **PEP 740 attestations** (Trusted Publishing) + an SLSA build-provenance
456
513
  attestation — see [SECURITY.md](SECURITY.md).
457
514
 
515
+ ## Compliance
516
+
517
+ The EU AI Act's record-keeping duty for high-risk systems (Article 12 — automatic,
518
+ tamper-evident, independently verifiable logs) applies from **August 2, 2026**;
519
+ the GPAI Code of Practice expects documented, credible model evaluations.
520
+ **[COMPLIANCE.md](COMPLIANCE.md)** maps — honestly, and without claiming more
521
+ than the cryptography supports — what a verified receipt covers under Article 12,
522
+ NIST AI RMF MEASURE, and the logging standards in flight (prEN 18229-1,
523
+ ISO/IEC DIS 24970), including the anti-patterns no one should claim.
524
+
458
525
  ## Roadmap
459
526
 
460
527
  - **v0.1** — the offline verifier plus a real example bundle.
@@ -475,13 +542,27 @@ attestation — see [SECURITY.md](SECURITY.md).
475
542
  eval (end-of-task hook) or a pytest run (pytest11 plugin), plus a composite GitHub Action.
476
543
  - **v1.1** — trust hardening: a signed `assurance_level`, a THREAT_MODEL, a self_attested-
477
544
  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
545
+ - **v1.2** — holder binding + witness quorum: **Key Binding JWT** verification
479
546
  (RFC 9901 §4.3, fail-closed; closes #1) with `cnf.jwk` issuance and holder presentation, **C2SP
480
547
  tlog-cosignature** verification (Ed25519 cosignature/v1, witness quorum, split-view resistance), and
481
548
  `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.
549
+ - **v1.3** the portable proof: **C2SP tlog-proof** emit/verify (+
550
+ `verify-proof` CLI), **ML-DSA-44** witness cosignatures (post-quantum, `[pq]` extra), **Token
551
+ Status List** snapshot verification (offline revocation), SD-JWT VC markers (`dc+sd-jwt`,
552
+ `vct`, `status`), COMPLIANCE.md (EU AI Act Art. 12 / NIST AI RMF mapping), and the mutation
553
+ suite as a CI gate.
554
+ - **v1.4 (current release)** — distribution: a **promptfoo** adapter (results.json v3 →
555
+ pass_rate receipt), the **Hugging Face Community Evals bridge** (`pb1.` receipt tokens for the
556
+ `.eval_results` `verifyToken` field + a strict YAML entry emitter — proofbundle-verifiable,
557
+ explicitly NOT the HF-internal badge token), `hf-token` CLI, and the **Python 3.10+ floor**
558
+ (3.9 is EOL since 2025-10).
559
+ - **v2.0 (next major, research differentiation)** — **per-sample Merkle receipts**: the payload carries a
560
+ Merkle root over *individual* sample results, so a verifier can force a random-sample re-check instead of
561
+ trusting only the reported aggregate. This is the THREAT_MODEL's one named structural gap ("forced random
562
+ sub-sampling"), and closing it is the road from *authorship* to *sampled correctness*.
563
+ - **Deferred** (explicitly not yet built, stated honestly) — full SD-JWT VC conformance + `vct`
564
+ type-metadata resolution (pre-IESG), an official in-toto eval predicate (proposal path via
565
+ OpenSSF/CoSAI), and a full in-toto client.
485
566
 
486
567
  ## Contributing
487
568
 
@@ -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. 209 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)
@@ -103,12 +110,19 @@ 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
106
- 4. **sd-jwt-key-binding** (optional, v1.2) — if the SD-JWT carries a Key Binding
107
- JWT (RFC 9901 §4.3), it is verified **fail-closed**: `typ` is `kb+jwt`,
108
- `iat`/`aud`/`nonce`/`sd_hash` are present, `sd_hash` binds the exact presented
109
- disclosure set, and the signature verifies under the issuer-bound `cnf.jwk`
110
- holder key. A present-but-broken KB-JWT fails the bundle it is never
111
- silently ignored.
113
+ 4. **sd-jwt-key-binding** (optional, v1.2; hardened v1.3) — proof-of-possession, **fail-closed**.
114
+ The check runs only once the issuer signature itself verified (an unauthenticated SD-JWT can
115
+ never report a valid holder binding). Then: `typ` is `kb+jwt`, `iat`/`aud`/`nonce`/`sd_hash` are
116
+ present, `sd_hash` binds the exact presented disclosure set, and the signature verifies under the
117
+ issuer-bound `cnf.jwk` holder key. A present-but-broken KB-JWT fails the bundle; and if the issuer
118
+ **bound** a `cnf` holder key but the presentation carries **no** KB-JWT, that fails too — stripping
119
+ the binding is a bearer downgrade, not a valid receipt. `verify --aud/--nonce` enforce RFC 9901
120
+ §7.3 audience/replay binding when the relying party supplies them.
121
+
122
+ Beyond the single-file bundle, the library also verifies, all offline: a **witnessed C2SP checkpoint**
123
+ (`verify_witnessed_checkpoint`, Ed25519 + post-quantum ML-DSA-44 cosignatures — the quorum counts distinct
124
+ witness **keys**, not names, so one key under many names can never stuff a threshold), a **C2SP
125
+ `.tlog-proof`** (`proofbundle verify-proof`), and a **Token Status List** revocation snapshot.
112
126
 
113
127
  The verifier treats the payload as opaque bytes. It proves that these exact
114
128
  bytes were signed and anchored, not what they mean. That is on purpose: it keeps
@@ -147,7 +161,7 @@ flowchart LR
147
161
  pip install proofbundle
148
162
  ```
149
163
 
150
- Requires Python 3.9+ and [`cryptography`](https://cryptography.io). Signature
164
+ Requires Python 3.10+ and [`cryptography`](https://cryptography.io). Signature
151
165
  math is delegated to `cryptography`; this project never rolls its own crypto.
152
166
  The Merkle and SD-JWT logic is pure standard library.
153
167
 
@@ -169,7 +183,7 @@ proofbundle verify examples/example_bundle.json
169
183
  ```
170
184
 
171
185
  <div align="center">
172
- <img src="assets/demo.svg" alt="proofbundle verify output: four PASS checks and OK" width="680">
186
+ <img src="assets/demo.svg" alt="proofbundle verify output: PASS checks and OK" width="680">
173
187
  </div>
174
188
 
175
189
  Machine-readable output and a non-zero exit code on failure:
@@ -267,6 +281,41 @@ both log-signed **and** cosigned by a quorum of distinct witnesses, offline,
267
281
  which rules out a split view by the log operator. This is the same
268
282
  witnessed-checkpoint pattern Rekor v2 (GA October 2025) institutionalizes.
269
283
 
284
+ Since v1.3 the witness layer is complete: **ML-DSA-44 cosignatures** (C2SP type
285
+ 0x06, FIPS 204 — post-quantum, the spec's SHOULD for new witness deployments;
286
+ optional `proofbundle[pq]` extra, Ed25519 stays the default) and the
287
+ **[C2SP tlog-proof](https://github.com/C2SP/C2SP/blob/main/tlog-proof.md)** file
288
+ format (`.tlog-proof`) — index + inclusion proof + (co)signed checkpoint in one
289
+ portable, offline-verifiable "transparent signature":
290
+
291
+ ```bash
292
+ proofbundle verify-proof receipt.tlog-proof --payload-file result.json \
293
+ --log-vkey <log-vkey> --witness-vkey <w1> --witness-vkey <w2> --threshold 2
294
+ ```
295
+
296
+ Revocation joins the offline model too: a receipt SD-JWT can carry a Token
297
+ Status List `status` claim, checked against a signed, **bundled list snapshot**
298
+ (`proofbundle.statuslist`) — staleness is reported explicitly instead of
299
+ assumed away.
300
+
301
+ ```mermaid
302
+ flowchart LR
303
+ B["bundle.json<br/>(signed + anchored receipt)"] --> CK["tlog-checkpoint<br/>(signed note over the root)"]
304
+ CK --> W1["witness cosignature<br/>Ed25519 · 0x04"]
305
+ CK --> W2["witness cosignature<br/>ML-DSA-44 · 0x06 · PQ"]
306
+ B -->|"index + inclusion proof"| TP[".tlog-proof<br/>transparent signature"]
307
+ W1 --> TP
308
+ W2 --> TP
309
+ TP --> V{{"proofbundle verify-proof<br/>offline · k-of-n policy"}}
310
+ V --> R1["log signature"]
311
+ V --> R2["witness quorum"]
312
+ V --> R3["merkle inclusion"]
313
+
314
+ style V fill:#D6248A,stroke:#D6248A,color:#fff
315
+ style W2 fill:#1e2327,stroke:#D6248A,color:#fff
316
+ style TP fill:#1e2327,stroke:#D6248A,color:#fff
317
+ ```
318
+
270
319
  ## Bundle format (`proofbundle/v0.1`)
271
320
 
272
321
  The format is specified normatively in [SPEC.md](SPEC.md) (fields, encodings,
@@ -314,6 +363,11 @@ The scope is deliberately narrow. It does exactly what it says and no more:
314
363
  - Witness cosignatures (v1.2) prove consistency *observations* by the listed
315
364
  witnesses; real split-view resistance additionally requires the witnesses to be
316
365
  operationally independent — a deployment property no file format can supply.
366
+ - Post-quantum: witness **cosignatures** may be ML-DSA-44 (v1.3, `[pq]` extra) — the layer where
367
+ the ecosystem (C2SP, Sigstore) is deploying PQ first. Primary signatures stay Ed25519 until the
368
+ ecosystem moves; a configured ML-DSA witness on a build without PQ support fails closed.
369
+ - Revocation (v1.3) is checked against a SIGNED status-list snapshot the relying party supplies;
370
+ its freshness is reported and only judged against the caller's clock — never assumed.
317
371
  - The verifier does not fetch anything. Trust anchors (the signer key, the
318
372
  expected root, witness keys) are inputs you supply out of band.
319
373
  - No custom cryptography. Ed25519 comes from `cryptography`; Merkle hashing is
@@ -384,7 +438,7 @@ threshold was met without revealing the model or the data. See [INTEROP.md](INTE
384
438
  [lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) `results_*.json` (the
385
439
  genuine `acc,none` filter-suffix format). **`proofbundle.adapters.from_eee_dataset`** (v0.9) reads an
386
440
  Every Eval Ever v0.2.2 aggregate JSON and builds a signed receipt — validated against the vendored EEE
387
- schema, with **no runtime import** of `every_eval_ever` (it needs Python 3.12; proofbundle stays 3.9+).
441
+ schema, with **no runtime import** of `every_eval_ever` (it needs Python 3.12; proofbundle stays 3.10+).
388
442
  - **in-toto test-result export, DSSE-signed** (v0.9) — `proofbundle.intoto.export_intoto_dsse(claim,
389
443
  signer)` emits the receipt as a DSSE-signed in-toto Statement v1 with the **generic
390
444
  `test-result/v0.1` predicate** (result PASSED/FAILED, `configuration` ResourceDescriptors), so a generic
@@ -410,6 +464,16 @@ bundle-bound views.
410
464
  Every release ships **PEP 740 attestations** (Trusted Publishing) + an SLSA build-provenance
411
465
  attestation — see [SECURITY.md](SECURITY.md).
412
466
 
467
+ ## Compliance
468
+
469
+ The EU AI Act's record-keeping duty for high-risk systems (Article 12 — automatic,
470
+ tamper-evident, independently verifiable logs) applies from **August 2, 2026**;
471
+ the GPAI Code of Practice expects documented, credible model evaluations.
472
+ **[COMPLIANCE.md](COMPLIANCE.md)** maps — honestly, and without claiming more
473
+ than the cryptography supports — what a verified receipt covers under Article 12,
474
+ NIST AI RMF MEASURE, and the logging standards in flight (prEN 18229-1,
475
+ ISO/IEC DIS 24970), including the anti-patterns no one should claim.
476
+
413
477
  ## Roadmap
414
478
 
415
479
  - **v0.1** — the offline verifier plus a real example bundle.
@@ -430,13 +494,27 @@ attestation — see [SECURITY.md](SECURITY.md).
430
494
  eval (end-of-task hook) or a pytest run (pytest11 plugin), plus a composite GitHub Action.
431
495
  - **v1.1** — trust hardening: a signed `assurance_level`, a THREAT_MODEL, a self_attested-
432
496
  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
497
+ - **v1.2** — holder binding + witness quorum: **Key Binding JWT** verification
434
498
  (RFC 9901 §4.3, fail-closed; closes #1) with `cnf.jwk` issuance and holder presentation, **C2SP
435
499
  tlog-cosignature** verification (Ed25519 cosignature/v1, witness quorum, split-view resistance), and
436
500
  `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.
501
+ - **v1.3** the portable proof: **C2SP tlog-proof** emit/verify (+
502
+ `verify-proof` CLI), **ML-DSA-44** witness cosignatures (post-quantum, `[pq]` extra), **Token
503
+ Status List** snapshot verification (offline revocation), SD-JWT VC markers (`dc+sd-jwt`,
504
+ `vct`, `status`), COMPLIANCE.md (EU AI Act Art. 12 / NIST AI RMF mapping), and the mutation
505
+ suite as a CI gate.
506
+ - **v1.4 (current release)** — distribution: a **promptfoo** adapter (results.json v3 →
507
+ pass_rate receipt), the **Hugging Face Community Evals bridge** (`pb1.` receipt tokens for the
508
+ `.eval_results` `verifyToken` field + a strict YAML entry emitter — proofbundle-verifiable,
509
+ explicitly NOT the HF-internal badge token), `hf-token` CLI, and the **Python 3.10+ floor**
510
+ (3.9 is EOL since 2025-10).
511
+ - **v2.0 (next major, research differentiation)** — **per-sample Merkle receipts**: the payload carries a
512
+ Merkle root over *individual* sample results, so a verifier can force a random-sample re-check instead of
513
+ trusting only the reported aggregate. This is the THREAT_MODEL's one named structural gap ("forced random
514
+ sub-sampling"), and closing it is the road from *authorship* to *sampled correctness*.
515
+ - **Deferred** (explicitly not yet built, stated honestly) — full SD-JWT VC conformance + `vct`
516
+ type-metadata resolution (pre-IESG), an official in-toto eval predicate (proposal path via
517
+ OpenSSF/CoSAI), and a full in-toto client.
440
518
 
441
519
  ## Contributing
442
520
 
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proofbundle"
7
- version = "1.2.0"
7
+ version = "1.4.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
- requires-python = ">=3.9"
10
+ requires-python = ">=3.10"
11
11
  license = { text = "MIT" }
12
12
  authors = [{ name = "Konrad Gruszka" }]
13
13
  keywords = [
@@ -26,10 +26,11 @@ classifiers = [
26
26
  "Intended Audience :: Developers",
27
27
  "License :: OSI Approved :: MIT License",
28
28
  "Programming Language :: Python :: 3",
29
- "Programming Language :: Python :: 3.9",
30
29
  "Programming Language :: Python :: 3.10",
31
30
  "Programming Language :: Python :: 3.11",
32
31
  "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: 3.14",
33
34
  "Topic :: Security :: Cryptography",
34
35
  ]
35
36
  dependencies = ["cryptography>=42"]
@@ -45,16 +46,20 @@ sdjwt = []
45
46
  eval = ["rfc8785>=0.1.4"]
46
47
  # The lm-eval adapter reads exported results.json (no import) → pure stdlib.
47
48
  adapters = []
49
+ # ML-DSA-44 (FIPS 204) cosignature verification (C2SP tlog-cosignature type 0x06). Needs a
50
+ # cryptography build with OpenSSL 3.5+ (default PyPI wheels since 48.0.0). Verify-only by default;
51
+ # the emit helper needs the same. Ed25519 paths never require this.
52
+ pq = ["cryptography>=48"]
48
53
  # The pytest plugin (opt-in test-run receipt via the pytest11 entry-point) — pytest is an optional extra,
49
54
  # never a core dependency. Floor 7.0: the hooks/stats API is signature-stable across pytest 7/8/9.
50
55
  pytest = ["pytest>=7"]
51
56
  # The inspect_ai adapter uses the STABLE read_eval_log API (lazy import). Pinned with an UPPER bound:
52
57
  # the .eval format + pydantic schema change between versions (inspect_ai issue 834), and the fixture
53
- # test is bound to this range. inspect_ai requires Python >= 3.10, so the marker gates it out on 3.9
54
- # (base + [eval]/[sdjwt] still work on 3.9; the inspect adapter test skips there). Fixes the red 3.9 CI.
55
- inspect = ['inspect_ai>=0.3.112,<0.4; python_version >= "3.10"']
58
+ # test is bound to this range. Since v1.4.0 the project floor is Python 3.10, so no per-extra version
59
+ # marker is needed here (inspect_ai's own >=3.10 requirement is always satisfiable on a supported base).
60
+ inspect = ['inspect_ai>=0.3.112,<0.4']
56
61
  dev = ["pytest>=7", "ruff>=0.5", "jsonschema>=4", "mypy>=1.8", "build>=1", "hypothesis>=6",
57
- "rfc8785>=0.1.4", "sd-jwt>=0.10", 'inspect_ai>=0.3.112,<0.4; python_version >= "3.10"']
62
+ "rfc8785>=0.1.4", "sd-jwt>=0.10", 'inspect_ai>=0.3.112,<0.4']
58
63
 
59
64
  [project.urls]
60
65
  Homepage = "https://b7n0de.com"
@@ -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.4.0"
17
17
 
18
18
  __all__ = [
19
19
  "__version__",
@@ -26,6 +26,12 @@ __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",
33
+ "receipt_token",
34
+ "verify_receipt_token",
29
35
  "VerificationResult",
30
36
  "Check",
31
37
  "ProofBundleError",
@@ -39,13 +45,23 @@ _LAZY = {
39
45
  "Check": ".errors", "ProofBundleError": ".errors", "VerificationResult": ".errors",
40
46
  "verify_consistency": ".merkle", "verify_inclusion": ".merkle",
41
47
  "verify_key_binding": ".kbjwt",
48
+ "verify_tlog_proof": ".tlogproof",
49
+ "verify_cosignature": ".checkpoint",
50
+ "verify_witnessed_checkpoint": ".checkpoint",
51
+ "verify_status_snapshot": ".statuslist",
52
+ "receipt_token": ".hf_evals",
53
+ "verify_receipt_token": ".hf_evals",
42
54
  }
43
55
 
44
56
  if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime stays lazy
45
57
  from .bundle import SCHEMA, load_bundle, recompute_merkle_root_b64, verify_bundle
46
58
  from .emit import emit_bundle, generate_signer
47
59
  from .errors import Check, ProofBundleError, VerificationResult
60
+ from .checkpoint import verify_cosignature, verify_witnessed_checkpoint
48
61
  from .kbjwt import verify_key_binding
62
+ from .hf_evals import receipt_token, verify_receipt_token
63
+ from .statuslist import verify_status_snapshot
64
+ from .tlogproof import verify_tlog_proof
49
65
  from .merkle import verify_consistency, verify_inclusion
50
66
 
51
67
 
@@ -7,5 +7,7 @@ each fixture in tests/fixtures documents its source + version.
7
7
  from .inspect_ai import from_inspect_ai_log
8
8
  from .eee import from_eee_dataset
9
9
  from .lm_eval import from_lm_eval_results
10
+ from .promptfoo import from_promptfoo_results
10
11
 
11
- __all__ = ["from_lm_eval_results", "from_inspect_ai_log", "from_eee_dataset"]
12
+ __all__ = ["from_lm_eval_results", "from_inspect_ai_log", "from_eee_dataset",
13
+ "from_promptfoo_results"]
@@ -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)