voltry-probe 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.
- voltry_probe/__init__.py +40 -0
- voltry_probe/attestation/__init__.py +14 -0
- voltry_probe/attestation/model.py +33 -0
- voltry_probe/attestation/verify.py +190 -0
- voltry_probe/cli.py +214 -0
- voltry_probe/evidence/__init__.py +11 -0
- voltry_probe/evidence/builder.py +165 -0
- voltry_probe/functional/__init__.py +13 -0
- voltry_probe/functional/runner.py +68 -0
- voltry_probe/monitor/__init__.py +13 -0
- voltry_probe/monitor/monitor.py +48 -0
- voltry_probe/py.typed +0 -0
- voltry_probe/readers/__init__.py +23 -0
- voltry_probe/readers/dcgm.py +64 -0
- voltry_probe/readers/nvml.py +188 -0
- voltry_probe/readers/redfish.py +26 -0
- voltry_probe/readers/taxonomy.py +38 -0
- voltry_probe/render/__init__.py +7 -0
- voltry_probe/render/assets/certificate.css +178 -0
- voltry_probe/render/assets/tokens.css +40 -0
- voltry_probe/render/certificate.py +231 -0
- voltry_probe/sources/__init__.py +13 -0
- voltry_probe/sources/base.py +40 -0
- voltry_probe/sources/fixture.py +19 -0
- voltry_probe/sources/live.py +94 -0
- voltry_probe-0.1.0.dist-info/METADATA +118 -0
- voltry_probe-0.1.0.dist-info/RECORD +30 -0
- voltry_probe-0.1.0.dist-info/WHEEL +4 -0
- voltry_probe-0.1.0.dist-info/entry_points.txt +2 -0
- voltry_probe-0.1.0.dist-info/licenses/LICENSE +201 -0
voltry_probe/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Voltry Probe — the open, read-only agent.
|
|
2
|
+
|
|
3
|
+
Read mode (Stage 3): read-only NVML/DCGM/Redfish mappers + real attestation verification
|
|
4
|
+
→ a signed evidence bundle. Render (Stage 4), CLI + container (Stage 5), and the
|
|
5
|
+
functional/monitor modes (Stage 9) build on this.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .attestation import AttestationOutcome, AttestationReport, verify_attestation
|
|
11
|
+
from .evidence import build_functional_bundle, build_monitor_bundle, build_read_bundle
|
|
12
|
+
from .functional import DrainConsentError, FunctionalRunner
|
|
13
|
+
from .monitor import MonitorSession, TelemetrySample
|
|
14
|
+
from .readers import map_dcgm, map_nvml, map_redfish, normalize_xid
|
|
15
|
+
from .render import render_certificate
|
|
16
|
+
from .sources import FixtureSource, RawCapture, RawSource
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"__version__",
|
|
22
|
+
"build_read_bundle",
|
|
23
|
+
"build_functional_bundle",
|
|
24
|
+
"build_monitor_bundle",
|
|
25
|
+
"FunctionalRunner",
|
|
26
|
+
"DrainConsentError",
|
|
27
|
+
"MonitorSession",
|
|
28
|
+
"TelemetrySample",
|
|
29
|
+
"render_certificate",
|
|
30
|
+
"map_nvml",
|
|
31
|
+
"map_dcgm",
|
|
32
|
+
"map_redfish",
|
|
33
|
+
"normalize_xid",
|
|
34
|
+
"verify_attestation",
|
|
35
|
+
"AttestationReport",
|
|
36
|
+
"AttestationOutcome",
|
|
37
|
+
"FixtureSource",
|
|
38
|
+
"RawCapture",
|
|
39
|
+
"RawSource",
|
|
40
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Attestation: real cryptographic verification of the device identity chain.
|
|
2
|
+
|
|
3
|
+
The verdict reflects the **real** chain state and is never stubbed as PASS (crypto-standards
|
|
4
|
+
skill, §security). ``UNVERIFIED`` when the root/PKI is unreachable; ``FAILED`` on a broken
|
|
5
|
+
chain or measurement; ``FALLBACK`` (lower confidence, marked) for parts without a hardware
|
|
6
|
+
root; ``VERIFIED`` only when the full chain checks out.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .model import AttestationReport
|
|
12
|
+
from .verify import AttestationOutcome, verify_attestation
|
|
13
|
+
|
|
14
|
+
__all__ = ["AttestationReport", "AttestationOutcome", "verify_attestation"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""The attestation report payload.
|
|
2
|
+
|
|
3
|
+
A deliberately small but faithful model of the NVIDIA Device Identity (over SPDM) chain:
|
|
4
|
+
a device identity key whose binding is signed by the NVIDIA root, plus a freshness-bound
|
|
5
|
+
measurement (VBIOS hash + nonce) signed by the device key. Verification (``verify.py``)
|
|
6
|
+
checks both signatures and the VBIOS hash — the same structure as the real chain
|
|
7
|
+
(root → device cert → signed measurement), without re-implementing X.509/SPDM transport.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from evidence_schema import IdentityScheme
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AttestationReport(BaseModel):
|
|
17
|
+
"""Verbatim attestation report to be verified (never trusted blindly)."""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(extra="forbid")
|
|
20
|
+
|
|
21
|
+
scheme: IdentityScheme = Field(description="HARDWARE_ROOT or SECONDARY_FALLBACK.")
|
|
22
|
+
device_id: str = Field(description="Device identity id (the permanent ECC-384 id).")
|
|
23
|
+
device_public_key_b64: str = Field(
|
|
24
|
+
description="Device identity public key (SPKI DER, base64), P-384."
|
|
25
|
+
)
|
|
26
|
+
root_signature_b64: str = Field(
|
|
27
|
+
description="Root CA signature over the canonical (device_id, device_public_key) binding."
|
|
28
|
+
)
|
|
29
|
+
vbios_hash: str = Field(description="VBIOS measurement hash attested by this report.")
|
|
30
|
+
nonce: str = Field(description="Freshness nonce bound into the measurement signature.")
|
|
31
|
+
measurement_signature_b64: str = Field(
|
|
32
|
+
description="Device-key signature over the canonical (nonce, vbios_hash) measurement."
|
|
33
|
+
)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Verify an attestation report against a trusted root — real ECDSA P-384 checks.
|
|
2
|
+
|
|
3
|
+
Outcome rules (never a stubbed PASS):
|
|
4
|
+
- no report → UNVERIFIED (authenticity NOT_ASSESSED)
|
|
5
|
+
- no trusted root provided → UNVERIFIED (chain unevaluable; authenticity NOT_ASSESSED)
|
|
6
|
+
- root signature invalid → FAILED (authenticity FAIL — disqualifying)
|
|
7
|
+
- measurement signature invalid → FAILED (authenticity FAIL — disqualifying)
|
|
8
|
+
- VBIOS hash != expected → reflash_detected=True, firmware FAIL (disqualifying)
|
|
9
|
+
- all checks pass, HARDWARE_ROOT→ VERIFIED (authenticity PASS, high confidence)
|
|
10
|
+
- all checks pass, FALLBACK → FALLBACK (authenticity PASS, lower confidence, marked)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
import binascii
|
|
17
|
+
|
|
18
|
+
import rfc8785
|
|
19
|
+
from cryptography.exceptions import InvalidSignature
|
|
20
|
+
from cryptography.hazmat.primitives import hashes
|
|
21
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
22
|
+
from evidence_schema import (
|
|
23
|
+
Attestation,
|
|
24
|
+
AttestationVerdict,
|
|
25
|
+
GateResult,
|
|
26
|
+
IdentityScheme,
|
|
27
|
+
)
|
|
28
|
+
from evidence_schema.sign import load_public_key_spki_b64
|
|
29
|
+
from evidence_schema.types import UtcDateTime, utcnow
|
|
30
|
+
from pydantic import BaseModel, ConfigDict
|
|
31
|
+
|
|
32
|
+
from .model import AttestationReport
|
|
33
|
+
|
|
34
|
+
_HASH = hashes.SHA384
|
|
35
|
+
# Confidence levels by outcome (documented, not magic): a hardware-rooted verified chain
|
|
36
|
+
# is near-certain; a secondary fallback is materially lower and explicitly marked.
|
|
37
|
+
_CONFIDENCE_VERIFIED = 0.99
|
|
38
|
+
_CONFIDENCE_FALLBACK = 0.60
|
|
39
|
+
_CONFIDENCE_UNVERIFIED = 0.0
|
|
40
|
+
_CONFIDENCE_FAILED = 0.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AttestationOutcome(BaseModel):
|
|
44
|
+
"""The verified result: the certificate's Attestation block + derived gate inputs."""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(extra="forbid")
|
|
47
|
+
|
|
48
|
+
attestation: Attestation
|
|
49
|
+
reflash_detected: bool
|
|
50
|
+
authenticity_confidence: float
|
|
51
|
+
identity_scheme: IdentityScheme
|
|
52
|
+
vbios_hash: str | None
|
|
53
|
+
authenticity_gate: GateResult
|
|
54
|
+
firmware_gate: GateResult
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _root_statement_bytes(report: AttestationReport) -> bytes:
|
|
58
|
+
return rfc8785.dumps(
|
|
59
|
+
{"device_id": report.device_id, "device_public_key_b64": report.device_public_key_b64}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _measurement_bytes(report: AttestationReport) -> bytes:
|
|
64
|
+
return rfc8785.dumps({"nonce": report.nonce, "vbios_hash": report.vbios_hash})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _ecdsa_ok(public_key: ec.EllipticCurvePublicKey, signature_b64: str, payload: bytes) -> bool:
|
|
68
|
+
try:
|
|
69
|
+
public_key.verify(
|
|
70
|
+
base64.b64decode(signature_b64, validate=True), payload, ec.ECDSA(_HASH())
|
|
71
|
+
)
|
|
72
|
+
return True
|
|
73
|
+
except (InvalidSignature, ValueError, binascii.Error):
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _unverified(
|
|
78
|
+
scheme: IdentityScheme,
|
|
79
|
+
detail: str,
|
|
80
|
+
*,
|
|
81
|
+
root_reachability: bool,
|
|
82
|
+
measured_at: UtcDateTime | None,
|
|
83
|
+
) -> AttestationOutcome:
|
|
84
|
+
return AttestationOutcome(
|
|
85
|
+
attestation=Attestation(
|
|
86
|
+
scheme=scheme,
|
|
87
|
+
verdict=AttestationVerdict.UNVERIFIED,
|
|
88
|
+
root_reachability=root_reachability,
|
|
89
|
+
detail=detail,
|
|
90
|
+
measured_at=measured_at,
|
|
91
|
+
),
|
|
92
|
+
reflash_detected=False,
|
|
93
|
+
authenticity_confidence=_CONFIDENCE_UNVERIFIED,
|
|
94
|
+
identity_scheme=scheme,
|
|
95
|
+
vbios_hash=None,
|
|
96
|
+
authenticity_gate=GateResult.NOT_ASSESSED,
|
|
97
|
+
firmware_gate=GateResult.NOT_ASSESSED,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def verify_attestation(
|
|
102
|
+
report: AttestationReport | None,
|
|
103
|
+
*,
|
|
104
|
+
trusted_root_public_key: ec.EllipticCurvePublicKey | None = None,
|
|
105
|
+
expected_vbios_hash: str | None = None,
|
|
106
|
+
measured_at: UtcDateTime | None = None,
|
|
107
|
+
) -> AttestationOutcome:
|
|
108
|
+
"""Verify ``report`` against ``trusted_root_public_key`` and return the real outcome."""
|
|
109
|
+
when = measured_at if measured_at is not None else utcnow()
|
|
110
|
+
|
|
111
|
+
if report is None:
|
|
112
|
+
return _unverified(
|
|
113
|
+
IdentityScheme.SECONDARY_FALLBACK,
|
|
114
|
+
"No attestation report available.",
|
|
115
|
+
root_reachability=False,
|
|
116
|
+
measured_at=when,
|
|
117
|
+
)
|
|
118
|
+
if trusted_root_public_key is None:
|
|
119
|
+
return _unverified(
|
|
120
|
+
report.scheme,
|
|
121
|
+
"Trusted root unreachable; attestation chain not evaluated.",
|
|
122
|
+
root_reachability=False,
|
|
123
|
+
measured_at=when,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# 1) Chain: the root must have signed the device-key binding.
|
|
127
|
+
root_ok = _ecdsa_ok(
|
|
128
|
+
trusted_root_public_key, report.root_signature_b64, _root_statement_bytes(report)
|
|
129
|
+
)
|
|
130
|
+
# 2) Measurement: the device key must have signed (nonce, vbios_hash).
|
|
131
|
+
measurement_ok = False
|
|
132
|
+
if root_ok:
|
|
133
|
+
try:
|
|
134
|
+
device_key = load_public_key_spki_b64(report.device_public_key_b64)
|
|
135
|
+
measurement_ok = _ecdsa_ok(
|
|
136
|
+
device_key, report.measurement_signature_b64, _measurement_bytes(report)
|
|
137
|
+
)
|
|
138
|
+
except (ValueError, binascii.Error):
|
|
139
|
+
measurement_ok = False
|
|
140
|
+
|
|
141
|
+
if not (root_ok and measurement_ok):
|
|
142
|
+
detail = "Root signature invalid." if not root_ok else "Measurement signature invalid."
|
|
143
|
+
return AttestationOutcome(
|
|
144
|
+
attestation=Attestation(
|
|
145
|
+
scheme=report.scheme,
|
|
146
|
+
verdict=AttestationVerdict.FAILED,
|
|
147
|
+
root_reachability=True,
|
|
148
|
+
detail=detail,
|
|
149
|
+
measured_at=when,
|
|
150
|
+
),
|
|
151
|
+
reflash_detected=False,
|
|
152
|
+
authenticity_confidence=_CONFIDENCE_FAILED,
|
|
153
|
+
identity_scheme=report.scheme,
|
|
154
|
+
vbios_hash=report.vbios_hash,
|
|
155
|
+
authenticity_gate=GateResult.FAIL,
|
|
156
|
+
firmware_gate=GateResult.NOT_ASSESSED,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# 3) Re-flash detection: compare attested VBIOS hash to the known-good expectation.
|
|
160
|
+
reflash = expected_vbios_hash is not None and report.vbios_hash != expected_vbios_hash
|
|
161
|
+
firmware_gate = (
|
|
162
|
+
GateResult.FAIL
|
|
163
|
+
if reflash
|
|
164
|
+
else (GateResult.PASS if expected_vbios_hash is not None else GateResult.NOT_ASSESSED)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if report.scheme is IdentityScheme.HARDWARE_ROOT:
|
|
168
|
+
verdict, confidence = AttestationVerdict.VERIFIED, _CONFIDENCE_VERIFIED
|
|
169
|
+
detail = "Device Identity chain verified; measurement signature valid."
|
|
170
|
+
else:
|
|
171
|
+
verdict, confidence = AttestationVerdict.FALLBACK, _CONFIDENCE_FALLBACK
|
|
172
|
+
detail = "Secondary (no hardware root) scheme verified at lower confidence."
|
|
173
|
+
|
|
174
|
+
return AttestationOutcome(
|
|
175
|
+
attestation=Attestation(
|
|
176
|
+
scheme=report.scheme,
|
|
177
|
+
verdict=verdict,
|
|
178
|
+
root_reachability=True,
|
|
179
|
+
detail=detail + (" VBIOS re-flash detected." if reflash else ""),
|
|
180
|
+
measured_at=when,
|
|
181
|
+
),
|
|
182
|
+
reflash_detected=reflash,
|
|
183
|
+
authenticity_confidence=confidence,
|
|
184
|
+
identity_scheme=report.scheme,
|
|
185
|
+
vbios_hash=report.vbios_hash,
|
|
186
|
+
# A verified-genuine chain passes authenticity even on the marked fallback scheme;
|
|
187
|
+
# a detected re-flash fails the firmware gate (disqualifying) but the part is still genuine.
|
|
188
|
+
authenticity_gate=GateResult.PASS,
|
|
189
|
+
firmware_gate=firmware_gate,
|
|
190
|
+
)
|
voltry_probe/cli.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""``voltry`` — the probe CLI.
|
|
2
|
+
|
|
3
|
+
Three commands:
|
|
4
|
+
- ``voltry scan`` read-only capture → signed evidence bundle (works fully OFFLINE).
|
|
5
|
+
- ``voltry cert`` bundle → self-contained offline HTML certificate (works OFFLINE).
|
|
6
|
+
- ``voltry submit`` upload a signed bundle to the platform — **opt-in and separate**: it
|
|
7
|
+
is the only networked command, requires an explicit consent flag, and its HTTP client
|
|
8
|
+
is an optional dependency so scan/cert stay offline and dependency-light (§1.7).
|
|
9
|
+
|
|
10
|
+
No PII is collected: identity is device-level only; account linkage is server-side (NGC
|
|
11
|
+
GDPR posture).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import platform
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
from cryptography.hazmat.primitives import serialization
|
|
22
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
23
|
+
from evidence_schema import AgentInfo, EvidenceBundle, RunMode, generate_keypair, verify_bundle
|
|
24
|
+
|
|
25
|
+
from . import __version__
|
|
26
|
+
from .evidence import build_read_bundle
|
|
27
|
+
from .render import render_certificate
|
|
28
|
+
from .sources import FixtureSource
|
|
29
|
+
|
|
30
|
+
app = typer.Typer(
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
add_completion=False,
|
|
33
|
+
help="Voltry Probe — read-only GPU condition & provenance (scan/cert offline; submit opt-in).",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _version_callback(value: bool) -> None:
|
|
38
|
+
if value:
|
|
39
|
+
typer.echo(f"voltry-probe {__version__}")
|
|
40
|
+
raise typer.Exit()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.callback()
|
|
44
|
+
def _root(
|
|
45
|
+
version: bool = typer.Option(
|
|
46
|
+
False,
|
|
47
|
+
"--version",
|
|
48
|
+
callback=_version_callback,
|
|
49
|
+
is_eager=True,
|
|
50
|
+
help="Show the version and exit.",
|
|
51
|
+
),
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Voltry Probe — read-only GPU condition & provenance."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _load_signing_key(signing_key: Path | None, ephemeral: bool) -> ec.EllipticCurvePrivateKey:
|
|
57
|
+
"""Load the operator signing key from PEM, or generate an ephemeral one if opted in."""
|
|
58
|
+
if signing_key is not None:
|
|
59
|
+
key = serialization.load_pem_private_key(signing_key.read_bytes(), password=None)
|
|
60
|
+
if not isinstance(key, ec.EllipticCurvePrivateKey):
|
|
61
|
+
raise typer.BadParameter("signing key must be an EC (P-384) private key")
|
|
62
|
+
return key
|
|
63
|
+
if ephemeral:
|
|
64
|
+
typer.echo(
|
|
65
|
+
"WARNING: using an ephemeral signing key — not a persistent operator identity; "
|
|
66
|
+
"for production pass --signing-key.",
|
|
67
|
+
err=True,
|
|
68
|
+
)
|
|
69
|
+
return generate_keypair()
|
|
70
|
+
raise typer.BadParameter("provide --signing-key PATH (PEM) or pass --ephemeral-key")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _load_root_pubkey(path: Path | None) -> ec.EllipticCurvePublicKey | None:
|
|
74
|
+
if path is None:
|
|
75
|
+
return None
|
|
76
|
+
key = serialization.load_pem_public_key(path.read_bytes())
|
|
77
|
+
if not isinstance(key, ec.EllipticCurvePublicKey):
|
|
78
|
+
raise typer.BadParameter("trusted root must be an EC public key")
|
|
79
|
+
return key
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command()
|
|
83
|
+
def scan(
|
|
84
|
+
fixture: Path = typer.Option(
|
|
85
|
+
None, help="Captured RawCapture JSON (dev/sim). Omit to read live hardware."
|
|
86
|
+
),
|
|
87
|
+
out: Path = typer.Option(
|
|
88
|
+
None, "--out", "-o", help="Write the signed bundle JSON here (default: stdout)."
|
|
89
|
+
),
|
|
90
|
+
methodology: str = typer.Option(
|
|
91
|
+
"read-v0", help="Methodology version hash stamped into the bundle."
|
|
92
|
+
),
|
|
93
|
+
signing_key: Path = typer.Option(
|
|
94
|
+
None, help="Operator EC P-384 private key (PEM) to sign the bundle."
|
|
95
|
+
),
|
|
96
|
+
ephemeral_key: bool = typer.Option(
|
|
97
|
+
False, "--ephemeral-key", help="Sign with a throwaway key (dev only)."
|
|
98
|
+
),
|
|
99
|
+
trusted_root: Path = typer.Option(
|
|
100
|
+
None, help="NVIDIA root EC public key (PEM) for attestation."
|
|
101
|
+
),
|
|
102
|
+
expected_vbios: str = typer.Option(None, help="Known-good VBIOS hash for re-flash detection."),
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Read the device (read-only) and emit a signed evidence bundle. Offline."""
|
|
105
|
+
key = _load_signing_key(signing_key, ephemeral_key)
|
|
106
|
+
root_pub = _load_root_pubkey(trusted_root)
|
|
107
|
+
if fixture is not None:
|
|
108
|
+
source = FixtureSource(fixture)
|
|
109
|
+
else: # pragma: no cover - requires a GPU + the [hardware] extra
|
|
110
|
+
from .sources.live import LiveSource
|
|
111
|
+
|
|
112
|
+
source = LiveSource()
|
|
113
|
+
capture = source.capture()
|
|
114
|
+
bundle = build_read_bundle(
|
|
115
|
+
capture,
|
|
116
|
+
signer_key=key,
|
|
117
|
+
agent=AgentInfo(
|
|
118
|
+
name="voltry-probe",
|
|
119
|
+
version=__version__,
|
|
120
|
+
run_mode=RunMode.READ,
|
|
121
|
+
host_arch=platform.machine(),
|
|
122
|
+
),
|
|
123
|
+
methodology_version_hash=methodology,
|
|
124
|
+
trusted_root_public_key=root_pub,
|
|
125
|
+
expected_vbios_hash=expected_vbios,
|
|
126
|
+
signer_label="operator",
|
|
127
|
+
)
|
|
128
|
+
payload = bundle.model_dump_json(indent=2)
|
|
129
|
+
if out is None:
|
|
130
|
+
sys.stdout.write(payload + "\n")
|
|
131
|
+
else:
|
|
132
|
+
out.write_text(payload, encoding="utf-8")
|
|
133
|
+
typer.echo(f"wrote signed bundle: {out}", err=True)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command()
|
|
137
|
+
def cert(
|
|
138
|
+
bundle: Path = typer.Argument(..., help="A signed evidence bundle JSON (from `voltry scan`)."),
|
|
139
|
+
out: Path = typer.Option(
|
|
140
|
+
None, "--out", "-o", help="Write the HTML certificate here (default: stdout)."
|
|
141
|
+
),
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Render a bundle to a self-contained, offline HTML certificate. Offline."""
|
|
144
|
+
try:
|
|
145
|
+
text = bundle.read_text(encoding="utf-8")
|
|
146
|
+
except OSError as exc:
|
|
147
|
+
typer.echo(f"ERROR: bundle file not found: {bundle}", err=True)
|
|
148
|
+
raise typer.Exit(2) from exc
|
|
149
|
+
try:
|
|
150
|
+
model = EvidenceBundle.model_validate_json(text)
|
|
151
|
+
except ValueError as exc:
|
|
152
|
+
typer.echo(f"ERROR: not a valid evidence bundle ({bundle}): {exc}", err=True)
|
|
153
|
+
raise typer.Exit(2) from exc
|
|
154
|
+
# A certificate is authoritative only if the signature actually verifies. Render an
|
|
155
|
+
# unverified bundle with a prominent watermark rather than refusing outright, but never
|
|
156
|
+
# let it look like proof (security §trust).
|
|
157
|
+
verified = model.signature is not None and verify_bundle(model)
|
|
158
|
+
if not verified:
|
|
159
|
+
typer.echo(
|
|
160
|
+
"WARNING: bundle signature is UNVERIFIED — the certificate is a rendered view, "
|
|
161
|
+
"not cryptographic proof.",
|
|
162
|
+
err=True,
|
|
163
|
+
)
|
|
164
|
+
html = render_certificate(model, verified=verified)
|
|
165
|
+
if out is None:
|
|
166
|
+
sys.stdout.write(html + "\n")
|
|
167
|
+
else:
|
|
168
|
+
out.write_text(html, encoding="utf-8")
|
|
169
|
+
typer.echo(f"wrote certificate: {out}", err=True)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command()
|
|
173
|
+
def submit(
|
|
174
|
+
bundle: Path = typer.Argument(..., help="A signed evidence bundle JSON to upload."),
|
|
175
|
+
url: str = typer.Option(..., help="Platform ingest URL."),
|
|
176
|
+
i_consent_to_submit: bool = typer.Option(
|
|
177
|
+
False,
|
|
178
|
+
"--i-consent-to-submit",
|
|
179
|
+
help="Required. Uploading the signed bundle (including raw reads) leaves your premises.",
|
|
180
|
+
),
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Upload a signed bundle to the platform. OPT-IN and SEPARATE from scan/cert (§1.7)."""
|
|
183
|
+
if not i_consent_to_submit:
|
|
184
|
+
typer.echo(
|
|
185
|
+
"ERROR: submission is opt-in and separate from scan/cert. Nothing leaves your "
|
|
186
|
+
"premises without consent. Re-run with --i-consent-to-submit to upload the signed "
|
|
187
|
+
"bundle (which includes its raw reads) to the platform.",
|
|
188
|
+
err=True,
|
|
189
|
+
)
|
|
190
|
+
raise typer.Exit(2)
|
|
191
|
+
try:
|
|
192
|
+
import httpx
|
|
193
|
+
except ImportError as exc: # pragma: no cover - exercised only without the extra
|
|
194
|
+
typer.echo(
|
|
195
|
+
"submit requires the network extra: pip install 'voltry-probe[submit]'", err=True
|
|
196
|
+
)
|
|
197
|
+
raise typer.Exit(3) from exc
|
|
198
|
+
data = bundle.read_text(encoding="utf-8")
|
|
199
|
+
response = httpx.post( # pragma: no cover
|
|
200
|
+
url,
|
|
201
|
+
content=data,
|
|
202
|
+
headers={"content-type": "application/json"},
|
|
203
|
+
timeout=30.0, # bound the consent-gated upload; never hang on a slow endpoint
|
|
204
|
+
)
|
|
205
|
+
typer.echo(f"submitted to {url}: HTTP {response.status_code}") # pragma: no cover
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main() -> None:
|
|
209
|
+
"""Console-script entry point (`voltry`)."""
|
|
210
|
+
app()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__": # pragma: no cover - module run
|
|
214
|
+
main()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Thin adapter over evidence-schema — assembles reader outputs into a signed bundle.
|
|
2
|
+
|
|
3
|
+
This never re-implements the contract; it imports the frozen models + signing from
|
|
4
|
+
evidence-schema and only orchestrates assembly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .builder import build_functional_bundle, build_monitor_bundle, build_read_bundle
|
|
10
|
+
|
|
11
|
+
__all__ = ["build_read_bundle", "build_functional_bundle", "build_monitor_bundle"]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Assemble a Read-mode :class:`EvidenceBundle` from a capture, then sign it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
9
|
+
from evidence_schema import (
|
|
10
|
+
AgentInfo,
|
|
11
|
+
DeterministicGates,
|
|
12
|
+
EvidenceBundle,
|
|
13
|
+
FunctionalBlock,
|
|
14
|
+
GateResult,
|
|
15
|
+
History,
|
|
16
|
+
IdentityBlock,
|
|
17
|
+
MeasuredBlock,
|
|
18
|
+
ProvenanceBlock,
|
|
19
|
+
RawPayload,
|
|
20
|
+
RawReads,
|
|
21
|
+
Tier,
|
|
22
|
+
sign_bundle,
|
|
23
|
+
)
|
|
24
|
+
from evidence_schema.types import UtcDateTime, utcnow
|
|
25
|
+
|
|
26
|
+
from ..attestation import AttestationReport, verify_attestation
|
|
27
|
+
from ..readers import map_dcgm, map_nvml, map_redfish
|
|
28
|
+
from ..sources.base import RawCapture
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _raw_payload(source: str, key: str, payload: object, captured_at: UtcDateTime) -> RawPayload:
|
|
32
|
+
"""Wrap a verbatim source payload as a RawPayload with an integrity hash."""
|
|
33
|
+
content = json.dumps(payload, sort_keys=True, ensure_ascii=False) # not the signing path
|
|
34
|
+
return RawPayload(
|
|
35
|
+
source=source,
|
|
36
|
+
key=key,
|
|
37
|
+
format="json",
|
|
38
|
+
content=content,
|
|
39
|
+
sha256=hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
|
40
|
+
captured_at=captured_at,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_read_bundle(
|
|
45
|
+
capture: RawCapture,
|
|
46
|
+
*,
|
|
47
|
+
signer_key: ec.EllipticCurvePrivateKey,
|
|
48
|
+
agent: AgentInfo,
|
|
49
|
+
methodology_version_hash: str,
|
|
50
|
+
trusted_root_public_key: ec.EllipticCurvePublicKey | None = None,
|
|
51
|
+
expected_vbios_hash: str | None = None,
|
|
52
|
+
signer_label: str | None = "operator",
|
|
53
|
+
created_at: UtcDateTime | None = None,
|
|
54
|
+
functional: FunctionalBlock | None = None,
|
|
55
|
+
tier: Tier = Tier.BRONZE,
|
|
56
|
+
history: History = History.RECONSTRUCTED,
|
|
57
|
+
born_on: UtcDateTime | None = None,
|
|
58
|
+
) -> EvidenceBundle:
|
|
59
|
+
"""Map a capture into a schema-valid, signed bundle.
|
|
60
|
+
|
|
61
|
+
Read mode (defaults) ⇒ Bronze, reconstructed history. Functional mode passes a
|
|
62
|
+
``functional`` block (the functional gates derive from it) with ``tier=SILVER``;
|
|
63
|
+
Monitor mode passes ``tier=GOLD``, ``history=BORN_ON`` and a ``born_on`` timestamp.
|
|
64
|
+
Attestation is verified for real (never stubbed) regardless of mode.
|
|
65
|
+
"""
|
|
66
|
+
when = created_at if created_at is not None else utcnow()
|
|
67
|
+
|
|
68
|
+
nvml_out = map_nvml(capture.nvml)
|
|
69
|
+
dcgm_out = map_dcgm(capture.dcgm)
|
|
70
|
+
redfish_out = map_redfish(capture.redfish)
|
|
71
|
+
|
|
72
|
+
report = AttestationReport.model_validate(capture.attestation) if capture.attestation else None
|
|
73
|
+
att = verify_attestation(
|
|
74
|
+
report,
|
|
75
|
+
trusted_root_public_key=trusted_root_public_key,
|
|
76
|
+
expected_vbios_hash=expected_vbios_hash,
|
|
77
|
+
measured_at=capture.captured_at,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
ecc384_id = (report.device_id if report else None) or nvml_out.gpu_uuid or "UNKNOWN-IDENTITY"
|
|
81
|
+
|
|
82
|
+
identity = IdentityBlock(
|
|
83
|
+
device_part=nvml_out.device_part or nvml_out.device_model or "UNKNOWN",
|
|
84
|
+
device_model=nvml_out.device_model,
|
|
85
|
+
serial=nvml_out.serial or "UNKNOWN",
|
|
86
|
+
ecc384_id=ecc384_id,
|
|
87
|
+
gpu_uuid=nvml_out.gpu_uuid,
|
|
88
|
+
board_id=redfish_out.board_id,
|
|
89
|
+
attestation=att.attestation,
|
|
90
|
+
vbios_version=nvml_out.vbios_version,
|
|
91
|
+
vbios_hash=att.vbios_hash,
|
|
92
|
+
reflash_detected=att.reflash_detected,
|
|
93
|
+
identity_scheme=att.identity_scheme,
|
|
94
|
+
authenticity_confidence=att.authenticity_confidence,
|
|
95
|
+
gates=DeterministicGates(
|
|
96
|
+
authenticity=att.authenticity_gate,
|
|
97
|
+
firmware_vbios=att.firmware_gate,
|
|
98
|
+
# Functional gates derive from the functional block (functional mode); else
|
|
99
|
+
# NOT_ASSESSED (Read/Monitor mode is non-disruptive).
|
|
100
|
+
functional_burnin=functional.burnin_result if functional else GateResult.NOT_ASSESSED,
|
|
101
|
+
sdc_functional=functional.sdc_functional if functional else GateResult.NOT_ASSESSED,
|
|
102
|
+
data_sanitization=(
|
|
103
|
+
functional.sanitization.result if functional else GateResult.NOT_ASSESSED
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
measured = MeasuredBlock(
|
|
109
|
+
ecc=nvml_out.ecc,
|
|
110
|
+
xid=nvml_out.xid,
|
|
111
|
+
pages=nvml_out.pages,
|
|
112
|
+
spare_rows=nvml_out.spare_rows,
|
|
113
|
+
stability=nvml_out.stability,
|
|
114
|
+
thermals=nvml_out.thermals,
|
|
115
|
+
clock_power=nvml_out.clock_power,
|
|
116
|
+
nvlink=dcgm_out.nvlink,
|
|
117
|
+
pcie=dcgm_out.pcie,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
payloads = [_raw_payload("nvml", "readout", capture.nvml, capture.captured_at)]
|
|
121
|
+
if capture.dcgm is not None:
|
|
122
|
+
payloads.append(_raw_payload("dcgm", "telemetry", capture.dcgm, capture.captured_at))
|
|
123
|
+
if capture.redfish is not None:
|
|
124
|
+
payloads.append(_raw_payload("redfish", "system", capture.redfish, capture.captured_at))
|
|
125
|
+
if capture.attestation is not None:
|
|
126
|
+
payloads.append(
|
|
127
|
+
_raw_payload("attestation", "report", capture.attestation, capture.captured_at)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
bundle = EvidenceBundle(
|
|
131
|
+
created_at=when,
|
|
132
|
+
agent=agent,
|
|
133
|
+
methodology_version_hash=methodology_version_hash,
|
|
134
|
+
calibration_snapshot_id=None, # Phase 0: no modeled fields.
|
|
135
|
+
identity=identity,
|
|
136
|
+
measured=measured,
|
|
137
|
+
functional=functional,
|
|
138
|
+
raw_reads=RawReads(payloads=payloads),
|
|
139
|
+
environment=None, # No facility instrumentation ⇒ exposure Not Assessed.
|
|
140
|
+
provenance=ProvenanceBlock(
|
|
141
|
+
tier=tier,
|
|
142
|
+
history=history,
|
|
143
|
+
# Born-on (Monitor) implies a continuous chain; reconstructed implies gaps.
|
|
144
|
+
chain_gaps=history is not History.BORN_ON,
|
|
145
|
+
exposure_assessed=False,
|
|
146
|
+
born_on=born_on,
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
return sign_bundle(bundle, signer_key, signer=signer_label)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def build_functional_bundle(
|
|
153
|
+
capture: RawCapture, *, functional: FunctionalBlock, **kwargs: object
|
|
154
|
+
) -> EvidenceBundle:
|
|
155
|
+
"""Functional mode (Silver): a bundle carrying the drained-unit functional results."""
|
|
156
|
+
return build_read_bundle(capture, functional=functional, tier=Tier.SILVER, **kwargs) # type: ignore[arg-type]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def build_monitor_bundle(
|
|
160
|
+
capture: RawCapture, *, born_on: UtcDateTime, **kwargs: object
|
|
161
|
+
) -> EvidenceBundle:
|
|
162
|
+
"""Monitor mode (Gold): a born-on bundle from continuous read-only capture."""
|
|
163
|
+
return build_read_bundle(
|
|
164
|
+
capture, tier=Tier.GOLD, history=History.BORN_ON, born_on=born_on, **kwargs # type: ignore[arg-type]
|
|
165
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Functional mode (Silver) — the ONLY package where device-mutating calls may live.
|
|
2
|
+
|
|
3
|
+
DCGM r4 diagnostics + burn-in + the SDC functional test stress and drain the GPU, so they
|
|
4
|
+
run exclusively here and only behind an explicit drain-consent flag. Everything outside
|
|
5
|
+
``voltry_probe/functional/`` is read-only (invariant §1.1), enforced by the read-only
|
|
6
|
+
invariant test (which excludes this package) and the forbidden-patterns hook.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .runner import DrainConsentError, FunctionalRunner
|
|
12
|
+
|
|
13
|
+
__all__ = ["FunctionalRunner", "DrainConsentError"]
|