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