proofbundle 1.4.0__tar.gz → 1.7.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.4.0/src/proofbundle.egg-info → proofbundle-1.7.0}/PKG-INFO +36 -12
- {proofbundle-1.4.0 → proofbundle-1.7.0}/README.md +34 -10
- {proofbundle-1.4.0 → proofbundle-1.7.0}/pyproject.toml +2 -2
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/__init__.py +11 -1
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/adapters/__init__.py +3 -1
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/adapters/eee.py +1 -1
- proofbundle-1.7.0/src/proofbundle/adapters/samples.py +82 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/bundle.py +20 -4
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/cli.py +83 -4
- proofbundle-1.7.0/src/proofbundle/demo.py +153 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/evalclaim.py +60 -6
- proofbundle-1.7.0/src/proofbundle/persample.py +270 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/statuslist.py +18 -6
- {proofbundle-1.4.0 → proofbundle-1.7.0/src/proofbundle.egg-info}/PKG-INFO +36 -12
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle.egg-info/SOURCES.txt +5 -0
- proofbundle-1.7.0/tests/test_demo.py +54 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_evalclaim.py +30 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_kbjwt.py +24 -6
- proofbundle-1.7.0/tests/test_persample.py +418 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_statuslist.py +36 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/LICENSE +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/setup.cfg +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/_integration.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/dsse.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/emit.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/errors.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/hf_evals.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/intoto.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/merkle.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/py.typed +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/sdjwt.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/signature.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle.egg-info/requires.txt +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_adapters.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_adversarial.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_bundle.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_checkpoint.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_cli.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_cli_eval.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_cosignature.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_eee.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_emit.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_examples.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_hf_evals.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_inspect_hook.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_intoto.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_merkle.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_merkle_property.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_promptfoo.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_rekor_interop.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_schema.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_signature.py +0 -0
- {proofbundle-1.4.0 → proofbundle-1.7.0}/tests/test_tlogproof.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofbundle
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.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
|
|
@@ -10,7 +10,7 @@ Project-URL: Issues, https://github.com/b7n0de/proofbundle/issues
|
|
|
10
10
|
Project-URL: Changelog, https://github.com/b7n0de/proofbundle/blob/main/CHANGELOG.md
|
|
11
11
|
Project-URL: Documentation, https://github.com/b7n0de/proofbundle#readme
|
|
12
12
|
Keywords: cryptography,merkle,transparency-log,ed25519,sd-jwt,verifiable-credentials,attestation,provenance,rfc6962
|
|
13
|
-
Classifier: Development Status ::
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -63,13 +63,16 @@ revocation snapshots, aligned to the in-toto test-result predicate. One portable
|
|
|
63
63
|
file, no server, no network.**
|
|
64
64
|
|
|
65
65
|
[](https://github.com/b7n0de/proofbundle/actions/workflows/ci.yml)
|
|
66
|
+
[](LICENSE)
|
|
67
|
+
[](https://github.com/astral-sh/ruff)
|
|
68
|
+
<!-- Badges below are enabled AFTER the first PyPI release (they render broken/false before it,
|
|
69
|
+
which reads as abandonment). Un-comment on first publish — see RELEASE.md:
|
|
66
70
|
[](https://pypi.org/project/proofbundle/)
|
|
67
71
|
[](https://pypi.org/project/proofbundle/)
|
|
68
72
|
[](https://pepy.tech/project/proofbundle)
|
|
69
|
-
[](LICENSE)
|
|
70
|
-
[](https://github.com/astral-sh/ruff)
|
|
71
73
|
[](https://slsa.dev)
|
|
72
74
|
[-D6248A.svg)](https://pypi.org/project/proofbundle/)
|
|
75
|
+
-->
|
|
73
76
|
[](https://github.com/C2SP/C2SP/blob/main/tlog-cosignature.md)
|
|
74
77
|
[](scripts/mutation_check.py)
|
|
75
78
|
[](https://github.com/C2SP/C2SP/blob/main/tlog-proof.md)
|
|
@@ -81,7 +84,7 @@ file, no server, no network.**
|
|
|
81
84
|
|
|
82
85
|
**At a glance:** `proofbundle emit` signs and anchors a payload; `proofbundle
|
|
83
86
|
verify` checks one self-contained `bundle.json` with three offline cryptographic
|
|
84
|
-
checks → `OK` or `FAILED`. No network, no daemon, no own crypto.
|
|
87
|
+
checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 257 tests + a CI mutation gate.
|
|
85
88
|
|
|
86
89
|
## Contents
|
|
87
90
|
|
|
@@ -146,6 +149,8 @@ not a proof that the number is *true* or that the evaluation was well designed.
|
|
|
146
149
|
| `reproduced` | independently re-run and matched |
|
|
147
150
|
| `enclave_attested` | produced in an attested trusted execution environment |
|
|
148
151
|
|
|
152
|
+
Since v1.6 `decode_eval_claim(bundle, expected_context=...)` enforces the signed `context_binding` (cross-context replay guard), and verify-side re-checks the `samples.n == n` invariant — guarantees no longer live only in the emitter.
|
|
153
|
+
|
|
149
154
|
Full detail: **[THREAT_MODEL.md](THREAT_MODEL.md)** — what `verify` catches and what it structurally cannot.
|
|
150
155
|
|
|
151
156
|
## What it verifies
|
|
@@ -223,7 +228,7 @@ pip install "proofbundle[sdjwt]"
|
|
|
223
228
|
## Quickstart
|
|
224
229
|
|
|
225
230
|
```bash
|
|
226
|
-
#
|
|
231
|
+
# from a git checkout (examples/ ships in the repo, not the wheel):
|
|
227
232
|
python examples/make_example.py
|
|
228
233
|
|
|
229
234
|
# verify it
|
|
@@ -234,6 +239,15 @@ proofbundle verify examples/example_bundle.json
|
|
|
234
239
|
<img src="assets/demo.svg" alt="proofbundle verify output: PASS checks and OK" width="680">
|
|
235
240
|
</div>
|
|
236
241
|
|
|
242
|
+
See the whole trust story in five seconds — pip-only, offline, in memory (honest receipt verifies, six tampers fail, a swapped sample is caught):
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
pip install proofbundle
|
|
246
|
+
proofbundle demo
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Full walkthrough incl. the forced-random-sample audit: **[docs/DEMO.md](docs/DEMO.md)**.
|
|
250
|
+
|
|
237
251
|
Machine-readable output and a non-zero exit code on failure:
|
|
238
252
|
|
|
239
253
|
```bash
|
|
@@ -327,7 +341,7 @@ Since v1.2 proofbundle also speaks the witness layer:
|
|
|
327
341
|
(Ed25519 cosignature/v1) — `verify_witnessed_checkpoint` checks a checkpoint is
|
|
328
342
|
both log-signed **and** cosigned by a quorum of distinct witnesses, offline,
|
|
329
343
|
which rules out a split view by the log operator. This is the same
|
|
330
|
-
witnessed-checkpoint pattern Rekor v2 (GA October 2025)
|
|
344
|
+
witnessed-checkpoint pattern Rekor v2 (GA October 2025) is integrating.
|
|
331
345
|
|
|
332
346
|
Since v1.3 the witness layer is complete: **ML-DSA-44 cosignatures** (C2SP type
|
|
333
347
|
0x06, FIPS 204 — post-quantum, the spec's SHOULD for new witness deployments;
|
|
@@ -551,15 +565,25 @@ ISO/IEC DIS 24970), including the anti-patterns no one should claim.
|
|
|
551
565
|
Status List** snapshot verification (offline revocation), SD-JWT VC markers (`dc+sd-jwt`,
|
|
552
566
|
`vct`, `status`), COMPLIANCE.md (EU AI Act Art. 12 / NIST AI RMF mapping), and the mutation
|
|
553
567
|
suite as a CI gate.
|
|
554
|
-
- **v1.4
|
|
568
|
+
- **v1.4** — distribution: a **promptfoo** adapter (results.json v3 →
|
|
555
569
|
pass_rate receipt), the **Hugging Face Community Evals bridge** (`pb1.` receipt tokens for the
|
|
556
570
|
`.eval_results` `verifyToken` field + a strict YAML entry emitter — proofbundle-verifiable,
|
|
557
571
|
explicitly NOT the HF-internal badge token), `hf-token` CLI, and the **Python 3.10+ floor**
|
|
558
572
|
(3.9 is EOL since 2025-10).
|
|
559
|
-
- **
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
573
|
+
- **v1.5** — per-sample receipts: a signed **Merkle commitment over every
|
|
574
|
+
individual sample** (`samples` claim field, SPEC §7g), salted per-leaf disclosures (RFC 9901
|
|
575
|
+
mechanic), **sample openings** with replay guards, and a **spot-check audit protocol**
|
|
576
|
+
(`audit-challenge` / `verify-opening` CLI; auditor-nonce, beacon, and documented-grindable
|
|
577
|
+
self-challenge modes; PoR soundness 1−(1−m)^k) — narrowing the THREAT_MODEL's sub-sampling gap.
|
|
578
|
+
Extractors for lm-eval `--log_samples` JSONL and promptfoo rows.
|
|
579
|
+
- **v1.6** — external principal-security review fixes: verify-side sample/`leaf_alg`/root invariants
|
|
580
|
+
and signed `context_binding` enforcement (was signed but unchecked), status-list freshness
|
|
581
|
+
(`exp`/`ttl` None-safe), `merkle.hash_alg` now required; Beta classifier; 26-operator mutation gate.
|
|
582
|
+
- **v1.6.1** — developer experience: `proofbundle demo` (pip-only, fail-closed tamper smoke test), a
|
|
583
|
+
runnable per-sample example, `docs/DEMO.md`.
|
|
584
|
+
- **v1.7 (current release)** — release supply-chain hardening: `release.yml` builds once and
|
|
585
|
+
publishes the **attested** bytes (sha256 gate: attested == published), a reviewer-gated `pypi`
|
|
586
|
+
environment, least-privilege permissions, SHA-pinned actions, `RELEASE.md` + `docs/REVIEWERS.md`.
|
|
563
587
|
- **Deferred** (explicitly not yet built, stated honestly) — full SD-JWT VC conformance + `vct`
|
|
564
588
|
type-metadata resolution (pre-IESG), an official in-toto eval predicate (proposal path via
|
|
565
589
|
OpenSSF/CoSAI), and a full in-toto client.
|
|
@@ -15,13 +15,16 @@ revocation snapshots, aligned to the in-toto test-result predicate. One portable
|
|
|
15
15
|
file, no server, no network.**
|
|
16
16
|
|
|
17
17
|
[](https://github.com/b7n0de/proofbundle/actions/workflows/ci.yml)
|
|
18
|
+
[](LICENSE)
|
|
19
|
+
[](https://github.com/astral-sh/ruff)
|
|
20
|
+
<!-- Badges below are enabled AFTER the first PyPI release (they render broken/false before it,
|
|
21
|
+
which reads as abandonment). Un-comment on first publish — see RELEASE.md:
|
|
18
22
|
[](https://pypi.org/project/proofbundle/)
|
|
19
23
|
[](https://pypi.org/project/proofbundle/)
|
|
20
24
|
[](https://pepy.tech/project/proofbundle)
|
|
21
|
-
[](LICENSE)
|
|
22
|
-
[](https://github.com/astral-sh/ruff)
|
|
23
25
|
[](https://slsa.dev)
|
|
24
26
|
[-D6248A.svg)](https://pypi.org/project/proofbundle/)
|
|
27
|
+
-->
|
|
25
28
|
[](https://github.com/C2SP/C2SP/blob/main/tlog-cosignature.md)
|
|
26
29
|
[](scripts/mutation_check.py)
|
|
27
30
|
[](https://github.com/C2SP/C2SP/blob/main/tlog-proof.md)
|
|
@@ -33,7 +36,7 @@ file, no server, no network.**
|
|
|
33
36
|
|
|
34
37
|
**At a glance:** `proofbundle emit` signs and anchors a payload; `proofbundle
|
|
35
38
|
verify` checks one self-contained `bundle.json` with three offline cryptographic
|
|
36
|
-
checks → `OK` or `FAILED`. No network, no daemon, no own crypto.
|
|
39
|
+
checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 257 tests + a CI mutation gate.
|
|
37
40
|
|
|
38
41
|
## Contents
|
|
39
42
|
|
|
@@ -98,6 +101,8 @@ not a proof that the number is *true* or that the evaluation was well designed.
|
|
|
98
101
|
| `reproduced` | independently re-run and matched |
|
|
99
102
|
| `enclave_attested` | produced in an attested trusted execution environment |
|
|
100
103
|
|
|
104
|
+
Since v1.6 `decode_eval_claim(bundle, expected_context=...)` enforces the signed `context_binding` (cross-context replay guard), and verify-side re-checks the `samples.n == n` invariant — guarantees no longer live only in the emitter.
|
|
105
|
+
|
|
101
106
|
Full detail: **[THREAT_MODEL.md](THREAT_MODEL.md)** — what `verify` catches and what it structurally cannot.
|
|
102
107
|
|
|
103
108
|
## What it verifies
|
|
@@ -175,7 +180,7 @@ pip install "proofbundle[sdjwt]"
|
|
|
175
180
|
## Quickstart
|
|
176
181
|
|
|
177
182
|
```bash
|
|
178
|
-
#
|
|
183
|
+
# from a git checkout (examples/ ships in the repo, not the wheel):
|
|
179
184
|
python examples/make_example.py
|
|
180
185
|
|
|
181
186
|
# verify it
|
|
@@ -186,6 +191,15 @@ proofbundle verify examples/example_bundle.json
|
|
|
186
191
|
<img src="assets/demo.svg" alt="proofbundle verify output: PASS checks and OK" width="680">
|
|
187
192
|
</div>
|
|
188
193
|
|
|
194
|
+
See the whole trust story in five seconds — pip-only, offline, in memory (honest receipt verifies, six tampers fail, a swapped sample is caught):
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pip install proofbundle
|
|
198
|
+
proofbundle demo
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Full walkthrough incl. the forced-random-sample audit: **[docs/DEMO.md](docs/DEMO.md)**.
|
|
202
|
+
|
|
189
203
|
Machine-readable output and a non-zero exit code on failure:
|
|
190
204
|
|
|
191
205
|
```bash
|
|
@@ -279,7 +293,7 @@ Since v1.2 proofbundle also speaks the witness layer:
|
|
|
279
293
|
(Ed25519 cosignature/v1) — `verify_witnessed_checkpoint` checks a checkpoint is
|
|
280
294
|
both log-signed **and** cosigned by a quorum of distinct witnesses, offline,
|
|
281
295
|
which rules out a split view by the log operator. This is the same
|
|
282
|
-
witnessed-checkpoint pattern Rekor v2 (GA October 2025)
|
|
296
|
+
witnessed-checkpoint pattern Rekor v2 (GA October 2025) is integrating.
|
|
283
297
|
|
|
284
298
|
Since v1.3 the witness layer is complete: **ML-DSA-44 cosignatures** (C2SP type
|
|
285
299
|
0x06, FIPS 204 — post-quantum, the spec's SHOULD for new witness deployments;
|
|
@@ -503,15 +517,25 @@ ISO/IEC DIS 24970), including the anti-patterns no one should claim.
|
|
|
503
517
|
Status List** snapshot verification (offline revocation), SD-JWT VC markers (`dc+sd-jwt`,
|
|
504
518
|
`vct`, `status`), COMPLIANCE.md (EU AI Act Art. 12 / NIST AI RMF mapping), and the mutation
|
|
505
519
|
suite as a CI gate.
|
|
506
|
-
- **v1.4
|
|
520
|
+
- **v1.4** — distribution: a **promptfoo** adapter (results.json v3 →
|
|
507
521
|
pass_rate receipt), the **Hugging Face Community Evals bridge** (`pb1.` receipt tokens for the
|
|
508
522
|
`.eval_results` `verifyToken` field + a strict YAML entry emitter — proofbundle-verifiable,
|
|
509
523
|
explicitly NOT the HF-internal badge token), `hf-token` CLI, and the **Python 3.10+ floor**
|
|
510
524
|
(3.9 is EOL since 2025-10).
|
|
511
|
-
- **
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
525
|
+
- **v1.5** — per-sample receipts: a signed **Merkle commitment over every
|
|
526
|
+
individual sample** (`samples` claim field, SPEC §7g), salted per-leaf disclosures (RFC 9901
|
|
527
|
+
mechanic), **sample openings** with replay guards, and a **spot-check audit protocol**
|
|
528
|
+
(`audit-challenge` / `verify-opening` CLI; auditor-nonce, beacon, and documented-grindable
|
|
529
|
+
self-challenge modes; PoR soundness 1−(1−m)^k) — narrowing the THREAT_MODEL's sub-sampling gap.
|
|
530
|
+
Extractors for lm-eval `--log_samples` JSONL and promptfoo rows.
|
|
531
|
+
- **v1.6** — external principal-security review fixes: verify-side sample/`leaf_alg`/root invariants
|
|
532
|
+
and signed `context_binding` enforcement (was signed but unchecked), status-list freshness
|
|
533
|
+
(`exp`/`ttl` None-safe), `merkle.hash_alg` now required; Beta classifier; 26-operator mutation gate.
|
|
534
|
+
- **v1.6.1** — developer experience: `proofbundle demo` (pip-only, fail-closed tamper smoke test), a
|
|
535
|
+
runnable per-sample example, `docs/DEMO.md`.
|
|
536
|
+
- **v1.7 (current release)** — release supply-chain hardening: `release.yml` builds once and
|
|
537
|
+
publishes the **attested** bytes (sha256 gate: attested == published), a reviewer-gated `pypi`
|
|
538
|
+
environment, least-privilege permissions, SHA-pinned actions, `RELEASE.md` + `docs/REVIEWERS.md`.
|
|
515
539
|
- **Deferred** (explicitly not yet built, stated honestly) — full SD-JWT VC conformance + `vct`
|
|
516
540
|
type-metadata resolution (pre-IESG), an official in-toto eval predicate (proposal path via
|
|
517
541
|
OpenSSF/CoSAI), and a full in-toto client.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proofbundle"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.7.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.10"
|
|
@@ -22,7 +22,7 @@ keywords = [
|
|
|
22
22
|
"rfc6962",
|
|
23
23
|
]
|
|
24
24
|
classifiers = [
|
|
25
|
-
"Development Status ::
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
26
|
"Intended Audience :: Developers",
|
|
27
27
|
"License :: OSI Approved :: MIT License",
|
|
28
28
|
"Programming Language :: Python :: 3",
|
|
@@ -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.7.0"
|
|
17
17
|
|
|
18
18
|
__all__ = [
|
|
19
19
|
"__version__",
|
|
@@ -32,6 +32,10 @@ __all__ = [
|
|
|
32
32
|
"verify_status_snapshot",
|
|
33
33
|
"receipt_token",
|
|
34
34
|
"verify_receipt_token",
|
|
35
|
+
"build_sample_tree",
|
|
36
|
+
"sample_opening",
|
|
37
|
+
"verify_sample_opening",
|
|
38
|
+
"audit_challenge",
|
|
35
39
|
"VerificationResult",
|
|
36
40
|
"Check",
|
|
37
41
|
"ProofBundleError",
|
|
@@ -51,6 +55,10 @@ _LAZY = {
|
|
|
51
55
|
"verify_status_snapshot": ".statuslist",
|
|
52
56
|
"receipt_token": ".hf_evals",
|
|
53
57
|
"verify_receipt_token": ".hf_evals",
|
|
58
|
+
"build_sample_tree": ".persample",
|
|
59
|
+
"sample_opening": ".persample",
|
|
60
|
+
"verify_sample_opening": ".persample",
|
|
61
|
+
"audit_challenge": ".persample",
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime stays lazy
|
|
@@ -60,6 +68,8 @@ if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime s
|
|
|
60
68
|
from .checkpoint import verify_cosignature, verify_witnessed_checkpoint
|
|
61
69
|
from .kbjwt import verify_key_binding
|
|
62
70
|
from .hf_evals import receipt_token, verify_receipt_token
|
|
71
|
+
from .persample import (audit_challenge, build_sample_tree, sample_opening,
|
|
72
|
+
verify_sample_opening)
|
|
63
73
|
from .statuslist import verify_status_snapshot
|
|
64
74
|
from .tlogproof import verify_tlog_proof
|
|
65
75
|
from .merkle import verify_consistency, verify_inclusion
|
|
@@ -8,6 +8,8 @@ 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
10
|
from .promptfoo import from_promptfoo_results
|
|
11
|
+
from .samples import samples_from_lm_eval_jsonl, samples_from_promptfoo_results
|
|
11
12
|
|
|
12
13
|
__all__ = ["from_lm_eval_results", "from_inspect_ai_log", "from_eee_dataset",
|
|
13
|
-
"from_promptfoo_results"
|
|
14
|
+
"from_promptfoo_results",
|
|
15
|
+
"samples_from_lm_eval_jsonl", "samples_from_promptfoo_results"]
|
|
@@ -5,7 +5,7 @@ it has no cryptography. This converter is strictly additive: it reads an EEE agg
|
|
|
5
5
|
signed, selectively-disclosable proofbundle receipt from it.
|
|
6
6
|
|
|
7
7
|
IMPORTANT: `every_eval_ever` is NOT imported at runtime — it requires Python 3.12+ (pydantic/numpy/pandas/
|
|
8
|
-
duckdb), while proofbundle stays 3.
|
|
8
|
+
duckdb), while proofbundle stays 3.10+. We parse the EEE JSON directly and OPTIONALLY validate it against the
|
|
9
9
|
vendored `eee_eval_schema.json` (schema version 0.2.2, MIT) using `jsonschema` if available.
|
|
10
10
|
|
|
11
11
|
Field mapping (verified 2026-07 against schemas/eval.schema.json v0.2.2):
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Per-sample record extractors for the v1.5 sample tree (file-based, no framework imports).
|
|
2
|
+
|
|
3
|
+
Each extractor maps a framework's per-sample export into small, canonical leaf records for
|
|
4
|
+
:func:`proofbundle.persample.build_sample_tree`. Records are returned in canonical order
|
|
5
|
+
(sorted by the framework's stable sample identity) — the tree builder assigns and embeds `idx`.
|
|
6
|
+
|
|
7
|
+
Privacy layering (two-layer hiding, per the v1.5 design): records carry the framework's
|
|
8
|
+
CONTENT HASHES (or compact result fields), never benchmark plaintext — so opening a sample for
|
|
9
|
+
audit reveals the model's result without necessarily revealing the benchmark item's text.
|
|
10
|
+
lm-evaluation-harness conveniently already emits per-doc `doc_hash`/`prompt_hash`/`target_hash`
|
|
11
|
+
(SHA-256, verified against lm_eval/evaluator.py) — note these are UNSALTED upstream hashes and
|
|
12
|
+
therefore linkable on their own; hiding comes from the salted disclosure wrapping them, which is
|
|
13
|
+
why they go INSIDE the leaf, never beside it.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import List
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def samples_from_lm_eval_jsonl(path) -> List[dict]:
|
|
24
|
+
"""Read an lm-evaluation-harness ``--log_samples`` JSONL (samples_<task>_*.jsonl) into leaf
|
|
25
|
+
records: (doc_id, filter, doc/prompt/target hashes, filtered responses, metric values).
|
|
26
|
+
|
|
27
|
+
Sorted by (doc_id, filter). Metric values are stringified (leaves are transport artifacts;
|
|
28
|
+
numeric canonicalization stays out of the commitment)."""
|
|
29
|
+
records = []
|
|
30
|
+
for line_no, line in enumerate(Path(path).read_text(encoding="utf-8").splitlines(), 1):
|
|
31
|
+
if not line.strip():
|
|
32
|
+
continue
|
|
33
|
+
try:
|
|
34
|
+
row = json.loads(line)
|
|
35
|
+
except json.JSONDecodeError as exc:
|
|
36
|
+
raise ValueError(f"line {line_no}: not valid JSON") from exc
|
|
37
|
+
if not isinstance(row, dict) or "doc_id" not in row:
|
|
38
|
+
raise ValueError(f"line {line_no}: not an lm-eval sample row")
|
|
39
|
+
doc_id = row.get("doc_id")
|
|
40
|
+
if isinstance(doc_id, bool) or not isinstance(doc_id, int):
|
|
41
|
+
raise ValueError(f"line {line_no}: doc_id missing or not an integer")
|
|
42
|
+
metrics = row.get("metrics") or []
|
|
43
|
+
rec = {"id": doc_id, "epoch": 1, "filter": str(row.get("filter", "none")),
|
|
44
|
+
"doc_hash": str(row.get("doc_hash", "")),
|
|
45
|
+
"prompt_hash": str(row.get("prompt_hash", "")),
|
|
46
|
+
"target_hash": str(row.get("target_hash", "")),
|
|
47
|
+
"filtered_resps": [str(r) for r in (row.get("filtered_resps") or [])],
|
|
48
|
+
"metrics": {m: str(row[m]) for m in metrics if m in row}}
|
|
49
|
+
records.append(rec)
|
|
50
|
+
if not records:
|
|
51
|
+
raise ValueError("no sample rows found")
|
|
52
|
+
records.sort(key=lambda r: (r["id"], r["filter"]))
|
|
53
|
+
return records
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def samples_from_promptfoo_results(path) -> List[dict]:
|
|
57
|
+
"""Read a promptfoo results.json (summary v3) into leaf records:
|
|
58
|
+
(testIdx, promptIdx, provider, success, score). Sorted by (testIdx, promptIdx, provider)."""
|
|
59
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
60
|
+
summary = data.get("results")
|
|
61
|
+
if not isinstance(summary, dict) or summary.get("version") != 3:
|
|
62
|
+
raise ValueError("not a promptfoo v3 output file (see adapters.promptfoo)")
|
|
63
|
+
records = []
|
|
64
|
+
for i, row in enumerate(summary.get("results") or []):
|
|
65
|
+
if not isinstance(row, dict):
|
|
66
|
+
raise ValueError(f"results[{i}] is not an object")
|
|
67
|
+
test_idx, prompt_idx = row.get("testIdx"), row.get("promptIdx")
|
|
68
|
+
for name, val in (("testIdx", test_idx), ("promptIdx", prompt_idx)):
|
|
69
|
+
if isinstance(val, bool) or not isinstance(val, int):
|
|
70
|
+
raise ValueError(f"results[{i}].{name} missing or not an integer")
|
|
71
|
+
success = row.get("success")
|
|
72
|
+
if not isinstance(success, bool):
|
|
73
|
+
raise ValueError(f"results[{i}].success missing or not a boolean")
|
|
74
|
+
prov = row.get("provider")
|
|
75
|
+
provider = str(prov.get("id")) if isinstance(prov, dict) else str(prov or "unknown")
|
|
76
|
+
records.append({"id": test_idx, "epoch": 1, "prompt_idx": prompt_idx,
|
|
77
|
+
"provider": provider, "success": success,
|
|
78
|
+
"score": str(row.get("score", ""))})
|
|
79
|
+
if not records:
|
|
80
|
+
raise ValueError("no result rows found")
|
|
81
|
+
records.sort(key=lambda r: (r["id"], r["prompt_idx"], r["provider"]))
|
|
82
|
+
return records
|
|
@@ -132,7 +132,9 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
|
|
|
132
132
|
# 2. merkle inclusion of the payload
|
|
133
133
|
mk = _require_dict(_require(bundle, "merkle", "merkle"), "merkle")
|
|
134
134
|
_reject_unknown(mk, _MERKLE_KEYS, "merkle")
|
|
135
|
-
|
|
135
|
+
# v1.6 (external review): hash_alg is REQUIRED — the emitter always writes it, and a
|
|
136
|
+
# silent default is exactly where a future multi-alg version would hide an alg-confusion.
|
|
137
|
+
hash_alg = _require(mk, "hash_alg", "merkle.hash_alg")
|
|
136
138
|
if hash_alg != "sha256-rfc6962":
|
|
137
139
|
raise UnsupportedError(f"merkle hash_alg {hash_alg!r} not supported in v0.1")
|
|
138
140
|
leaf_index = _require_int(mk, "leaf_index", "merkle.leaf_index")
|
|
@@ -189,6 +191,20 @@ def verify_bundle(bundle: Union[dict, str], *, expected_aud=None, expected_nonce
|
|
|
189
191
|
"sd-jwt-key-binding", False,
|
|
190
192
|
"issuer bound a holder key (cnf) but the presentation carries NO Key Binding JWT — "
|
|
191
193
|
"required proof-of-possession is missing (bearer downgrade, RFC 9901 §4.3)")
|
|
194
|
+
elif not sd_res.get("sig_checked"):
|
|
195
|
+
# v1.6 fail-closed (external review, CRITICAL): gating holder-binding on issuer
|
|
196
|
+
# verification opened a KEY-OMISSION downgrade — strip the KB-JWT AND drop
|
|
197
|
+
# issuer_public_key_b64, and a cnf-bound credential silently passed as a bearer
|
|
198
|
+
# token. A `cnf`-carrying SD-JWT whose issuer cannot be verified is REFUSED, never
|
|
199
|
+
# "structure-only passed". SD-JWTs without `cnf` keep the documented no-key
|
|
200
|
+
# backward-compat path unchanged.
|
|
201
|
+
sd_part, _ = split_key_binding(compact)
|
|
202
|
+
if _issuer_requires_holder_binding(sd_part):
|
|
203
|
+
result.add(
|
|
204
|
+
"sd-jwt-key-binding", False,
|
|
205
|
+
"SD-JWT declares a cnf holder key but NO issuer key was supplied — holder "
|
|
206
|
+
"binding is unverifiable, refusing (fail-closed; supply "
|
|
207
|
+
"sd_jwt_vc.issuer_public_key_b64)")
|
|
192
208
|
|
|
193
209
|
return result
|
|
194
210
|
|
|
@@ -208,9 +224,9 @@ def recompute_merkle_root_b64(bundle: Union[dict, str]) -> dict:
|
|
|
208
224
|
raise BundleFormatError("bundle must be a JSON object")
|
|
209
225
|
payload = _b64d(_require(bundle, "payload_b64", "payload_b64"), "payload_b64")
|
|
210
226
|
mk = _require_dict(_require(bundle, "merkle", "merkle"), "merkle")
|
|
211
|
-
# Validate hash_alg the
|
|
212
|
-
#
|
|
213
|
-
hash_alg = mk
|
|
227
|
+
# Validate hash_alg the SAME way verify_bundle does — REQUIRED, not silently defaulted (release-review #13:
|
|
228
|
+
# the docstring claimed strict-as-verify_bundle but this defaulted a missing hash_alg; verify_bundle _require's it).
|
|
229
|
+
hash_alg = _require(mk, "hash_alg", "merkle.hash_alg")
|
|
214
230
|
if hash_alg != "sha256-rfc6962":
|
|
215
231
|
raise UnsupportedError(f"merkle hash_alg {hash_alg!r} not supported in v0.1")
|
|
216
232
|
leaf_index = _require_int(mk, "leaf_index", "merkle.leaf_index")
|
|
@@ -48,11 +48,15 @@ def _cmd_emit_eval(args: argparse.Namespace) -> int:
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def _cmd_show_eval(args: argparse.Namespace) -> int:
|
|
51
|
+
from .bundle import load_bundle # noqa: PLC0415
|
|
51
52
|
from .evalclaim import ( # noqa: PLC0415
|
|
52
53
|
DEFAULT_ASSURANCE, check_freshness, claim_warnings, decode_eval_claim, sd_jwt_hidden_count,
|
|
53
54
|
)
|
|
54
55
|
try:
|
|
55
|
-
|
|
56
|
+
# Resolve the path to a dict ONCE and pass that object to every reader — a second per-function re-read of
|
|
57
|
+
# the same path would reopen a TOCTOU window (CWE-367) between the reads. Release-review fix 2026-07-02.
|
|
58
|
+
bundle = load_bundle(args.receipt)
|
|
59
|
+
claim = decode_eval_claim(bundle, expected_context=getattr(args, "context", None))
|
|
56
60
|
except (OSError, ValueError, ProofBundleError) as exc: # missing/invalid receipt file → clean exit, not a traceback
|
|
57
61
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
58
62
|
return 2
|
|
@@ -67,7 +71,7 @@ def _cmd_show_eval(args: argparse.Namespace) -> int:
|
|
|
67
71
|
print(f"dataset commit {claim['dataset_id_commit']}")
|
|
68
72
|
print(f"issuer {claim['issuer']}")
|
|
69
73
|
print(f"timestamp {claim['timestamp']}")
|
|
70
|
-
hidden = sd_jwt_hidden_count(
|
|
74
|
+
hidden = sd_jwt_hidden_count(bundle)
|
|
71
75
|
if hidden is not None:
|
|
72
76
|
print(f"sd-jwt {hidden} field(s) withheld (selective disclosure)")
|
|
73
77
|
fresh = check_freshness(claim)
|
|
@@ -80,10 +84,14 @@ def _cmd_show_eval(args: argparse.Namespace) -> int:
|
|
|
80
84
|
|
|
81
85
|
|
|
82
86
|
def _cmd_verify(args: argparse.Namespace) -> int:
|
|
87
|
+
from .bundle import load_bundle # noqa: PLC0415
|
|
83
88
|
try:
|
|
84
|
-
|
|
89
|
+
# Resolve the path to a dict ONCE and pass it to both verify_bundle and recompute — a second per-function
|
|
90
|
+
# re-read of the same path reopens a TOCTOU window (release-review consistency fix, mirrors show-eval).
|
|
91
|
+
bundle = load_bundle(args.bundle)
|
|
92
|
+
result = verify_bundle(bundle, expected_aud=getattr(args, "aud", None),
|
|
85
93
|
expected_nonce=getattr(args, "nonce", None))
|
|
86
|
-
roots = recompute_merkle_root_b64(
|
|
94
|
+
roots = recompute_merkle_root_b64(bundle) if args.verbose else None
|
|
87
95
|
except (ProofBundleError, OSError, ValueError) as exc: # file/JSON/format errors → clean exit, never a raw traceback
|
|
88
96
|
if args.json:
|
|
89
97
|
print(json.dumps({"ok": False, "error": str(exc)}))
|
|
@@ -177,6 +185,50 @@ def _cmd_hf_token(args: argparse.Namespace) -> int:
|
|
|
177
185
|
return 2
|
|
178
186
|
|
|
179
187
|
|
|
188
|
+
def _cmd_audit_challenge(args: argparse.Namespace) -> int:
|
|
189
|
+
from .persample import audit_challenge # noqa: PLC0415
|
|
190
|
+
try:
|
|
191
|
+
nonce = bytes.fromhex(args.nonce) if args.nonce else b""
|
|
192
|
+
indices = audit_challenge(args.root, args.n, args.k, nonce)
|
|
193
|
+
except (ProofBundleError, ValueError) as exc:
|
|
194
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
195
|
+
return 2
|
|
196
|
+
if args.json:
|
|
197
|
+
print(json.dumps({"indices": indices, "n": args.n, "k": args.k,
|
|
198
|
+
"mode": "auditor-nonce" if args.nonce else "self-challenge"}))
|
|
199
|
+
else:
|
|
200
|
+
if not args.nonce:
|
|
201
|
+
print("WARNING: self-challenge mode (no --nonce) is a sanity check only — "
|
|
202
|
+
"a producer can grind by re-salting; real audits supply a fresh nonce",
|
|
203
|
+
file=sys.stderr)
|
|
204
|
+
print(" ".join(str(i) for i in indices))
|
|
205
|
+
return 0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _cmd_verify_opening(args: argparse.Namespace) -> int:
|
|
209
|
+
from .persample import verify_sample_opening # noqa: PLC0415
|
|
210
|
+
try:
|
|
211
|
+
with open(args.opening, encoding="utf-8") as handle:
|
|
212
|
+
opening = json.load(handle)
|
|
213
|
+
res = verify_sample_opening(opening, args.root, args.n)
|
|
214
|
+
except (ProofBundleError, OSError, ValueError) as exc:
|
|
215
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
216
|
+
return 2
|
|
217
|
+
if args.json:
|
|
218
|
+
print(json.dumps(res))
|
|
219
|
+
else:
|
|
220
|
+
print(f"[{'PASS' if res['ok'] else 'FAIL'}] sample-opening: {res['detail']}")
|
|
221
|
+
if res["ok"]:
|
|
222
|
+
print(json.dumps(res["record"], indent=2))
|
|
223
|
+
print("=> OK" if res["ok"] else "=> FAILED")
|
|
224
|
+
return 0 if res["ok"] else 1
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _cmd_demo(args: argparse.Namespace) -> int:
|
|
228
|
+
from .demo import run_demo # noqa: PLC0415
|
|
229
|
+
return run_demo(as_json=args.json)
|
|
230
|
+
|
|
231
|
+
|
|
180
232
|
def build_parser() -> argparse.ArgumentParser:
|
|
181
233
|
parser = argparse.ArgumentParser(
|
|
182
234
|
prog="proofbundle",
|
|
@@ -213,6 +265,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
213
265
|
|
|
214
266
|
show_eval = sub.add_parser("show-eval", help="verify an eval receipt and print the claim")
|
|
215
267
|
show_eval.add_argument("receipt", help="path to the eval receipt bundle JSON")
|
|
268
|
+
show_eval.add_argument("--context", dest="context", default=None,
|
|
269
|
+
help="require the receipt's signed context_binding to equal this (cross-context replay guard)")
|
|
216
270
|
show_eval.set_defaults(func=_cmd_show_eval)
|
|
217
271
|
|
|
218
272
|
verify_proof = sub.add_parser(
|
|
@@ -238,6 +292,31 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
238
292
|
help="verify a pb1. token instead of emitting one")
|
|
239
293
|
hf_token.set_defaults(func=_cmd_hf_token)
|
|
240
294
|
|
|
295
|
+
challenge = sub.add_parser(
|
|
296
|
+
"audit-challenge",
|
|
297
|
+
help="derive k audit indices from a samples root (v1.5; supply --nonce for real audits)")
|
|
298
|
+
challenge.add_argument("root", help="the receipt's samples root (base64)")
|
|
299
|
+
challenge.add_argument("n", type=int, help="committed sample count")
|
|
300
|
+
challenge.add_argument("k", type=int, help="number of samples to challenge")
|
|
301
|
+
challenge.add_argument("--nonce", help="fresh auditor nonce (hex, >=32 hex chars recommended)")
|
|
302
|
+
challenge.add_argument("--json", action="store_true", help="machine readable output")
|
|
303
|
+
challenge.set_defaults(func=_cmd_audit_challenge)
|
|
304
|
+
|
|
305
|
+
verify_opening = sub.add_parser(
|
|
306
|
+
"verify-opening", help="verify one sample opening against a samples root (v1.5)")
|
|
307
|
+
verify_opening.add_argument("opening", help="opening JSON file (index/disclosure/proof_b64)")
|
|
308
|
+
verify_opening.add_argument("--root", required=True, help="the receipt's samples root (base64)")
|
|
309
|
+
verify_opening.add_argument("--n", required=True, type=int, help="committed sample count")
|
|
310
|
+
verify_opening.add_argument("--json", action="store_true", help="machine readable output")
|
|
311
|
+
verify_opening.set_defaults(func=_cmd_verify_opening)
|
|
312
|
+
|
|
313
|
+
demo = sub.add_parser(
|
|
314
|
+
"demo",
|
|
315
|
+
help="run the whole trust story in memory (pip-only, offline): honest receipt verifies, "
|
|
316
|
+
"six tampers fail, a swapped sample is caught")
|
|
317
|
+
demo.add_argument("--json", action="store_true", help="machine readable output")
|
|
318
|
+
demo.set_defaults(func=_cmd_demo)
|
|
319
|
+
|
|
241
320
|
return parser
|
|
242
321
|
|
|
243
322
|
|