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.
- {proofbundle-1.9.1/src/proofbundle.egg-info → proofbundle-2.0.0b1}/PKG-INFO +3 -1
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/README.md +1 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/pyproject.toml +6 -1
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/__init__.py +1 -1
- {proofbundle-1.9.1 → 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.1 → proofbundle-2.0.0b1}/src/proofbundle/statuslist.py +6 -2
- {proofbundle-1.9.1 → proofbundle-2.0.0b1/src/proofbundle.egg-info}/PKG-INFO +3 -1
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/SOURCES.txt +3 -0
- {proofbundle-1.9.1 → 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.1 → proofbundle-2.0.0b1}/tests/test_fuzz_parsers.py +7 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/LICENSE +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/setup.cfg +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/_integration.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/__init__.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/_provenance.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/eee.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/promptfoo.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/adapters/samples.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/beacon.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/bundle.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/demo.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/dsse.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/emit.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/errors.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/evalclaim.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/hf_evals.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/intoto.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/merkle.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/persample.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/prereg.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/py.typed +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/sdjwt.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/signature.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_adapters.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_adversarial.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_beacon.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_bundle.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_checkpoint.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_cli.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_cli_eval.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_cosignature.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_demo.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_eee.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_emit.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_evalclaim.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_examples.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_hf_evals.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_inspect_hook.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_intoto.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_kbjwt.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_merkle.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_merkle_property.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_persample.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_prereg.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_promptfoo.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_provenance.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_rekor_interop.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_schema.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_signature.py +0 -0
- {proofbundle-1.9.1 → proofbundle-2.0.0b1}/tests/test_statuslist.py +0 -0
- {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:
|
|
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 = "
|
|
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")))
|
|
@@ -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
|
-
|
|
96
|
-
|
|
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:
|
|
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
|
|
@@ -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
|
|
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
|