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.
Files changed (85) hide show
  1. {proofbundle-1.9.0/src/proofbundle.egg-info → proofbundle-2.0.0b1}/PKG-INFO +6 -4
  2. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/README.md +4 -3
  3. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/pyproject.toml +6 -1
  4. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/__init__.py +1 -1
  5. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/cli.py +39 -0
  6. proofbundle-2.0.0b1/src/proofbundle/experimental/__init__.py +28 -0
  7. proofbundle-2.0.0b1/src/proofbundle/experimental/enclave.py +196 -0
  8. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/statuslist.py +32 -5
  9. {proofbundle-1.9.0 → proofbundle-2.0.0b1/src/proofbundle.egg-info}/PKG-INFO +6 -4
  10. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/SOURCES.txt +3 -0
  11. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/requires.txt +2 -0
  12. proofbundle-2.0.0b1/tests/test_experimental_enclave.py +205 -0
  13. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_fuzz_parsers.py +16 -0
  14. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_statuslist.py +32 -0
  15. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/LICENSE +0 -0
  16. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/setup.cfg +0 -0
  17. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/_inspect_registry.py +0 -0
  18. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/_integration.py +0 -0
  19. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/__init__.py +0 -0
  20. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/_provenance.py +0 -0
  21. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/eee.py +0 -0
  22. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/inspect_ai.py +0 -0
  23. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/lm_eval.py +0 -0
  24. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/promptfoo.py +0 -0
  25. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/adapters/samples.py +0 -0
  26. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/beacon.py +0 -0
  27. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/bundle.py +0 -0
  28. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/checkpoint.py +0 -0
  29. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/demo.py +0 -0
  30. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/dsse.py +0 -0
  31. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/eee_eval_schema.json +0 -0
  32. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/emit.py +0 -0
  33. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/errors.py +0 -0
  34. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/evalclaim.py +0 -0
  35. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/hf_evals.py +0 -0
  36. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/inspect_hook.py +0 -0
  37. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/intoto.py +0 -0
  38. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/kbjwt.py +0 -0
  39. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/merkle.py +0 -0
  40. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/persample.py +0 -0
  41. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/prereg.py +0 -0
  42. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/py.typed +0 -0
  43. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/pytest_plugin.py +0 -0
  44. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/sdjwt.py +0 -0
  45. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/sdjwt_issue.py +0 -0
  46. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/signature.py +0 -0
  47. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle/tlogproof.py +0 -0
  48. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/dependency_links.txt +0 -0
  49. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/entry_points.txt +0 -0
  50. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/top_level.txt +0 -0
  51. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_adapters.py +0 -0
  52. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_adversarial.py +0 -0
  53. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_beacon.py +0 -0
  54. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_bundle.py +0 -0
  55. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_bundle_robustness.py +0 -0
  56. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_checkpoint.py +0 -0
  57. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_cli.py +0 -0
  58. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_cli_eval.py +0 -0
  59. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_cosignature.py +0 -0
  60. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_cosignature_mldsa.py +0 -0
  61. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_demo.py +0 -0
  62. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_eee.py +0 -0
  63. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_emit.py +0 -0
  64. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_eval_claim_schema.py +0 -0
  65. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_evalclaim.py +0 -0
  66. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_examples.py +0 -0
  67. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_hf_evals.py +0 -0
  68. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_inspect_hook.py +0 -0
  69. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_intoto.py +0 -0
  70. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_intoto_dsse.py +0 -0
  71. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_kbjwt.py +0 -0
  72. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_merkle.py +0 -0
  73. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_merkle_property.py +0 -0
  74. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_persample.py +0 -0
  75. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_prereg.py +0 -0
  76. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_promptfoo.py +0 -0
  77. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_provenance.py +0 -0
  78. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_pytest_plugin.py +0 -0
  79. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_rekor_interop.py +0 -0
  80. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_rfc6962_external_vectors.py +0 -0
  81. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_schema.py +0 -0
  82. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_sdjwt_issue.py +0 -0
  83. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_sdjwt_reference.py +0 -0
  84. {proofbundle-1.9.0 → proofbundle-2.0.0b1}/tests/test_signature.py +0 -0
  85. {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: 1.9.0
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 rfc8785 JCS)
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, 299 tests + a CI mutation gate + property-based parser fuzzing. Correctness
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 rfc8785 JCS)
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, 299 tests + a CI mutation gate + property-based parser fuzzing. Correctness
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 = "1.9.0"
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.
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  from typing import TYPE_CHECKING
15
15
 
16
- __version__ = "1.9.0"
16
+ __version__ = "2.0.0b1"
17
17
 
18
18
  __all__ = [
19
19
  "__version__",
@@ -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) -> dict:
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
- Returns ``{ok, status, status_label, fresh, iat, exp, ttl, detail}`` — ``ok`` covers
82
- signature + structure + lookup; combining ``ok`` with ``fresh`` is the caller's policy.
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
- result = {"ok": False, "status": None, "status_label": None, "fresh": None,
85
- "iat": None, "exp": None, "ttl": None, "detail": ""}
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: 1.9.0
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 rfc8785 JCS)
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, 299 tests + a CI mutation gate + property-based parser fuzzing. Correctness
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
@@ -16,6 +16,8 @@ inspect_ai<0.4,>=0.3.112
16
16
  [eval]
17
17
  rfc8785>=0.1.4
18
18
 
19
+ [experimental]
20
+
19
21
  [inspect]
20
22
  inspect_ai<0.4,>=0.3.112
21
23
 
@@ -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