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.
- {proofbundle-1.2.0/src/proofbundle.egg-info → proofbundle-1.3.0}/PKG-INFO +77 -10
- {proofbundle-1.2.0 → proofbundle-1.3.0}/README.md +72 -9
- {proofbundle-1.2.0 → proofbundle-1.3.0}/pyproject.toml +7 -1
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/__init__.py +12 -1
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/adapters/lm_eval.py +3 -1
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/bundle.py +32 -14
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/checkpoint.py +172 -31
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/cli.py +59 -3
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/kbjwt.py +9 -8
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/sdjwt_issue.py +29 -3
- proofbundle-1.3.0/src/proofbundle/statuslist.py +194 -0
- proofbundle-1.3.0/src/proofbundle/tlogproof.py +175 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0/src/proofbundle.egg-info}/PKG-INFO +77 -10
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/SOURCES.txt +6 -1
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/requires.txt +3 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_cosignature.py +15 -18
- proofbundle-1.3.0/tests/test_cosignature_mldsa.py +153 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_kbjwt.py +31 -5
- proofbundle-1.3.0/tests/test_statuslist.py +143 -0
- proofbundle-1.3.0/tests/test_tlogproof.py +163 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/LICENSE +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/setup.cfg +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/_integration.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/adapters/eee.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/dsse.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/emit.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/errors.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/evalclaim.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/intoto.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/merkle.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/py.typed +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/sdjwt.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle/signature.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_adapters.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_adversarial.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_bundle.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_checkpoint.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_cli.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_cli_eval.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_eee.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_emit.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_evalclaim.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_examples.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_inspect_hook.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_intoto.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_merkle.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_merkle_property.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_rekor_interop.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_schema.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-1.2.0 → proofbundle-1.3.0}/tests/test_sdjwt_reference.py +0 -0
- {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.
|
|
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,
|
|
57
|
-
|
|
58
|
-
|
|
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
|
[](https://github.com/b7n0de/proofbundle/actions/workflows/ci.yml)
|
|
61
67
|
[](https://pypi.org/project/proofbundle/)
|
|
@@ -65,6 +71,9 @@ no server, no network.**
|
|
|
65
71
|
[](https://github.com/astral-sh/ruff)
|
|
66
72
|
[](https://slsa.dev)
|
|
67
73
|
[-D6248A.svg)](https://pypi.org/project/proofbundle/)
|
|
74
|
+
[](https://github.com/C2SP/C2SP/blob/main/tlog-cosignature.md)
|
|
75
|
+
[](scripts/mutation_check.py)
|
|
76
|
+
[](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.
|
|
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:
|
|
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
|
|
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
|
-
- **
|
|
483
|
-
|
|
484
|
-
|
|
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,
|
|
12
|
-
|
|
13
|
-
|
|
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
|
[](https://github.com/b7n0de/proofbundle/actions/workflows/ci.yml)
|
|
16
18
|
[](https://pypi.org/project/proofbundle/)
|
|
@@ -20,6 +22,9 @@ no server, no network.**
|
|
|
20
22
|
[](https://github.com/astral-sh/ruff)
|
|
21
23
|
[](https://slsa.dev)
|
|
22
24
|
[-D6248A.svg)](https://pypi.org/project/proofbundle/)
|
|
25
|
+
[](https://github.com/C2SP/C2SP/blob/main/tlog-cosignature.md)
|
|
26
|
+
[](scripts/mutation_check.py)
|
|
27
|
+
[](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.
|
|
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:
|
|
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
|
|
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
|
-
- **
|
|
438
|
-
|
|
439
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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)
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
# v1.
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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)
|