voltage-verify 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """voltage-verify: bind two hardware attestations to the workload you meant to run, and check them."""
3
+
4
+ __version__ = "0.1.0"
@@ -0,0 +1,9 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Allow ``python -m voltage_verify ...``, which is what the VM instructions use under sudo."""
3
+
4
+ import sys
5
+
6
+ from .cli import main
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
@@ -0,0 +1,184 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """The part that runs inside the attested VM, as root.
3
+
4
+ It reads the manifest the verifier prepared, derives the two commitments, asks the Intel TDX
5
+ guest driver for a quote over ``commitment512`` (through the kernel's configfs TSM
6
+ interface), asks NVIDIA's attestation service for GPU claims over ``commitment256`` (through
7
+ the nv-attestation-sdk), fetches the public collateral needed for offline verification, and
8
+ writes one bundle. It chooses nothing: every hash comes from the manifest.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import errno
14
+ import json
15
+ import platform
16
+ import secrets
17
+ import shutil
18
+ import subprocess
19
+ import time
20
+ import urllib.request
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from . import pcs, tdx
26
+ from .bundle import Bundle
27
+ from .canonical import commitments
28
+ from .manifest import validate_manifest
29
+
30
+ TSM_REPORT_DIR = Path("/sys/kernel/config/tsm/report")
31
+ NRAS_GPU_URL = "https://nras.attestation.nvidia.com/v3/attest/gpu"
32
+ NRAS_OCSP_URL = "https://ocsp.ndis.nvidia.com/"
33
+ NRAS_RIM_URL = "https://rim.attestation.nvidia.com/v1/rim/"
34
+ NRAS_JWKS_URL = "https://nras.attestation.nvidia.com/.well-known/jwks.json"
35
+
36
+
37
+ class AttestError(RuntimeError):
38
+ """Raised when evidence cannot be produced on this machine."""
39
+
40
+
41
+ def tsm_quote(report_data: bytes, attempts: int = 5) -> bytes:
42
+ """Request a TDX quote through configfs TSM. Retries only on EINVAL (concurrent request)."""
43
+ if len(report_data) != 64:
44
+ raise AttestError("report_data must be exactly 64 bytes")
45
+ if not TSM_REPORT_DIR.is_dir():
46
+ raise AttestError(f"{TSM_REPORT_DIR} is missing: not a TDX guest, or configfs TSM not mounted (run as root)")
47
+ for attempt in range(attempts):
48
+ entry = TSM_REPORT_DIR / f"voltage-verify-{secrets.token_hex(4)}"
49
+ try:
50
+ entry.mkdir()
51
+ (entry / "inblob").write_bytes(report_data)
52
+ quote = (entry / "outblob").read_bytes()
53
+ provider = (entry / "provider").read_text().strip() if (entry / "provider").exists() else ""
54
+ if provider and provider != "tdx_guest":
55
+ raise AttestError(f"TSM provider is {provider!r}, not tdx_guest")
56
+ return quote
57
+ except OSError as err:
58
+ if err.errno != errno.EINVAL or attempt == attempts - 1:
59
+ raise AttestError(f"TSM quote request failed: {err}") from err
60
+ time.sleep(0.05 * (attempt + 1))
61
+ finally:
62
+ try:
63
+ entry.rmdir()
64
+ except OSError:
65
+ pass
66
+ raise AttestError("TSM quote request failed after retries")
67
+
68
+
69
+ def gpu_count() -> int:
70
+ try:
71
+ import pynvml # type: ignore
72
+
73
+ pynvml.nvmlInit()
74
+ return int(pynvml.nvmlDeviceGetCount())
75
+ except Exception: # noqa: BLE001
76
+ smi = shutil.which("nvidia-smi")
77
+ if not smi:
78
+ return 0
79
+ out = subprocess.run([smi, "-L"], capture_output=True, text=True, timeout=30, check=False).stdout
80
+ return sum(1 for line in out.splitlines() if line.startswith("GPU "))
81
+
82
+
83
+ def nras_attest(nonce_hex: str, mode: str) -> Any:
84
+ """Run the nv-attestation-sdk against NRAS with our nonce. Returns the parsed token array."""
85
+ try:
86
+ from nv_attestation_sdk import attestation # type: ignore
87
+ except ImportError as err:
88
+ raise AttestError("nv-attestation-sdk is not installed: pip install 'voltage-verify[attest]'") from err
89
+ client = attestation.Attestation()
90
+ client.set_name("voltage-verify")
91
+ client.set_nonce(nonce_hex)
92
+ client.set_claims_version("2.0")
93
+ client.add_verifier(
94
+ attestation.Devices.GPU,
95
+ attestation.Environment.REMOTE,
96
+ NRAS_GPU_URL,
97
+ "",
98
+ ocsp_url=NRAS_OCSP_URL,
99
+ rim_url=NRAS_RIM_URL,
100
+ )
101
+ if mode == "multi-gpu-ppcie":
102
+ # On a multi-GPU node in NVIDIA Protected PCIe mode the SDK refuses "standalone"
103
+ # attestation; ppcie_mode=False means "not standalone" for this SDK version.
104
+ options = {"ppcie_mode": False}
105
+ try:
106
+ evidence = client.get_evidence(options=options)
107
+ except TypeError:
108
+ evidence = client.get_evidence(options)
109
+ try:
110
+ ok = client.attest(evidence, options=options)
111
+ except TypeError:
112
+ ok = client.attest(evidence)
113
+ else:
114
+ evidence = client.get_evidence()
115
+ ok = client.attest(evidence)
116
+ token = client.get_token()
117
+ if not token:
118
+ raise AttestError("NRAS returned no token")
119
+ parsed = json.loads(token) if isinstance(token, str) else token
120
+ if not ok:
121
+ raise AttestError("NRAS attestation did not succeed (token kept for diagnosis): " + json.dumps(parsed)[:300])
122
+ return parsed
123
+
124
+
125
+ def fetch_json(url: str) -> dict[str, Any]:
126
+ req = urllib.request.Request(url, headers={"User-Agent": pcs.USER_AGENT})
127
+ with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310
128
+ return json.loads(resp.read().decode("utf-8"))
129
+
130
+
131
+ def environment_facts() -> dict[str, Any]:
132
+ facts: dict[str, Any] = {"kernel": platform.release(), "python": platform.python_version()}
133
+ smi = shutil.which("nvidia-smi")
134
+ if smi:
135
+ for key, args in (("gpus", ["-L"]), ("conf_compute", ["conf-compute", "-q"])):
136
+ try:
137
+ run = subprocess.run([smi, *args], capture_output=True, text=True, timeout=30, check=False)
138
+ facts[key] = run.stdout.strip()
139
+ except (OSError, subprocess.SubprocessError):
140
+ pass
141
+ return facts
142
+
143
+
144
+ def run_attest(manifest_path: Path, out_path: Path, mode: str = "auto", with_collateral: bool = True) -> Bundle:
145
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
146
+ validate_manifest(manifest)
147
+ com = commitments(manifest)
148
+
149
+ quote_raw = tsm_quote(com.sha512)
150
+ quote = tdx.parse_quote(quote_raw)
151
+ if quote.report.report_data != com.sha512:
152
+ raise AttestError("the TDX quote does not carry the requested report_data")
153
+
154
+ if mode == "auto":
155
+ mode = "multi-gpu-ppcie" if gpu_count() > 1 else "single-gpu"
156
+ nras = nras_attest(com.sha256_hex, mode)
157
+
158
+ jwks: dict[str, Any] | None = None
159
+ collateral: dict[str, Any] | None = None
160
+ if with_collateral:
161
+ try:
162
+ jwks = fetch_json(NRAS_JWKS_URL)
163
+ except Exception as err: # noqa: BLE001
164
+ print(f"warning: could not fetch NRAS JWKS for offline verification: {err}")
165
+ try:
166
+ pck = tdx.pck_info(quote.pck_leaf)
167
+ collateral = {"intel": pcs.fetch_collateral(pck.fmspc, pcs.pck_ca_kind(quote)).to_json()}
168
+ except Exception as err: # noqa: BLE001
169
+ print(f"warning: could not fetch Intel collateral for offline verification: {err}")
170
+
171
+ bundle = Bundle(
172
+ manifest=manifest,
173
+ commitments={"sha256": com.sha256_hex, "sha512": com.sha512_hex},
174
+ quote=quote.raw,
175
+ report_data=com.sha512,
176
+ nvidia_mode=mode,
177
+ nras=nras,
178
+ jwks=jwks,
179
+ collateral=collateral,
180
+ environment=environment_facts(),
181
+ created_at=datetime.now(timezone.utc),
182
+ )
183
+ bundle.save(out_path)
184
+ return bundle
@@ -0,0 +1,101 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """The evidence bundle: one JSON file that carries everything a verifier needs.
3
+
4
+ Format ``voltage-verify-bundle/1``::
5
+
6
+ {
7
+ "format": "voltage-verify-bundle/1",
8
+ "tool": {"name": "voltage-verify", "version": "0.1.0"},
9
+ "created_at": "2026-09-12T01:02:03+00:00",
10
+ "manifest": { ... the workload manifest, verbatim ... },
11
+ "commitments": {"sha256": "<hex>", "sha512": "<hex>"},
12
+ "tdx": {"quote_b64": "<base64>", "report_data_hex": "<hex>"},
13
+ "nvidia": {"mode": "single-gpu" | "multi-gpu-ppcie", "nras": [...], "jwks": {...}},
14
+ "collateral": {"intel": { ... pcs.Collateral ... }},
15
+ "environment": { ... free-form facts recorded on the VM ... }
16
+ }
17
+
18
+ The manifest is the only input a verifier must trust nothing about: every other field is
19
+ either derived from it (commitments), signed by Intel (quote, collateral) or signed by NVIDIA
20
+ (NRAS tokens). The JWKS and Intel collateral are embedded so that verification can run
21
+ offline against the material of the day; an online verification refetches and compares.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import base64
27
+ import json
28
+ from dataclasses import dataclass, field
29
+ from datetime import datetime, timezone
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ from . import __version__
34
+
35
+ FORMAT = "voltage-verify-bundle/1"
36
+ MANIFEST_FORMAT = "voltage-verify-manifest/1"
37
+
38
+
39
+ class BundleError(ValueError):
40
+ """Raised when a bundle file is not usable."""
41
+
42
+
43
+ @dataclass
44
+ class Bundle:
45
+ manifest: dict[str, Any]
46
+ commitments: dict[str, str]
47
+ quote: bytes
48
+ report_data: bytes
49
+ nvidia_mode: str
50
+ nras: Any
51
+ jwks: dict[str, Any] | None
52
+ collateral: dict[str, Any] | None
53
+ environment: dict[str, Any] = field(default_factory=dict)
54
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
55
+ tool: dict[str, str] = field(default_factory=lambda: {"name": "voltage-verify", "version": __version__})
56
+
57
+ def to_json(self) -> dict[str, Any]:
58
+ return {
59
+ "format": FORMAT,
60
+ "tool": self.tool,
61
+ "created_at": self.created_at.isoformat(timespec="seconds"),
62
+ "manifest": self.manifest,
63
+ "commitments": self.commitments,
64
+ "tdx": {"quote_b64": base64.b64encode(self.quote).decode("ascii"), "report_data_hex": self.report_data.hex()},
65
+ "nvidia": {"mode": self.nvidia_mode, "nras": self.nras, "jwks": self.jwks},
66
+ "collateral": self.collateral,
67
+ "environment": self.environment,
68
+ }
69
+
70
+ def save(self, path: Path) -> None:
71
+ path.write_text(json.dumps(self.to_json(), indent=2, sort_keys=True), encoding="utf-8")
72
+
73
+ @classmethod
74
+ def from_json(cls, data: dict[str, Any]) -> Bundle:
75
+ if data.get("format") != FORMAT:
76
+ raise BundleError(f"unsupported bundle format {data.get('format')!r}")
77
+ try:
78
+ created = datetime.fromisoformat(str(data["created_at"]).replace("Z", "+00:00"))
79
+ return cls(
80
+ manifest=data["manifest"],
81
+ commitments=dict(data["commitments"]),
82
+ quote=base64.b64decode(data["tdx"]["quote_b64"]),
83
+ report_data=bytes.fromhex(data["tdx"]["report_data_hex"]),
84
+ nvidia_mode=str(data["nvidia"]["mode"]),
85
+ nras=data["nvidia"]["nras"],
86
+ jwks=data["nvidia"].get("jwks"),
87
+ collateral=data.get("collateral"),
88
+ environment=dict(data.get("environment") or {}),
89
+ created_at=created,
90
+ tool=dict(data.get("tool") or {}),
91
+ )
92
+ except (KeyError, TypeError, ValueError) as err:
93
+ raise BundleError(f"bundle is missing or has a malformed field: {err}") from err
94
+
95
+ @classmethod
96
+ def load(cls, path: Path) -> Bundle:
97
+ try:
98
+ data = json.loads(path.read_text(encoding="utf-8"))
99
+ except (OSError, ValueError) as err:
100
+ raise BundleError(f"cannot read {path}: {err}") from err
101
+ return cls.from_json(data)
@@ -0,0 +1,67 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Canonical serialisation of the workload manifest and the two commitments derived from it.
3
+
4
+ The manifest is serialised the way RFC 8785 (JSON Canonicalization Scheme) does for the
5
+ value types this tool emits: object keys sorted by code point, no insignificant whitespace,
6
+ UTF-8, no floats. Two parties that hold the same manifest therefore compute byte-identical
7
+ input and, from it, the same commitments:
8
+
9
+ * ``commitment256`` = SHA-256(canonical manifest): the 32-byte nonce handed to the NVIDIA
10
+ attestation flow (NRAS echoes it back in ``eat_nonce``).
11
+ * ``commitment512`` = SHA-512(canonical manifest): the 64-byte ``report_data`` written into
12
+ the Intel TDX quote request (the quote embeds it verbatim).
13
+
14
+ Anyone who changes a single byte of the manifest (image digest, model digest, challenge,
15
+ anything) changes both commitments, and both proofs stop matching.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ from dataclasses import dataclass
23
+ from typing import Any
24
+
25
+
26
+ class CanonicalError(ValueError):
27
+ """Raised when a manifest cannot be canonicalised deterministically."""
28
+
29
+
30
+ def _reject_floats(value: Any, path: str = "$") -> None:
31
+ if isinstance(value, float):
32
+ raise CanonicalError(f"floats are not allowed in a manifest (at {path})")
33
+ if isinstance(value, dict):
34
+ for k, v in value.items():
35
+ if not isinstance(k, str):
36
+ raise CanonicalError(f"object keys must be strings (at {path})")
37
+ _reject_floats(v, f"{path}.{k}")
38
+ elif isinstance(value, list):
39
+ for i, v in enumerate(value):
40
+ _reject_floats(v, f"{path}[{i}]")
41
+
42
+
43
+ def canonical_bytes(manifest: dict[str, Any]) -> bytes:
44
+ """Return the canonical UTF-8 encoding of ``manifest``."""
45
+ _reject_floats(manifest)
46
+ return json.dumps(manifest, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class Commitments:
51
+ canonical: bytes
52
+ sha256: bytes
53
+ sha512: bytes
54
+
55
+ @property
56
+ def sha256_hex(self) -> str:
57
+ return self.sha256.hex()
58
+
59
+ @property
60
+ def sha512_hex(self) -> str:
61
+ return self.sha512.hex()
62
+
63
+
64
+ def commitments(manifest: dict[str, Any]) -> Commitments:
65
+ """Compute both commitments of a manifest."""
66
+ raw = canonical_bytes(manifest)
67
+ return Commitments(raw, hashlib.sha256(raw).digest(), hashlib.sha512(raw).digest())
voltage_verify/cli.py ADDED
@@ -0,0 +1,184 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Command line entry point."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from . import __version__
12
+ from .bundle import Bundle, BundleError
13
+ from .canonical import commitments
14
+ from .manifest import ManifestError, build_manifest
15
+ from .registry import RegistryError
16
+ from .verify import Report, verify_bundle
17
+
18
+
19
+ def _print_report(report: Report) -> None:
20
+ width = max(len(c.name) for c in report.checks) if report.checks else 20
21
+ for check in report.checks:
22
+ print(f"[{check.label}] {check.name.ljust(width)} {check.detail}")
23
+ facts = report.facts
24
+ if "tdx" in facts:
25
+ t = facts["tdx"]
26
+ print(f"\nTDX: MRTD {t['mr_td'][:32]}... TCB {t.get('tcb_status', '?')} QE {t.get('qe_status', '?')}")
27
+ if "nvidia" in facts:
28
+ for d in facts["nvidia"]["devices"]:
29
+ print(f"GPU: {d['name']} {d['hwmodel']} driver {d['driver']} vbios {d['vbios']} measurements {d['measres']}")
30
+ print("\nRESULT:", "VERIFIED" if report.ok else "NOT VERIFIED (" + ", ".join(report.failed()) + ")")
31
+
32
+
33
+ def cmd_manifest(args: argparse.Namespace) -> int:
34
+ extra: dict[str, str] = {}
35
+ for item in args.extra or []:
36
+ if "=" not in item:
37
+ print(f"--extra expects key=value, got {item!r}", file=sys.stderr)
38
+ return 2
39
+ key, value = item.split("=", 1)
40
+ extra[key] = value
41
+ try:
42
+ manifest = build_manifest(
43
+ image=args.image,
44
+ image_digest=args.image_digest,
45
+ artifacts=[Path(p) for p in (args.artifact or [])],
46
+ extra=extra,
47
+ statement=args.statement,
48
+ challenge=None if args.challenge in (None, "auto") else args.challenge,
49
+ )
50
+ except (ManifestError, RegistryError) as err:
51
+ print(f"error: {err}", file=sys.stderr)
52
+ return 2
53
+ Path(args.output).write_text(json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8")
54
+ com = commitments(manifest)
55
+ print(f"manifest written to {args.output}")
56
+ print(f"challenge {manifest['challenge']}")
57
+ print(f"commitment256 {com.sha256_hex} (NVIDIA nonce)")
58
+ print(f"commitment512 {com.sha512_hex[:64]}... (TDX report_data)")
59
+ print("keep the challenge: pass it to `verify --challenge` to reject replayed bundles")
60
+ return 0
61
+
62
+
63
+ def cmd_attest(args: argparse.Namespace) -> int:
64
+ from .attest import AttestError, run_attest # imported late: VM-only dependencies
65
+
66
+ try:
67
+ bundle = run_attest(Path(args.manifest), Path(args.output), mode=args.mode, with_collateral=not args.no_collateral)
68
+ except AttestError as err:
69
+ print(f"error: {err}", file=sys.stderr)
70
+ return 2
71
+ print(f"bundle written to {args.output} ({bundle.nvidia_mode}, {len(bundle.quote)}-byte quote)")
72
+ return 0
73
+
74
+
75
+ def _load(path: str) -> Bundle | None:
76
+ try:
77
+ return Bundle.load(Path(path))
78
+ except BundleError as err:
79
+ print(f"error: {err}", file=sys.stderr)
80
+ return None
81
+
82
+
83
+ def cmd_verify(args: argparse.Namespace) -> int:
84
+ bundle = _load(args.bundle)
85
+ if bundle is None:
86
+ return 2
87
+ report = verify_bundle(
88
+ bundle,
89
+ expected_challenge=args.challenge,
90
+ expected_image_digest=args.image_digest,
91
+ online=not args.offline,
92
+ allowed_hwmodels=set(args.hwmodel) if args.hwmodel else None,
93
+ require_devices=args.gpus,
94
+ )
95
+ if args.json:
96
+ print(json.dumps(report.to_json(), indent=2))
97
+ else:
98
+ _print_report(report)
99
+ return 0 if report.ok else 1
100
+
101
+
102
+ def cmd_inspect(args: argparse.Namespace) -> int:
103
+ bundle = _load(args.bundle)
104
+ if bundle is None:
105
+ return 2
106
+ data = bundle.to_json()
107
+ data["tdx"]["quote_b64"] = f"<{len(bundle.quote)} bytes>"
108
+ data["nvidia"]["jwks"] = "<embedded>" if bundle.jwks else None
109
+ data["collateral"] = "<embedded>" if bundle.collateral else None
110
+ print(json.dumps(data, indent=2, sort_keys=True))
111
+ return 0
112
+
113
+
114
+ def cmd_selftest(args: argparse.Namespace) -> int:
115
+ from .selftest import run_selftest
116
+
117
+ bundle = _load(args.bundle)
118
+ if bundle is None:
119
+ return 2
120
+ clean, results = run_selftest(bundle, online=not args.offline, expected_challenge=args.challenge)
121
+ print("clean bundle:", "VERIFIED" if clean.ok else "NOT VERIFIED (" + ", ".join(clean.failed()) + ")")
122
+ all_ok = clean.ok
123
+ for r in results:
124
+ all_ok = all_ok and r.passed
125
+ rejected = ", ".join(r.failed_checks) or "nothing"
126
+ print(f"[{'PASS' if r.passed else 'FAIL'}] {r.name.ljust(16)} {r.description} -> rejected on {rejected}")
127
+ print("\nSELFTEST:", "PASS" if all_ok else "FAIL")
128
+ return 0 if all_ok else 1
129
+
130
+
131
+ def build_parser() -> argparse.ArgumentParser:
132
+ p = argparse.ArgumentParser(
133
+ prog="voltage-verify",
134
+ description="Bind TDX and NVIDIA attestations to a workload and verify them.",
135
+ )
136
+ p.add_argument("--version", action="version", version=f"voltage-verify {__version__}")
137
+ sub = p.add_subparsers(dest="command", required=True)
138
+
139
+ m = sub.add_parser("manifest", help="build the workload manifest on the verifier's machine")
140
+ m.add_argument("--image", help="container image reference, e.g. ghcr.io/org/app:1.2.3")
141
+ m.add_argument("--image-digest", help="sha256:... digest, when the registry cannot be queried")
142
+ m.add_argument("--artifact", action="append", help="file to hash (model weights, config); repeatable")
143
+ m.add_argument("--extra", action="append", help="key=value to include in the manifest; repeatable")
144
+ m.add_argument("--statement", help="free text describing the run")
145
+ m.add_argument("--challenge", default="auto", help="32-byte hex challenge, or 'auto' to generate one")
146
+ m.add_argument("-o", "--output", default="manifest.json")
147
+ m.set_defaults(func=cmd_manifest)
148
+
149
+ a = sub.add_parser("attest", help="produce the evidence bundle; run as root inside the TDX VM")
150
+ a.add_argument("--manifest", required=True)
151
+ a.add_argument("-o", "--output", default="bundle.json")
152
+ a.add_argument("--mode", choices=["auto", "single-gpu", "multi-gpu-ppcie"], default="auto")
153
+ a.add_argument("--no-collateral", action="store_true", help="do not embed Intel/NVIDIA collateral")
154
+ a.set_defaults(func=cmd_attest)
155
+
156
+ v = sub.add_parser("verify", help="verify a bundle")
157
+ v.add_argument("bundle")
158
+ v.add_argument("--challenge", help="the challenge you issued; rejects replayed bundles")
159
+ v.add_argument("--image-digest", help="expected sha256:... of the workload image")
160
+ v.add_argument("--offline", action="store_true", help="use the collateral embedded in the bundle")
161
+ v.add_argument("--hwmodel", action="append", help="allowed GPU model claim, e.g. GH100; repeatable")
162
+ v.add_argument("--gpus", type=int, help="exact number of attested GPUs expected")
163
+ v.add_argument("--json", action="store_true")
164
+ v.set_defaults(func=cmd_verify)
165
+
166
+ i = sub.add_parser("inspect", help="print a bundle without the bulky fields")
167
+ i.add_argument("bundle")
168
+ i.set_defaults(func=cmd_inspect)
169
+
170
+ s = sub.add_parser("selftest", help="verify a bundle, then check that six mutations are rejected")
171
+ s.add_argument("bundle")
172
+ s.add_argument("--challenge")
173
+ s.add_argument("--offline", action="store_true")
174
+ s.set_defaults(func=cmd_selftest)
175
+ return p
176
+
177
+
178
+ def main(argv: list[str] | None = None) -> int:
179
+ args = build_parser().parse_args(argv)
180
+ return int(args.func(args))
181
+
182
+
183
+ if __name__ == "__main__":
184
+ sys.exit(main())
@@ -0,0 +1,16 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw
3
+ aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv
4
+ cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ
5
+ BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG
6
+ A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0
7
+ aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT
8
+ AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7
9
+ 1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB
10
+ uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ
11
+ MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50
12
+ ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV
13
+ Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI
14
+ KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg
15
+ AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=
16
+ -----END CERTIFICATE-----
voltage_verify/der.py ADDED
@@ -0,0 +1,92 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """A deliberately small DER reader, enough to walk the Intel SGX PCK certificate extension.
3
+
4
+ The extension (OID 1.2.840.113741.1.13.1) is a SEQUENCE of SEQUENCE { OID, value } pairs whose
5
+ values are OCTET STRINGs, INTEGERs, ENUMERATEDs or nested SEQUENCEs. Nothing else is needed,
6
+ so nothing else is implemented; unknown tags are returned as raw bytes.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+
14
+ TAG_INTEGER = 0x02
15
+ TAG_OCTET_STRING = 0x04
16
+ TAG_OID = 0x06
17
+ TAG_ENUMERATED = 0x0A
18
+ TAG_SEQUENCE = 0x30
19
+
20
+
21
+ class DerError(ValueError):
22
+ """Raised on malformed DER."""
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Node:
27
+ tag: int
28
+ value: Any # int | bytes | str (OID) | list[Node]
29
+
30
+
31
+ def _read_length(data: bytes, pos: int) -> tuple[int, int]:
32
+ first = data[pos]
33
+ pos += 1
34
+ if first < 0x80:
35
+ return first, pos
36
+ count = first & 0x7F
37
+ if count == 0 or count > 4 or pos + count > len(data):
38
+ raise DerError("unsupported DER length encoding")
39
+ return int.from_bytes(data[pos : pos + count], "big"), pos + count
40
+
41
+
42
+ def _decode_oid(raw: bytes) -> str:
43
+ if not raw:
44
+ raise DerError("empty OID")
45
+ first = raw[0]
46
+ parts = [str(first // 40), str(first % 40)]
47
+ value = 0
48
+ for byte in raw[1:]:
49
+ value = (value << 7) | (byte & 0x7F)
50
+ if not byte & 0x80:
51
+ parts.append(str(value))
52
+ value = 0
53
+ if value:
54
+ raise DerError("truncated OID")
55
+ return ".".join(parts)
56
+
57
+
58
+ def decode(data: bytes) -> list[Node]:
59
+ """Decode a concatenation of DER TLVs into nodes (SEQUENCEs are decoded recursively)."""
60
+ nodes: list[Node] = []
61
+ pos = 0
62
+ while pos < len(data):
63
+ if pos + 2 > len(data):
64
+ raise DerError("truncated TLV")
65
+ tag = data[pos]
66
+ length, body_start = _read_length(data, pos + 1)
67
+ body_end = body_start + length
68
+ if body_end > len(data):
69
+ raise DerError("TLV runs past the end of the buffer")
70
+ body = data[body_start:body_end]
71
+ if tag == TAG_SEQUENCE:
72
+ nodes.append(Node(tag, decode(body)))
73
+ elif tag == TAG_OID:
74
+ nodes.append(Node(tag, _decode_oid(body)))
75
+ elif tag in (TAG_INTEGER, TAG_ENUMERATED):
76
+ nodes.append(Node(tag, int.from_bytes(body, "big", signed=True)))
77
+ else:
78
+ nodes.append(Node(tag, body))
79
+ pos = body_end
80
+ return nodes
81
+
82
+
83
+ def pairs(sequence: Node) -> dict[str, Node]:
84
+ """Turn a SEQUENCE of { OID, value } SEQUENCEs into a dict keyed by dotted OID."""
85
+ if sequence.tag != TAG_SEQUENCE:
86
+ raise DerError("expected a SEQUENCE")
87
+ out: dict[str, Node] = {}
88
+ for item in sequence.value:
89
+ if item.tag != TAG_SEQUENCE or len(item.value) != 2 or item.value[0].tag != TAG_OID:
90
+ raise DerError("expected SEQUENCE { OID, value }")
91
+ out[item.value[0].value] = item.value[1]
92
+ return out