proofbundle 1.9.0__tar.gz → 2.0.0b1__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.9.0/src/proofbundle.egg-info → proofbundle-2.0.0b1}/PKG-INFO +6 -4
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/README.md +4 -3
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/pyproject.toml +6 -1
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/__init__.py +1 -1
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/cli.py +39 -0
- proofbundle-2.0.0b1/src/proofbundle/experimental/__init__.py +28 -0
- proofbundle-2.0.0b1/src/proofbundle/experimental/enclave.py +196 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/statuslist.py +32 -5
- {proofbundle-1.9.0 → proofbundle-2.0.0b1/src/proofbundle.egg-info}/PKG-INFO +6 -4
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/SOURCES.txt +3 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/requires.txt +2 -0
- proofbundle-2.0.0b1/tests/test_experimental_enclave.py +205 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_fuzz_parsers.py +16 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_statuslist.py +32 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/LICENSE +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/setup.cfg +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/_integration.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/_provenance.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/eee.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/samples.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/beacon.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/bundle.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/demo.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/dsse.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/emit.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/errors.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/evalclaim.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/hf_evals.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/intoto.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/merkle.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/persample.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/prereg.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/py.typed +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/sdjwt.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/signature.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_adapters.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_adversarial.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_beacon.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_bundle.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_checkpoint.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_cli.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_cli_eval.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_cosignature.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_demo.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_eee.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_emit.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_evalclaim.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_examples.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_hf_evals.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_inspect_hook.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_intoto.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_kbjwt.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_merkle.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_merkle_property.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_persample.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_prereg.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_promptfoo.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_provenance.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_rekor_interop.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_schema.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_signature.py +0 -0
- {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_tlogproof.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofbundle
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0b1
|
|
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
|
|
@@ -28,6 +28,7 @@ Provides-Extra: sdjwt
|
|
|
28
28
|
Provides-Extra: eval
|
|
29
29
|
Requires-Dist: rfc8785>=0.1.4; extra == "eval"
|
|
30
30
|
Provides-Extra: adapters
|
|
31
|
+
Provides-Extra: experimental
|
|
31
32
|
Provides-Extra: pq
|
|
32
33
|
Requires-Dist: cryptography>=48; extra == "pq"
|
|
33
34
|
Provides-Extra: pytest
|
|
@@ -145,6 +146,7 @@ flowchart LR
|
|
|
145
146
|
| For… | Read |
|
|
146
147
|
|---|---|
|
|
147
148
|
| Skeptics (why not SHA-256 / Sigstore / trust the issuer) | [docs/FAQ.md](docs/FAQ.md) |
|
|
149
|
+
| New to this? plain-terms glossary | [docs/GLOSSARY.md](docs/GLOSSARY.md) |
|
|
148
150
|
| Reviewers (30-minute adversarial audit path) | [docs/REVIEWERS.md](docs/REVIEWERS.md) |
|
|
149
151
|
| Where every trust anchor comes from | [docs/TRUST_ANCHORS.md](docs/TRUST_ANCHORS.md) |
|
|
150
152
|
| The demos, tier by tier | [docs/DEMO.md](docs/DEMO.md) |
|
|
@@ -152,13 +154,13 @@ flowchart LR
|
|
|
152
154
|
| Honest comparison to Rekor / in-toto / OMS / ValiChord | [INTEROP.md](INTEROP.md) |
|
|
153
155
|
| Regulatory mapping (and what to never claim) | [COMPLIANCE.md](COMPLIANCE.md) |
|
|
154
156
|
| Funders / role fit | [docs/PROJECT_BRIEF.md](docs/PROJECT_BRIEF.md) |
|
|
157
|
+
| **Preview:** TEE-attestation bridge (v2.0 beta) | [docs/EXPERIMENTAL_ENCLAVE.md](docs/EXPERIMENTAL_ENCLAVE.md) |
|
|
155
158
|
|
|
156
159
|
## Install
|
|
157
160
|
|
|
158
161
|
```bash
|
|
159
162
|
pip install proofbundle # core: offline verify + plain emit (dependency-free)
|
|
160
|
-
pip install "proofbundle[eval]" # + eval receipts, prereg, and the demo (adds
|
|
161
|
-
pip install "proofbundle[eval]" # emit eval receipts (adds an RFC 8785 canonicalizer)
|
|
163
|
+
pip install "proofbundle[eval]" # + eval receipts, prereg, and the demo (adds an RFC 8785 JCS canonicalizer)
|
|
162
164
|
pip install "proofbundle[inspect]" # inspect_ai adapter + hook
|
|
163
165
|
pip install "proofbundle[pq]" # verify ML-DSA-44 (post-quantum) witness cosignatures
|
|
164
166
|
```
|
|
@@ -168,7 +170,7 @@ Requires Python 3.10+. The verify path never rolls its own crypto — Ed25519 co
|
|
|
168
170
|
|
|
169
171
|
## Status & scope
|
|
170
172
|
|
|
171
|
-
Beta, SemVer-committed,
|
|
173
|
+
Beta, SemVer-committed, 303 tests + a CI mutation gate + property-based parser fuzzing. Correctness
|
|
172
174
|
is anchored to external RFC 6962 vectors and a real Rekor proof, not just its own bundles. It is
|
|
173
175
|
**not** a log service, a full in-toto client, a TEE, a consensus network, or a compliance product
|
|
174
176
|
by itself — it is the small, offline, standards-native receipt layer between them. Security policy:
|
|
@@ -97,6 +97,7 @@ flowchart LR
|
|
|
97
97
|
| For… | Read |
|
|
98
98
|
|---|---|
|
|
99
99
|
| Skeptics (why not SHA-256 / Sigstore / trust the issuer) | [docs/FAQ.md](docs/FAQ.md) |
|
|
100
|
+
| New to this? plain-terms glossary | [docs/GLOSSARY.md](docs/GLOSSARY.md) |
|
|
100
101
|
| Reviewers (30-minute adversarial audit path) | [docs/REVIEWERS.md](docs/REVIEWERS.md) |
|
|
101
102
|
| Where every trust anchor comes from | [docs/TRUST_ANCHORS.md](docs/TRUST_ANCHORS.md) |
|
|
102
103
|
| The demos, tier by tier | [docs/DEMO.md](docs/DEMO.md) |
|
|
@@ -104,13 +105,13 @@ flowchart LR
|
|
|
104
105
|
| Honest comparison to Rekor / in-toto / OMS / ValiChord | [INTEROP.md](INTEROP.md) |
|
|
105
106
|
| Regulatory mapping (and what to never claim) | [COMPLIANCE.md](COMPLIANCE.md) |
|
|
106
107
|
| Funders / role fit | [docs/PROJECT_BRIEF.md](docs/PROJECT_BRIEF.md) |
|
|
108
|
+
| **Preview:** TEE-attestation bridge (v2.0 beta) | [docs/EXPERIMENTAL_ENCLAVE.md](docs/EXPERIMENTAL_ENCLAVE.md) |
|
|
107
109
|
|
|
108
110
|
## Install
|
|
109
111
|
|
|
110
112
|
```bash
|
|
111
113
|
pip install proofbundle # core: offline verify + plain emit (dependency-free)
|
|
112
|
-
pip install "proofbundle[eval]" # + eval receipts, prereg, and the demo (adds
|
|
113
|
-
pip install "proofbundle[eval]" # emit eval receipts (adds an RFC 8785 canonicalizer)
|
|
114
|
+
pip install "proofbundle[eval]" # + eval receipts, prereg, and the demo (adds an RFC 8785 JCS canonicalizer)
|
|
114
115
|
pip install "proofbundle[inspect]" # inspect_ai adapter + hook
|
|
115
116
|
pip install "proofbundle[pq]" # verify ML-DSA-44 (post-quantum) witness cosignatures
|
|
116
117
|
```
|
|
@@ -120,7 +121,7 @@ Requires Python 3.10+. The verify path never rolls its own crypto — Ed25519 co
|
|
|
120
121
|
|
|
121
122
|
## Status & scope
|
|
122
123
|
|
|
123
|
-
Beta, SemVer-committed,
|
|
124
|
+
Beta, SemVer-committed, 303 tests + a CI mutation gate + property-based parser fuzzing. Correctness
|
|
124
125
|
is anchored to external RFC 6962 vectors and a real Rekor proof, not just its own bundles. It is
|
|
125
126
|
**not** a log service, a full in-toto client, a TEE, a consensus network, or a compliance product
|
|
126
127
|
by itself — it is the small, offline, standards-native receipt layer between them. Security policy:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proofbundle"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2.0.0b1"
|
|
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"
|
|
@@ -46,6 +46,11 @@ sdjwt = []
|
|
|
46
46
|
eval = ["rfc8785>=0.1.4"]
|
|
47
47
|
# The lm-eval adapter reads exported results.json (no import) → pure stdlib.
|
|
48
48
|
adapters = []
|
|
49
|
+
# EXPERIMENTAL (v2.0 preview): the TEE-attestation bridge (proofbundle.experimental.enclave).
|
|
50
|
+
# Opt-in and clearly unstable — API/wire-format may change or be removed without deprecation.
|
|
51
|
+
# No new runtime dependency (pure stdlib + the core `cryptography`); the extra exists to signal
|
|
52
|
+
# intent and gate the preview, mirroring the `sdjwt` extra. `pip install "proofbundle[experimental]"`.
|
|
53
|
+
experimental = []
|
|
49
54
|
# ML-DSA-44 (FIPS 204) cosignature verification (C2SP tlog-cosignature type 0x06). Needs a
|
|
50
55
|
# cryptography build with OpenSSL 3.5+ (default PyPI wheels since 48.0.0). Verify-only by default;
|
|
51
56
|
# the emit helper needs the same. Ed25519 paths never require this.
|
|
@@ -283,6 +283,34 @@ def _cmd_prereg(args: argparse.Namespace) -> int:
|
|
|
283
283
|
return 2
|
|
284
284
|
|
|
285
285
|
|
|
286
|
+
def _cmd_verify_enclave(args: argparse.Namespace) -> int:
|
|
287
|
+
import base64 as _b64 # noqa: PLC0415
|
|
288
|
+
from .bundle import load_bundle # noqa: PLC0415
|
|
289
|
+
from .experimental.enclave import (enclave_binding_for, # noqa: PLC0415
|
|
290
|
+
verify_enclave_attestation)
|
|
291
|
+
try:
|
|
292
|
+
bundle = load_bundle(args.receipt)
|
|
293
|
+
with open(args.eat, encoding="utf-8") as handle:
|
|
294
|
+
eat = handle.read().strip()
|
|
295
|
+
verifier_pub = _b64.b64decode(args.verifier_key, validate=True)
|
|
296
|
+
res = verify_enclave_attestation(
|
|
297
|
+
eat, verifier_pubkey=verifier_pub, expected_binding=enclave_binding_for(bundle),
|
|
298
|
+
expected_profile=args.profile)
|
|
299
|
+
except (ProofBundleError, OSError, ValueError) as exc:
|
|
300
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
301
|
+
return 2
|
|
302
|
+
if args.json:
|
|
303
|
+
print(json.dumps({k: res[k] for k in ("ok", "tier", "profile", "ueid", "nonce_ok",
|
|
304
|
+
"fresh", "detail")}))
|
|
305
|
+
else:
|
|
306
|
+
print(f"[{'PASS' if res['ok'] else 'FAIL'}] enclave-attestation: {res['detail']}")
|
|
307
|
+
if res["ok"]:
|
|
308
|
+
print(f" tier {res['tier']}")
|
|
309
|
+
print(f" profile {res['profile']}")
|
|
310
|
+
print("=> OK" if res["ok"] else "=> FAILED")
|
|
311
|
+
return 0 if res["ok"] else 1
|
|
312
|
+
|
|
313
|
+
|
|
286
314
|
def build_parser() -> argparse.ArgumentParser:
|
|
287
315
|
parser = argparse.ArgumentParser(
|
|
288
316
|
prog="proofbundle",
|
|
@@ -369,6 +397,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
369
397
|
verify_opening.add_argument("--json", action="store_true", help="machine readable output")
|
|
370
398
|
verify_opening.set_defaults(func=_cmd_verify_opening)
|
|
371
399
|
|
|
400
|
+
verify_enclave = sub.add_parser(
|
|
401
|
+
"verify-enclave",
|
|
402
|
+
help="[EXPERIMENTAL v2.0] verify a TEE Attestation Result (EAT) bound to a receipt")
|
|
403
|
+
verify_enclave.add_argument("eat", help="path to the EAT (compact JWS) file")
|
|
404
|
+
verify_enclave.add_argument("--receipt", required=True, help="the receipt bundle JSON the EAT must bind")
|
|
405
|
+
verify_enclave.add_argument("--verifier-key", required=True,
|
|
406
|
+
help="the RATS Verifier's Ed25519 public key (base64)")
|
|
407
|
+
verify_enclave.add_argument("--profile", help="pin an expected eat_profile URI (optional)")
|
|
408
|
+
verify_enclave.add_argument("--json", action="store_true", help="machine readable output")
|
|
409
|
+
verify_enclave.set_defaults(func=_cmd_verify_enclave)
|
|
410
|
+
|
|
372
411
|
demo = sub.add_parser(
|
|
373
412
|
"demo",
|
|
374
413
|
help="run the whole trust story in memory (pip-only, offline): honest receipt verifies, "
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Experimental, opt-in features — NOT part of the stable API surface (v2.0 beta).
|
|
2
|
+
|
|
3
|
+
Everything under ``proofbundle.experimental`` is a preview: its API, wire format, and behavior may
|
|
4
|
+
change or be removed in any release without a deprecation cycle. It is deliberately NOT re-exported
|
|
5
|
+
from the top-level package — you must import it explicitly, e.g.
|
|
6
|
+
``from proofbundle.experimental.enclave import verify_enclave_attestation``. Importing this
|
|
7
|
+
subpackage emits an ``ExperimentalWarning`` once, so nobody depends on it by accident.
|
|
8
|
+
|
|
9
|
+
The stable v1.x trusted core (signature / merkle / bundle) does not import anything here.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import warnings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExperimentalWarning(UserWarning):
|
|
18
|
+
"""Raised once when the experimental subpackage is first imported."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
warnings.warn(
|
|
22
|
+
"proofbundle.experimental is a v2.0 preview: its API and wire format may change or be removed "
|
|
23
|
+
"in any release without deprecation. Do not depend on it in production.",
|
|
24
|
+
ExperimentalWarning,
|
|
25
|
+
stacklevel=2,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = ["ExperimentalWarning"]
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""TEE-attestation bridge — bind a receipt to hardware-attested execution (v2.0 PREVIEW).
|
|
2
|
+
|
|
3
|
+
This closes the one gap a software-only receipt structurally has: it proves *who signed these
|
|
4
|
+
bytes and that nothing changed*, but not *that the computation ran untampered*. A Trusted
|
|
5
|
+
Execution Environment (Intel TDX, an NVIDIA confidential-computing GPU, …) can attest the latter.
|
|
6
|
+
This module lets a receipt carry `assurance_level = enclave_attested` in a way a relying party
|
|
7
|
+
can actually check — offline, standards-native, vendor-neutral.
|
|
8
|
+
|
|
9
|
+
**Scope, stated with the same honesty as the rest of proofbundle — read this before trusting it.**
|
|
10
|
+
proofbundle does NOT parse or appraise raw hardware evidence (a TDX DCAP quote, an NVIDIA GPU
|
|
11
|
+
attestation report). Appraising raw evidence needs vendor libraries, live collateral, and TCB
|
|
12
|
+
policy — it is the job of a **Verifier** in the IETF RATS architecture (RFC 9334). This module
|
|
13
|
+
implements the RATS **Passport model**: the Verifier appraises the evidence out of band and issues
|
|
14
|
+
a signed **Attestation Result**; proofbundle verifies that Attestation Result offline against the
|
|
15
|
+
Verifier's public key (a supplied trust anchor, exactly like every other anchor in
|
|
16
|
+
docs/TRUST_ANCHORS.md) and checks that it is bound to *this* receipt. So the chain of trust is:
|
|
17
|
+
you trust the Verifier's key → the Verifier's result says the enclave was genuine → the result's
|
|
18
|
+
nonce binds it to your receipt. proofbundle checks the last two links; the first is your anchor.
|
|
19
|
+
|
|
20
|
+
**Wire format (v2.0 preview — subject to change).** The Attestation Result is an EAT
|
|
21
|
+
(Entity Attestation Token, RFC 9711) in its **JSON/JWS** encoding, signed with **EdDSA** (the same
|
|
22
|
+
primitive as everything else here — no new dependency). Claims used:
|
|
23
|
+
- ``eat_nonce`` (RFC 9711 §4.1) — the binding: it MUST equal ``enclave_binding_for(bundle)``,
|
|
24
|
+
a base64url SHA-256 over the receipt's exact signed payload. Practically, the enclave places
|
|
25
|
+
this value in the hardware quote's user-data (Intel TDX ``REPORTDATA`` / an NVIDIA GPU report
|
|
26
|
+
nonce) when it runs the eval, so "this hardware ran the computation that produced this receipt"
|
|
27
|
+
is cryptographically tied. proofbundle only checks the EAT-level equality — it does not see
|
|
28
|
+
the raw quote.
|
|
29
|
+
- ``eat_profile`` (RFC 9711 §4.3.2) — a profile URI identifying the appraisal semantics; a
|
|
30
|
+
relying party MAY pin an expected profile.
|
|
31
|
+
- ``ueid`` (optional) — the attested entity/enclave id, reported, not interpreted.
|
|
32
|
+
- a ``tier`` string (this preview's stand-in for the AR4SI/EAR trustworthiness tier, which are
|
|
33
|
+
still IETF Internet-Drafts) — REPORTED verbatim, never interpreted as a guarantee.
|
|
34
|
+
- optional ``iat``/``exp`` — freshness, reported and only judged when the caller passes ``now``
|
|
35
|
+
(offline verifier, no trusted clock — same discipline as the status-list module).
|
|
36
|
+
|
|
37
|
+
What this does NOT establish, and must never be claimed: that the enclave is genuine (that is the
|
|
38
|
+
Verifier's appraisal, which you trust via its key), that the TEE vendor's root of trust is sound,
|
|
39
|
+
or that the eval inside the enclave was well-designed or honest. It raises the assurance floor from
|
|
40
|
+
"the issuer says so" to "a Verifier you trust attested the enclave, bound to this receipt" — no
|
|
41
|
+
further.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import base64
|
|
47
|
+
import hashlib
|
|
48
|
+
import json
|
|
49
|
+
from typing import Optional
|
|
50
|
+
|
|
51
|
+
from ..errors import BundleFormatError
|
|
52
|
+
from ..signature import verify_ed25519
|
|
53
|
+
|
|
54
|
+
__all__ = ["EAT_TYP", "enclave_binding_for", "verify_enclave_attestation",
|
|
55
|
+
"issue_enclave_attestation"]
|
|
56
|
+
|
|
57
|
+
EAT_TYP = "eat+jwt" # RFC 9711 media type application/eat+jwt
|
|
58
|
+
_BINDING_DOMAIN = b"proofbundle/v2/enclave-binding\x00"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _b64url(data: bytes) -> str:
|
|
62
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _b64url_decode(s: str) -> bytes:
|
|
66
|
+
raw = s.encode("ascii")
|
|
67
|
+
return base64.urlsafe_b64decode(raw + b"=" * (-len(raw) % 4))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def enclave_binding_for(bundle: dict) -> str:
|
|
71
|
+
"""The value an Attestation Result's ``eat_nonce`` MUST carry to be bound to ``bundle``.
|
|
72
|
+
|
|
73
|
+
= base64url(SHA-256(domain || the bundle's exact signed payload bytes)). This is what an
|
|
74
|
+
enclave puts in its hardware quote's user-data (TDX REPORTDATA / GPU report nonce) at run
|
|
75
|
+
time, so the resulting Attestation Result is tied to this exact receipt. Fits the RFC 9711
|
|
76
|
+
JSON ``eat_nonce`` size window (a 43-char base64url of 32 bytes, well within 8..88).
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(bundle, dict) or "payload_b64" not in bundle:
|
|
79
|
+
raise BundleFormatError("enclave_binding_for needs a bundle dict with payload_b64")
|
|
80
|
+
try:
|
|
81
|
+
payload = base64.b64decode(bundle["payload_b64"], validate=True)
|
|
82
|
+
except (ValueError, TypeError) as exc:
|
|
83
|
+
raise BundleFormatError("bundle.payload_b64 is not valid base64") from exc
|
|
84
|
+
return _b64url(hashlib.sha256(_BINDING_DOMAIN + payload).digest())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _match_nonce(eat_nonce, expected: str) -> bool:
|
|
88
|
+
"""RFC 9711 eat_nonce is a text string OR an array of text strings — accept either, match any."""
|
|
89
|
+
if isinstance(eat_nonce, str):
|
|
90
|
+
return eat_nonce == expected
|
|
91
|
+
if isinstance(eat_nonce, list):
|
|
92
|
+
return any(isinstance(x, str) and x == expected for x in eat_nonce)
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def verify_enclave_attestation(eat_jws: str, *, verifier_pubkey: bytes, expected_binding: str,
|
|
97
|
+
expected_profile: Optional[str] = None,
|
|
98
|
+
now: Optional[int] = None) -> dict:
|
|
99
|
+
"""Verify a Verifier-signed EAT Attestation Result and its binding to a receipt — offline.
|
|
100
|
+
|
|
101
|
+
Checks, fail-closed: compact-JWS shape, header ``typ`` == ``eat+jwt`` and ``alg`` == EdDSA,
|
|
102
|
+
signature under ``verifier_pubkey`` (the RATS Verifier key — a supplied trust anchor),
|
|
103
|
+
``eat_nonce`` == ``expected_binding`` (from :func:`enclave_binding_for`), and — if given —
|
|
104
|
+
``eat_profile`` == ``expected_profile``. Freshness (``iat``/``exp``) is reported and only
|
|
105
|
+
judged when ``now`` is supplied.
|
|
106
|
+
|
|
107
|
+
Returns ``{ok, tier, profile, ueid, nonce_ok, fresh, iat, exp, detail}``. ``ok`` covers
|
|
108
|
+
signature + typ/alg + binding (+ profile if requested); ``tier`` is the Verifier's declared
|
|
109
|
+
trustworthiness tier, REPORTED verbatim — combining ``ok``/``tier``/``fresh`` into a trust
|
|
110
|
+
decision is the relying party's policy.
|
|
111
|
+
"""
|
|
112
|
+
result = {"ok": False, "tier": None, "profile": None, "ueid": None, "nonce_ok": False,
|
|
113
|
+
"fresh": None, "iat": None, "exp": None, "detail": ""}
|
|
114
|
+
if not isinstance(eat_jws, str) or eat_jws.count(".") != 2:
|
|
115
|
+
result["detail"] = "not a compact JWS"
|
|
116
|
+
return result
|
|
117
|
+
header_b64, payload_b64, sig_b64 = eat_jws.split(".")
|
|
118
|
+
try:
|
|
119
|
+
header = json.loads(_b64url_decode(header_b64))
|
|
120
|
+
claims = json.loads(_b64url_decode(payload_b64))
|
|
121
|
+
sig = _b64url_decode(sig_b64)
|
|
122
|
+
except (ValueError, TypeError, json.JSONDecodeError):
|
|
123
|
+
result["detail"] = "malformed EAT token"
|
|
124
|
+
return result
|
|
125
|
+
if not isinstance(header, dict) or not isinstance(claims, dict):
|
|
126
|
+
result["detail"] = "malformed EAT token"
|
|
127
|
+
return result
|
|
128
|
+
if header.get("typ") != EAT_TYP:
|
|
129
|
+
result["detail"] = f"EAT typ must be '{EAT_TYP}'"
|
|
130
|
+
return result
|
|
131
|
+
if header.get("alg") != "EdDSA":
|
|
132
|
+
result["detail"] = f"EAT alg {header.get('alg')!r} not supported (EdDSA only)"
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
signing_input = f"{header_b64}.{payload_b64}".encode("ascii")
|
|
136
|
+
try:
|
|
137
|
+
sig_ok = verify_ed25519(verifier_pubkey, sig, signing_input)
|
|
138
|
+
except ValueError:
|
|
139
|
+
sig_ok = False
|
|
140
|
+
if not sig_ok:
|
|
141
|
+
result["detail"] = "EAT signature invalid (verifier key mismatch?)"
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
result["tier"] = claims.get("tier")
|
|
145
|
+
result["profile"] = claims.get("eat_profile")
|
|
146
|
+
result["ueid"] = claims.get("ueid")
|
|
147
|
+
iat, exp = claims.get("iat"), claims.get("exp")
|
|
148
|
+
result["iat"], result["exp"] = iat, exp
|
|
149
|
+
|
|
150
|
+
if not _match_nonce(claims.get("eat_nonce"), expected_binding):
|
|
151
|
+
result["detail"] = "eat_nonce does not bind this receipt (attestation is for other bytes)"
|
|
152
|
+
return result
|
|
153
|
+
result["nonce_ok"] = True
|
|
154
|
+
|
|
155
|
+
if expected_profile is not None and claims.get("eat_profile") != expected_profile:
|
|
156
|
+
result["detail"] = "eat_profile does not match the expected profile"
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
for _name, _val in (("iat", iat), ("exp", exp)):
|
|
160
|
+
if _val is not None and (isinstance(_val, bool) or not isinstance(_val, int)):
|
|
161
|
+
result["detail"] = f"EAT {_name} must be an integer when present"
|
|
162
|
+
return result
|
|
163
|
+
if now is not None:
|
|
164
|
+
if exp is None:
|
|
165
|
+
result["fresh"] = None # unbounded — cannot judge (relying-party policy)
|
|
166
|
+
else:
|
|
167
|
+
result["fresh"] = (iat is None or iat <= now) and now < exp
|
|
168
|
+
|
|
169
|
+
result["ok"] = True
|
|
170
|
+
result["detail"] = f"enclave attestation verified (tier={result['tier']!r})"
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def issue_enclave_attestation(binding: str, signer, *, profile: str, tier: str,
|
|
175
|
+
ueid: Optional[str] = None, iat: Optional[int] = None,
|
|
176
|
+
exp: Optional[int] = None) -> str:
|
|
177
|
+
"""Issue a signed EAT Attestation Result (for tests / a Verifier reference / demos).
|
|
178
|
+
|
|
179
|
+
``signer`` is the Verifier's Ed25519 key; ``binding`` is :func:`enclave_binding_for` of the
|
|
180
|
+
receipt (the value the enclave placed in its quote user-data). In production a real Verifier
|
|
181
|
+
emits this after appraising the raw TDX/GPU evidence — this helper does NOT appraise evidence,
|
|
182
|
+
it only frames + signs the result, so tests and reference verifiers have a token to check.
|
|
183
|
+
"""
|
|
184
|
+
# explizite Annotation (analog statuslist.py): die Init-Werte sind str, aber iat/exp weiter unten
|
|
185
|
+
# sind int — ohne Annotation inferiert mypy dict[str, str] und lehnt die int-Zuweisungen ab (CI-mypy,
|
|
186
|
+
# kein Runtime-Bug).
|
|
187
|
+
claims: dict[str, str | int] = {"eat_nonce": binding, "eat_profile": profile, "tier": tier}
|
|
188
|
+
if ueid is not None:
|
|
189
|
+
claims["ueid"] = ueid
|
|
190
|
+
if iat is not None:
|
|
191
|
+
claims["iat"] = iat
|
|
192
|
+
if exp is not None:
|
|
193
|
+
claims["exp"] = exp
|
|
194
|
+
header = {"alg": "EdDSA", "typ": EAT_TYP}
|
|
195
|
+
signing_input = _b64url(json.dumps(header).encode()) + "." + _b64url(json.dumps(claims).encode())
|
|
196
|
+
return signing_input + "." + _b64url(signer.sign(signing_input.encode("ascii")))
|
|
@@ -70,7 +70,8 @@ def _status_at(bit_array: bytes, bits: int, idx: int) -> int:
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def verify_status_snapshot(status_list_token: str, *, expected_uri: str, index: int,
|
|
73
|
-
issuer_pubkey: bytes, now: Optional[int] = None
|
|
73
|
+
issuer_pubkey: bytes, now: Optional[int] = None,
|
|
74
|
+
receipt_issuer_pubkey: Optional[bytes] = None) -> dict:
|
|
74
75
|
"""Verify a Status List Token snapshot and read one token's status, fully offline.
|
|
75
76
|
|
|
76
77
|
Checks, fail-closed: compact-JWS shape, `typ` == ``statuslist+jwt``, EdDSA signature under
|
|
@@ -78,11 +79,37 @@ def verify_status_snapshot(status_list_token: str, *, expected_uri: str, index:
|
|
|
78
79
|
`bits` ∈ {1,2,4,8}, zlib decode, index in range. Freshness: `iat`/`exp`/`ttl` are returned;
|
|
79
80
|
`fresh` is None unless ``now`` (POSIX seconds) is supplied, then it is
|
|
80
81
|
``iat <= now`` AND ``now < exp`` (if exp) AND ``now <= iat + ttl`` (if ttl).
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
|
|
83
|
+
**Trust-anchor separation (v1.9.1, external review #8/#12):** a status list signed by the
|
|
84
|
+
SAME key that signed the receipt carries no *independent* revocation assurance — the issuer
|
|
85
|
+
simply attests its own "still valid" state, and can flip it at will. Pass
|
|
86
|
+
``receipt_issuer_pubkey`` (the bundle's signing key) and the result reports
|
|
87
|
+
``self_issued=True`` when the status issuer key equals it. This is REPORTED, not fatal — the
|
|
88
|
+
relying party decides whether self-issued revocation is acceptable for its threat model (it
|
|
89
|
+
often is not; a distinct, independently-operated status authority is the stronger anchor).
|
|
90
|
+
|
|
91
|
+
Returns ``{ok, status, status_label, fresh, self_issued, iat, exp, ttl, detail}`` — ``ok``
|
|
92
|
+
covers signature + structure + lookup; combining ``ok`` with ``fresh``/``self_issued`` is the
|
|
93
|
+
caller's policy.
|
|
83
94
|
"""
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
# explizite Annotation: der Result-Dict traegt bool (ok/self_issued), str (status_label/detail), int
|
|
96
|
+
# (status/iat/exp/ttl) und None gemischt — ohne Annotation inferiert mypy nur 'str|bool|None' aus den
|
|
97
|
+
# Init-Werten und lehnt die spaeteren int-Zuweisungen ab (CI-mypy-Fehler, kein Runtime-Bug).
|
|
98
|
+
result: dict[str, str | bool | int | None] = {
|
|
99
|
+
"ok": False, "status": None, "status_label": None, "fresh": None,
|
|
100
|
+
"self_issued": None, "iat": None, "exp": None, "ttl": None, "detail": ""}
|
|
101
|
+
if receipt_issuer_pubkey is not None:
|
|
102
|
+
# hmac.compare_digest for a constant-time compare of the two public keys (defensive; the
|
|
103
|
+
# values are public, but consistent with the codebase's compare discipline).
|
|
104
|
+
import hmac as _hmac # noqa: PLC0415
|
|
105
|
+
# SYMMETRISCHER Typ-Guard: beide MUESSEN bytes/bytearray sein, sonst crasht bytes(str) mit TypeError
|
|
106
|
+
# statt fail-closed (verify_status_snapshot deklariert 'never crashes'). Non-bytes receipt_issuer_pubkey
|
|
107
|
+
# (str/int/list) → self_issued bleibt False (kein Crash, kein Fake-True).
|
|
108
|
+
result["self_issued"] = (isinstance(issuer_pubkey, (bytes, bytearray))
|
|
109
|
+
and isinstance(receipt_issuer_pubkey, (bytes, bytearray))
|
|
110
|
+
and len(issuer_pubkey) == len(receipt_issuer_pubkey)
|
|
111
|
+
and _hmac.compare_digest(bytes(issuer_pubkey),
|
|
112
|
+
bytes(receipt_issuer_pubkey)))
|
|
86
113
|
if status_list_token.count(".") != 2:
|
|
87
114
|
result["detail"] = "not a compact JWS"
|
|
88
115
|
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofbundle
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0b1
|
|
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
|
|
@@ -28,6 +28,7 @@ Provides-Extra: sdjwt
|
|
|
28
28
|
Provides-Extra: eval
|
|
29
29
|
Requires-Dist: rfc8785>=0.1.4; extra == "eval"
|
|
30
30
|
Provides-Extra: adapters
|
|
31
|
+
Provides-Extra: experimental
|
|
31
32
|
Provides-Extra: pq
|
|
32
33
|
Requires-Dist: cryptography>=48; extra == "pq"
|
|
33
34
|
Provides-Extra: pytest
|
|
@@ -145,6 +146,7 @@ flowchart LR
|
|
|
145
146
|
| For… | Read |
|
|
146
147
|
|---|---|
|
|
147
148
|
| Skeptics (why not SHA-256 / Sigstore / trust the issuer) | [docs/FAQ.md](docs/FAQ.md) |
|
|
149
|
+
| New to this? plain-terms glossary | [docs/GLOSSARY.md](docs/GLOSSARY.md) |
|
|
148
150
|
| Reviewers (30-minute adversarial audit path) | [docs/REVIEWERS.md](docs/REVIEWERS.md) |
|
|
149
151
|
| Where every trust anchor comes from | [docs/TRUST_ANCHORS.md](docs/TRUST_ANCHORS.md) |
|
|
150
152
|
| The demos, tier by tier | [docs/DEMO.md](docs/DEMO.md) |
|
|
@@ -152,13 +154,13 @@ flowchart LR
|
|
|
152
154
|
| Honest comparison to Rekor / in-toto / OMS / ValiChord | [INTEROP.md](INTEROP.md) |
|
|
153
155
|
| Regulatory mapping (and what to never claim) | [COMPLIANCE.md](COMPLIANCE.md) |
|
|
154
156
|
| Funders / role fit | [docs/PROJECT_BRIEF.md](docs/PROJECT_BRIEF.md) |
|
|
157
|
+
| **Preview:** TEE-attestation bridge (v2.0 beta) | [docs/EXPERIMENTAL_ENCLAVE.md](docs/EXPERIMENTAL_ENCLAVE.md) |
|
|
155
158
|
|
|
156
159
|
## Install
|
|
157
160
|
|
|
158
161
|
```bash
|
|
159
162
|
pip install proofbundle # core: offline verify + plain emit (dependency-free)
|
|
160
|
-
pip install "proofbundle[eval]" # + eval receipts, prereg, and the demo (adds
|
|
161
|
-
pip install "proofbundle[eval]" # emit eval receipts (adds an RFC 8785 canonicalizer)
|
|
163
|
+
pip install "proofbundle[eval]" # + eval receipts, prereg, and the demo (adds an RFC 8785 JCS canonicalizer)
|
|
162
164
|
pip install "proofbundle[inspect]" # inspect_ai adapter + hook
|
|
163
165
|
pip install "proofbundle[pq]" # verify ML-DSA-44 (post-quantum) witness cosignatures
|
|
164
166
|
```
|
|
@@ -168,7 +170,7 @@ Requires Python 3.10+. The verify path never rolls its own crypto — Ed25519 co
|
|
|
168
170
|
|
|
169
171
|
## Status & scope
|
|
170
172
|
|
|
171
|
-
Beta, SemVer-committed,
|
|
173
|
+
Beta, SemVer-committed, 303 tests + a CI mutation gate + property-based parser fuzzing. Correctness
|
|
172
174
|
is anchored to external RFC 6962 vectors and a real Rekor proof, not just its own bundles. It is
|
|
173
175
|
**not** a log service, a full in-toto client, a TEE, a consensus network, or a compliance product
|
|
174
176
|
by itself — it is the small, offline, standards-native receipt layer between them. Security policy:
|
|
@@ -41,6 +41,8 @@ src/proofbundle/adapters/inspect_ai.py
|
|
|
41
41
|
src/proofbundle/adapters/lm_eval.py
|
|
42
42
|
src/proofbundle/adapters/promptfoo.py
|
|
43
43
|
src/proofbundle/adapters/samples.py
|
|
44
|
+
src/proofbundle/experimental/__init__.py
|
|
45
|
+
src/proofbundle/experimental/enclave.py
|
|
44
46
|
tests/test_adapters.py
|
|
45
47
|
tests/test_adversarial.py
|
|
46
48
|
tests/test_beacon.py
|
|
@@ -57,6 +59,7 @@ tests/test_emit.py
|
|
|
57
59
|
tests/test_eval_claim_schema.py
|
|
58
60
|
tests/test_evalclaim.py
|
|
59
61
|
tests/test_examples.py
|
|
62
|
+
tests/test_experimental_enclave.py
|
|
60
63
|
tests/test_fuzz_parsers.py
|
|
61
64
|
tests/test_hf_evals.py
|
|
62
65
|
tests/test_inspect_hook.py
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""TEE-attestation bridge (v2.0 preview) — binding, verify roundtrip, adversarial red matrix.
|
|
2
|
+
|
|
3
|
+
Also pins the experimental gating: importing proofbundle.experimental warns, and enclave is NOT
|
|
4
|
+
on the top-level package surface.
|
|
5
|
+
"""
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
import unittest
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
|
12
|
+
|
|
13
|
+
from proofbundle import emit_bundle, generate_signer
|
|
14
|
+
from proofbundle.errors import BundleFormatError
|
|
15
|
+
from proofbundle.experimental.enclave import (EAT_TYP, enclave_binding_for,
|
|
16
|
+
issue_enclave_attestation,
|
|
17
|
+
verify_enclave_attestation)
|
|
18
|
+
|
|
19
|
+
PROFILE = "https://b7n0de.com/proofbundle/eat-profile/tdx-gpu/v1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _raw(k):
|
|
23
|
+
return k.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _b64url(b):
|
|
27
|
+
return base64.urlsafe_b64encode(b).rstrip(b"=").decode("ascii")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _b64url_decode(s):
|
|
31
|
+
return base64.urlsafe_b64decode(s.encode("ascii") + b"=" * (-len(s) % 4))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _setup():
|
|
35
|
+
"""A receipt + a Verifier who attests the enclave that produced it, bound to the receipt."""
|
|
36
|
+
issuer = generate_signer()
|
|
37
|
+
bundle = emit_bundle(b'{"suite": "safety", "passed": true}', issuer)
|
|
38
|
+
binding = enclave_binding_for(bundle)
|
|
39
|
+
verifier = generate_signer()
|
|
40
|
+
eat = issue_enclave_attestation(binding, verifier, profile=PROFILE, tier="affirming",
|
|
41
|
+
ueid="tdx:0x1234", iat=1_780_000_000, exp=1_780_003_600)
|
|
42
|
+
return bundle, binding, verifier, eat
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestBinding(unittest.TestCase):
|
|
46
|
+
def test_binding_is_stable_and_receipt_specific(self):
|
|
47
|
+
b1 = emit_bundle(b"payload-A", generate_signer())
|
|
48
|
+
b2 = emit_bundle(b"payload-B", generate_signer())
|
|
49
|
+
self.assertEqual(enclave_binding_for(b1), enclave_binding_for(b1)) # deterministic
|
|
50
|
+
self.assertNotEqual(enclave_binding_for(b1), enclave_binding_for(b2)) # bytes-specific
|
|
51
|
+
# fits the RFC 9711 JSON eat_nonce window (8..88 chars): sha256 b64url = 43 chars
|
|
52
|
+
self.assertEqual(len(enclave_binding_for(b1)), 43)
|
|
53
|
+
|
|
54
|
+
def test_binding_rejects_non_bundle(self):
|
|
55
|
+
with self.assertRaises(BundleFormatError):
|
|
56
|
+
enclave_binding_for({"no": "payload"})
|
|
57
|
+
with self.assertRaises(BundleFormatError):
|
|
58
|
+
enclave_binding_for({"payload_b64": "!!not-base64!!"})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestVerifyRoundtrip(unittest.TestCase):
|
|
62
|
+
def test_green(self):
|
|
63
|
+
bundle, binding, verifier, eat = _setup()
|
|
64
|
+
res = verify_enclave_attestation(eat, verifier_pubkey=_raw(verifier),
|
|
65
|
+
expected_binding=binding, expected_profile=PROFILE)
|
|
66
|
+
self.assertTrue(res["ok"], res["detail"])
|
|
67
|
+
self.assertEqual(res["tier"], "affirming")
|
|
68
|
+
self.assertEqual(res["profile"], PROFILE)
|
|
69
|
+
self.assertEqual(res["ueid"], "tdx:0x1234")
|
|
70
|
+
self.assertTrue(res["nonce_ok"])
|
|
71
|
+
|
|
72
|
+
def test_freshness_reported_not_assumed(self):
|
|
73
|
+
bundle, binding, verifier, eat = _setup()
|
|
74
|
+
r = verify_enclave_attestation(eat, verifier_pubkey=_raw(verifier), expected_binding=binding)
|
|
75
|
+
self.assertIsNone(r["fresh"]) # no clock → no judgement
|
|
76
|
+
self.assertTrue(verify_enclave_attestation(eat, verifier_pubkey=_raw(verifier),
|
|
77
|
+
expected_binding=binding, now=1_780_000_060)["fresh"])
|
|
78
|
+
self.assertFalse(verify_enclave_attestation(eat, verifier_pubkey=_raw(verifier),
|
|
79
|
+
expected_binding=binding, now=1_780_010_000)["fresh"]) # past exp
|
|
80
|
+
|
|
81
|
+
def test_unbounded_fresh_is_none(self):
|
|
82
|
+
bundle, binding, verifier, eat = _setup()
|
|
83
|
+
v = generate_signer()
|
|
84
|
+
eat2 = issue_enclave_attestation(binding, v, profile=PROFILE, tier="affirming") # no exp
|
|
85
|
+
r = verify_enclave_attestation(eat2, verifier_pubkey=_raw(v), expected_binding=binding,
|
|
86
|
+
now=10**12)
|
|
87
|
+
self.assertTrue(r["ok"])
|
|
88
|
+
self.assertIsNone(r["fresh"]) # unbounded → cannot judge
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestAdversarial(unittest.TestCase):
|
|
92
|
+
def test_red_wrong_verifier_key(self):
|
|
93
|
+
bundle, binding, verifier, eat = _setup()
|
|
94
|
+
res = verify_enclave_attestation(eat, verifier_pubkey=_raw(generate_signer()),
|
|
95
|
+
expected_binding=binding)
|
|
96
|
+
self.assertFalse(res["ok"])
|
|
97
|
+
self.assertIn("signature", res["detail"])
|
|
98
|
+
|
|
99
|
+
def test_red_binding_mismatch_other_receipt(self):
|
|
100
|
+
# An attestation for a DIFFERENT receipt must not verify against this one — the core guard.
|
|
101
|
+
bundle, binding, verifier, eat = _setup()
|
|
102
|
+
other = emit_bundle(b'{"forged": true}', generate_signer())
|
|
103
|
+
res = verify_enclave_attestation(eat, verifier_pubkey=_raw(verifier),
|
|
104
|
+
expected_binding=enclave_binding_for(other))
|
|
105
|
+
self.assertFalse(res["ok"])
|
|
106
|
+
self.assertIn("does not bind", res["detail"])
|
|
107
|
+
|
|
108
|
+
def test_red_wrong_typ(self):
|
|
109
|
+
bundle, binding, verifier, eat = _setup()
|
|
110
|
+
h, p, s = eat.split(".")
|
|
111
|
+
header = json.loads(_b64url_decode(h))
|
|
112
|
+
header["typ"] = "jwt"
|
|
113
|
+
h2 = _b64url(json.dumps(header).encode())
|
|
114
|
+
sig2 = _b64url(verifier.sign(f"{h2}.{p}".encode("ascii")))
|
|
115
|
+
res = verify_enclave_attestation(f"{h2}.{p}.{sig2}", verifier_pubkey=_raw(verifier),
|
|
116
|
+
expected_binding=binding)
|
|
117
|
+
self.assertFalse(res["ok"])
|
|
118
|
+
self.assertIn(EAT_TYP, res["detail"])
|
|
119
|
+
|
|
120
|
+
def test_red_alg_none(self):
|
|
121
|
+
bundle, binding, verifier, eat = _setup()
|
|
122
|
+
h, p, s = eat.split(".")
|
|
123
|
+
h2 = _b64url(json.dumps({"alg": "none", "typ": EAT_TYP}).encode())
|
|
124
|
+
res = verify_enclave_attestation(f"{h2}.{p}.{s}", verifier_pubkey=_raw(verifier),
|
|
125
|
+
expected_binding=binding)
|
|
126
|
+
self.assertFalse(res["ok"])
|
|
127
|
+
|
|
128
|
+
def test_red_profile_mismatch(self):
|
|
129
|
+
bundle, binding, verifier, eat = _setup()
|
|
130
|
+
res = verify_enclave_attestation(eat, verifier_pubkey=_raw(verifier),
|
|
131
|
+
expected_binding=binding,
|
|
132
|
+
expected_profile="https://evil.example/profile")
|
|
133
|
+
self.assertFalse(res["ok"])
|
|
134
|
+
self.assertIn("profile", res["detail"])
|
|
135
|
+
|
|
136
|
+
def test_red_claim_tamper_breaks_signature(self):
|
|
137
|
+
# Flip the tier in the payload without re-signing → signature fails.
|
|
138
|
+
bundle, binding, verifier, eat = _setup()
|
|
139
|
+
h, p, s = eat.split(".")
|
|
140
|
+
claims = json.loads(_b64url_decode(p))
|
|
141
|
+
claims["tier"] = "contraindicated->affirming"
|
|
142
|
+
p2 = _b64url(json.dumps(claims).encode())
|
|
143
|
+
res = verify_enclave_attestation(f"{h}.{p2}.{s}", verifier_pubkey=_raw(verifier),
|
|
144
|
+
expected_binding=binding)
|
|
145
|
+
self.assertFalse(res["ok"])
|
|
146
|
+
|
|
147
|
+
def test_red_garbage(self):
|
|
148
|
+
for bad in ("", "not.a.jws", "a.b", "x.y.z"):
|
|
149
|
+
res = verify_enclave_attestation(bad, verifier_pubkey=b"\x00" * 32, expected_binding="x")
|
|
150
|
+
self.assertFalse(res["ok"])
|
|
151
|
+
|
|
152
|
+
def test_red_string_exp_rejected(self):
|
|
153
|
+
bundle, binding, verifier, eat = _setup()
|
|
154
|
+
v = generate_signer()
|
|
155
|
+
h = _b64url(json.dumps({"alg": "EdDSA", "typ": EAT_TYP}).encode())
|
|
156
|
+
claims = {"eat_nonce": binding, "eat_profile": PROFILE, "tier": "affirming", "exp": "9999"}
|
|
157
|
+
p = _b64url(json.dumps(claims).encode())
|
|
158
|
+
sig = _b64url(v.sign(f"{h}.{p}".encode("ascii")))
|
|
159
|
+
res = verify_enclave_attestation(f"{h}.{p}.{sig}", verifier_pubkey=_raw(v),
|
|
160
|
+
expected_binding=binding, now=1)
|
|
161
|
+
self.assertFalse(res["ok"])
|
|
162
|
+
self.assertIn("exp", res["detail"])
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestExperimentalGating(unittest.TestCase):
|
|
166
|
+
def test_not_on_top_level_surface(self):
|
|
167
|
+
import proofbundle
|
|
168
|
+
self.assertNotIn("enclave", getattr(proofbundle, "__all__", []))
|
|
169
|
+
self.assertFalse(hasattr(proofbundle, "verify_enclave_attestation"))
|
|
170
|
+
|
|
171
|
+
def test_import_warns(self):
|
|
172
|
+
import importlib
|
|
173
|
+
import proofbundle.experimental as exp
|
|
174
|
+
with warnings.catch_warnings(record=True) as w:
|
|
175
|
+
warnings.simplefilter("always")
|
|
176
|
+
importlib.reload(exp)
|
|
177
|
+
self.assertTrue(any(issubclass(x.category, exp.ExperimentalWarning) for x in w))
|
|
178
|
+
|
|
179
|
+
def test_cli_verify_enclave(self):
|
|
180
|
+
import contextlib
|
|
181
|
+
import io
|
|
182
|
+
import os
|
|
183
|
+
import tempfile
|
|
184
|
+
from proofbundle.cli import main
|
|
185
|
+
bundle, binding, verifier, eat = _setup()
|
|
186
|
+
d = tempfile.mkdtemp()
|
|
187
|
+
rp = os.path.join(d, "receipt.json"); json.dump(bundle, open(rp, "w"))
|
|
188
|
+
ep = os.path.join(d, "att.eat"); open(ep, "w").write(eat)
|
|
189
|
+
vkey = base64.b64encode(_raw(verifier)).decode()
|
|
190
|
+
with contextlib.redirect_stdout(io.StringIO()), warnings.catch_warnings():
|
|
191
|
+
warnings.simplefilter("ignore")
|
|
192
|
+
rc = main(["verify-enclave", ep, "--receipt", rp, "--verifier-key", vkey,
|
|
193
|
+
"--profile", PROFILE, "--json"])
|
|
194
|
+
self.assertEqual(rc, 0)
|
|
195
|
+
# wrong receipt → fail
|
|
196
|
+
other = os.path.join(d, "other.json")
|
|
197
|
+
json.dump(emit_bundle(b"other", generate_signer()), open(other, "w"))
|
|
198
|
+
with contextlib.redirect_stdout(io.StringIO()), warnings.catch_warnings():
|
|
199
|
+
warnings.simplefilter("ignore")
|
|
200
|
+
rc2 = main(["verify-enclave", ep, "--receipt", other, "--verifier-key", vkey])
|
|
201
|
+
self.assertEqual(rc2, 1)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
unittest.main()
|
|
@@ -24,6 +24,7 @@ from proofbundle.checkpoint import verify_checkpoint, verify_cosignature
|
|
|
24
24
|
from proofbundle.statuslist import verify_status_snapshot
|
|
25
25
|
from proofbundle.kbjwt import split_key_binding, verify_key_binding
|
|
26
26
|
from proofbundle.sdjwt import verify_sd_jwt
|
|
27
|
+
from proofbundle.experimental.enclave import verify_enclave_attestation
|
|
27
28
|
|
|
28
29
|
_ALLOWED = (ProofBundleError, ValueError) # the documented "malformed input" surface
|
|
29
30
|
|
|
@@ -77,12 +78,27 @@ if given is not None:
|
|
|
77
78
|
def test_verify_sd_jwt_never_crashes(self, compact):
|
|
78
79
|
_must_not_crash(verify_sd_jwt, compact)
|
|
79
80
|
|
|
81
|
+
@settings(max_examples=200, deadline=None)
|
|
82
|
+
@given(_texts)
|
|
83
|
+
def test_verify_enclave_attestation_never_crashes(self, eat):
|
|
84
|
+
_must_not_crash(verify_enclave_attestation, eat, verifier_pubkey=b"\x00" * 32,
|
|
85
|
+
expected_binding="x")
|
|
86
|
+
|
|
80
87
|
@settings(max_examples=200, deadline=None)
|
|
81
88
|
@given(_texts)
|
|
82
89
|
def test_verify_status_snapshot_never_crashes(self, token):
|
|
83
90
|
_must_not_crash(verify_status_snapshot, token, expected_uri="u", index=0,
|
|
84
91
|
issuer_pubkey=b"\x00" * 32)
|
|
85
92
|
|
|
93
|
+
@settings(max_examples=200, deadline=None)
|
|
94
|
+
@given(_texts, st.one_of(st.text(min_size=32, max_size=32), st.integers(),
|
|
95
|
+
st.lists(st.integers()), st.none(), st.binary(max_size=64)))
|
|
96
|
+
def test_verify_status_snapshot_never_crashes_on_malformed_receipt_key(self, token, rk):
|
|
97
|
+
# v1.9.1 self_issued: receipt_issuer_pubkey darf JEDEN Typ tragen ohne die 'never crashes'-Zusage
|
|
98
|
+
# zu brechen (der symmetrische Typ-Guard fängt non-bytes → self_issued=False statt TypeError).
|
|
99
|
+
_must_not_crash(verify_status_snapshot, token, expected_uri="u", index=0,
|
|
100
|
+
issuer_pubkey=b"\x00" * 32, receipt_issuer_pubkey=rk)
|
|
101
|
+
|
|
86
102
|
|
|
87
103
|
if __name__ == "__main__":
|
|
88
104
|
unittest.main()
|
|
@@ -177,3 +177,35 @@ class TestFreshnessAndStrictTypes(unittest.TestCase):
|
|
|
177
177
|
res = verify_status_snapshot(f"{h}.{p}.{sig}", expected_uri=URI, index=0, issuer_pubkey=self.pub)
|
|
178
178
|
self.assertFalse(res["ok"])
|
|
179
179
|
self.assertIn("exp", res["detail"])
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class TestSelfIssuedSeparation(unittest.TestCase):
|
|
183
|
+
"""v1.9.1 (#8/#12): a status list signed by the receipt key has no independent revocation
|
|
184
|
+
assurance — verify_status_snapshot reports self_issued so the relying party can refuse it."""
|
|
185
|
+
|
|
186
|
+
def setUp(self):
|
|
187
|
+
self.status_signer = generate_signer()
|
|
188
|
+
self.status_pub = _raw(self.status_signer)
|
|
189
|
+
self.token = issue_status_list_token([0, 1], uri=URI, signer=self.status_signer, iat=IAT)
|
|
190
|
+
|
|
191
|
+
def test_self_issued_none_when_not_asked(self):
|
|
192
|
+
res = verify_status_snapshot(self.token, expected_uri=URI, index=0,
|
|
193
|
+
issuer_pubkey=self.status_pub)
|
|
194
|
+
self.assertTrue(res["ok"])
|
|
195
|
+
self.assertIsNone(res["self_issued"]) # not requested → not judged
|
|
196
|
+
|
|
197
|
+
def test_self_issued_true_when_same_key(self):
|
|
198
|
+
res = verify_status_snapshot(self.token, expected_uri=URI, index=0,
|
|
199
|
+
issuer_pubkey=self.status_pub,
|
|
200
|
+
receipt_issuer_pubkey=self.status_pub)
|
|
201
|
+
self.assertTrue(res["ok"]) # still valid crypto
|
|
202
|
+
self.assertTrue(res["self_issued"]) # but flagged as self-issued
|
|
203
|
+
|
|
204
|
+
def test_self_issued_false_when_distinct_key(self):
|
|
205
|
+
receipt_pub = _raw(generate_signer()) # an independent receipt issuer
|
|
206
|
+
res = verify_status_snapshot(self.token, expected_uri=URI, index=1,
|
|
207
|
+
issuer_pubkey=self.status_pub,
|
|
208
|
+
receipt_issuer_pubkey=receipt_pub)
|
|
209
|
+
self.assertTrue(res["ok"])
|
|
210
|
+
self.assertFalse(res["self_issued"]) # distinct anchor → not self-issued
|
|
211
|
+
self.assertEqual(res["status_label"], "INVALID")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|