proofbundle 1.9.1__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.1/src/proofbundle.egg-info → proofbundle-2.0.0b1}/PKG-INFO +3 -1
  2. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/README.md +1 -0
  3. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/pyproject.toml +6 -1
  4. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/__init__.py +1 -1
  5. {proofbundle-1.9.1 → 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.1 → proofbundle-2.0.0b1}/src/proofbundle/statuslist.py +6 -2
  9. {proofbundle-1.9.1 → proofbundle-2.0.0b1/src/proofbundle.egg-info}/PKG-INFO +3 -1
  10. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/SOURCES.txt +3 -0
  11. {proofbundle-1.9.1 → 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.1 → proofbundle-2.0.0b1}/tests/test_fuzz_parsers.py +7 -0
  14. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/LICENSE +0 -0
  15. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/setup.cfg +0 -0
  16. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/_inspect_registry.py +0 -0
  17. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/_integration.py +0 -0
  18. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/__init__.py +0 -0
  19. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/_provenance.py +0 -0
  20. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/eee.py +0 -0
  21. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/inspect_ai.py +0 -0
  22. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/lm_eval.py +0 -0
  23. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/promptfoo.py +0 -0
  24. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/samples.py +0 -0
  25. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/beacon.py +0 -0
  26. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/bundle.py +0 -0
  27. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/checkpoint.py +0 -0
  28. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/demo.py +0 -0
  29. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/dsse.py +0 -0
  30. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/eee_eval_schema.json +0 -0
  31. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/emit.py +0 -0
  32. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/errors.py +0 -0
  33. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/evalclaim.py +0 -0
  34. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/hf_evals.py +0 -0
  35. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/inspect_hook.py +0 -0
  36. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/intoto.py +0 -0
  37. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/kbjwt.py +0 -0
  38. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/merkle.py +0 -0
  39. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/persample.py +0 -0
  40. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/prereg.py +0 -0
  41. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/py.typed +0 -0
  42. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/pytest_plugin.py +0 -0
  43. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/sdjwt.py +0 -0
  44. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/sdjwt_issue.py +0 -0
  45. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/signature.py +0 -0
  46. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/tlogproof.py +0 -0
  47. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/dependency_links.txt +0 -0
  48. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/entry_points.txt +0 -0
  49. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/top_level.txt +0 -0
  50. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_adapters.py +0 -0
  51. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_adversarial.py +0 -0
  52. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_beacon.py +0 -0
  53. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_bundle.py +0 -0
  54. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_bundle_robustness.py +0 -0
  55. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_checkpoint.py +0 -0
  56. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_cli.py +0 -0
  57. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_cli_eval.py +0 -0
  58. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_cosignature.py +0 -0
  59. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_cosignature_mldsa.py +0 -0
  60. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_demo.py +0 -0
  61. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_eee.py +0 -0
  62. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_emit.py +0 -0
  63. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_eval_claim_schema.py +0 -0
  64. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_evalclaim.py +0 -0
  65. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_examples.py +0 -0
  66. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_hf_evals.py +0 -0
  67. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_inspect_hook.py +0 -0
  68. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_intoto.py +0 -0
  69. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_intoto_dsse.py +0 -0
  70. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_kbjwt.py +0 -0
  71. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_merkle.py +0 -0
  72. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_merkle_property.py +0 -0
  73. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_persample.py +0 -0
  74. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_prereg.py +0 -0
  75. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_promptfoo.py +0 -0
  76. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_provenance.py +0 -0
  77. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_pytest_plugin.py +0 -0
  78. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_rekor_interop.py +0 -0
  79. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_rfc6962_external_vectors.py +0 -0
  80. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_schema.py +0 -0
  81. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_sdjwt_issue.py +0 -0
  82. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_sdjwt_reference.py +0 -0
  83. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_signature.py +0 -0
  84. {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_statuslist.py +0 -0
  85. {proofbundle-1.9.1 → 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.1
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
@@ -153,6 +154,7 @@ flowchart LR
153
154
  | Honest comparison to Rekor / in-toto / OMS / ValiChord | [INTEROP.md](INTEROP.md) |
154
155
  | Regulatory mapping (and what to never claim) | [COMPLIANCE.md](COMPLIANCE.md) |
155
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) |
156
158
 
157
159
  ## Install
158
160
 
@@ -105,6 +105,7 @@ flowchart LR
105
105
  | Honest comparison to Rekor / in-toto / OMS / ValiChord | [INTEROP.md](INTEROP.md) |
106
106
  | Regulatory mapping (and what to never claim) | [COMPLIANCE.md](COMPLIANCE.md) |
107
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) |
108
109
 
109
110
  ## Install
110
111
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proofbundle"
7
- version = "1.9.1"
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.1"
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")))
@@ -92,8 +92,12 @@ def verify_status_snapshot(status_list_token: str, *, expected_uri: str, index:
92
92
  covers signature + structure + lookup; combining ``ok`` with ``fresh``/``self_issued`` is the
93
93
  caller's policy.
94
94
  """
95
- result = {"ok": False, "status": None, "status_label": None, "fresh": None,
96
- "self_issued": None, "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": ""}
97
101
  if receipt_issuer_pubkey is not None:
98
102
  # hmac.compare_digest for a constant-time compare of the two public keys (defensive; the
99
103
  # values are public, but consistent with the codebase's compare discipline).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proofbundle
3
- Version: 1.9.1
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
@@ -153,6 +154,7 @@ flowchart LR
153
154
  | Honest comparison to Rekor / in-toto / OMS / ValiChord | [INTEROP.md](INTEROP.md) |
154
155
  | Regulatory mapping (and what to never claim) | [COMPLIANCE.md](COMPLIANCE.md) |
155
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) |
156
158
 
157
159
  ## Install
158
160
 
@@ -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,6 +78,12 @@ 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):
File without changes
File without changes