Files
mpc/pkg/attestation/nvidia/gpu_report.go
zeekay 1996f1adc4 v0.61+v0.62: scheduler trust gate + NVIDIA attestation primitives
v0.61 — wire the scheduler workload-policy gate into the MPC SignGate.
SignGate gains optional Scheduler + Worker fields; AuthorizeSign now
runs scheduler.Eligible(worker, ci.PolicyID) BEFORE the policy
verifier and short-circuits on insufficient trust posture. Existing
callers that pass nil Scheduler are unaffected.

  pkg/scheduler/policy_gate.go     — TrustMode/IOLevel enums,
                                     WorkloadPolicy.Eligible(Worker)
  pkg/scheduler/service_drain.go   — AuthorizeDrain + AuthorizeSession
  pkg/mpc/sign_orchestrator.go     — Scheduler/Worker fields,
                                     WithScheduler(...), two-stage gate

v0.62 — NVIDIA Confidential-Compute attestation primitives. Clean-room
parsers + verifiers against the publicly documented nvTrust schema
(Apache-2 derived); no NVIDIA proprietary code vendored.

  pkg/attestation/nvidia/gpu_report.go   — GPU evidence-report parser
  pkg/attestation/nvidia/nras_client.go  — NRAS HTTP client + JWS verify
  pkg/attestation/nvidia/rim.go          — RIM signature + value match
  pkg/attestation/nvidia/composite.go    — VerifiedBundle (CPU+GPU bind)

Tests (57 passing, -race clean):
  pkg/scheduler                        — 11 (truth-table, drain,
                                          session, orchestrator
                                          integration)
  pkg/attestation/nvidia               — 46 (parser, RIM Ed25519/ECDSA/
                                          RSA-PSS, NRAS happy/error/
                                          signature paths, ECDSA JOSE)
  pkg/attestation/nvidia (hwattest)    — 3, env-skipped without real GPU

Sibling v0.60 reconciliation: pkg/registry + pkg/attestation already
land WorkerCapability + CompositeAttestation envelope using lattice
trust types. nvidia.VerifiedBundle is renamed (was Composite-) to
avoid collision with pkg/attestation.CompositeAttestation; the
canonical envelope wraps the verified blobs this package emits.

Residual: lattice/v7/types is not yet published, so pkg/registry +
pkg/attestation cannot compile against luxfi/[email protected]; the
sibling v0.60 work tracks that. pkg/policy still has a pre-existing
unresolved import of luxfi/fhe/threshold (should be /pkg/threshold);
sign_orchestrator_scheduler_test.go is ready to run as soon as that
upstream issue is fixed by its owner.
2025-12-28 16:53:46 -08:00

150 lines
5.3 KiB
Go

// GPU evidence-report parser. The NVIDIA Confidential Compute stack
// produces a JSON-encoded "evidence" envelope that the host forwards
// to the NVIDIA Remote Attestation Service (NRAS). The envelope wraps
// a binary attestation report signed by the GPU's confidential-compute
// firmware plus the GPU's certificate chain.
//
// This parser is a clean-room implementation of the publicly
// documented schema (see nvTrust, NVIDIA Apache-2 release). We do NOT
// vendor NVIDIA proprietary code. Field names mirror the public spec
// so downstream code reads naturally against the documentation.
package nvidia
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
)
// Errors returned by the parser.
var (
ErrReportEmpty = errors.New("nvidia/report: empty input")
ErrReportBadJSON = errors.New("nvidia/report: bad JSON envelope")
ErrReportBadVersion = errors.New("nvidia/report: unsupported evidence_version")
ErrReportMissingNonce = errors.New("nvidia/report: missing nonce")
ErrReportBadNonce = errors.New("nvidia/report: nonce must be 32 bytes hex")
ErrReportMissingArch = errors.New("nvidia/report: missing architecture")
ErrReportUnknownArch = errors.New("nvidia/report: unknown architecture")
ErrReportNoMeasurements = errors.New("nvidia/report: no measurements present")
)
// SupportedEvidenceVersions enumerates the report-envelope versions
// this parser understands. Bumped when NVIDIA ships a new schema.
var SupportedEvidenceVersions = map[string]bool{
"2.0": true, // Hopper H100 GA
"2.1": true, // Hopper post-launch fixes
"3.0": true, // Blackwell B100/B200
}
// SupportedArchitectures enumerates the GPU architectures we recognise.
var SupportedArchitectures = map[string]bool{
"Hopper": true,
"Blackwell": true,
}
// Measurement is one entry in the GPU's measurement list. PCRIndex
// follows the spec's natural numbering. Value is the raw measurement
// bytes (typically a 48-byte SHA-384 digest).
type Measurement struct {
PCRIndex uint8 `json:"pcr_index"`
Name string `json:"name"`
Value []byte `json:"-"` // decoded from ValueHex
ValueHex string `json:"value"`
}
// rawReport is the on-the-wire envelope. Field names are the
// canonical lower-snake spelling used by the NVIDIA evidence schema.
type rawReport struct {
EvidenceVersion string `json:"evidence_version"`
GPUUUID string `json:"gpu_uuid"`
Architecture string `json:"architecture"`
DriverVersion string `json:"driver_version"`
VBIOSVersion string `json:"vbios_version"`
NonceHex string `json:"nonce"`
Measurements []Measurement `json:"measurements"`
CertChain []string `json:"cert_chain"` // PEM blocks
Quote string `json:"attestation_quote"` // base64 binary
NVSwitchPresent bool `json:"nvswitch_present"`
}
// GPUReport is the parsed, validated GPU evidence report — every field
// downstream verification logic needs, with no JSON-specific shape.
type GPUReport struct {
EvidenceVersion string
UUID string
Architecture string
DriverVersion string
VBIOSVersion string
Nonce [32]byte
Measurements []Measurement
CertChain []string // PEM blocks (verified at NRAS, kept for audit)
QuoteB64 string
NVSwitchPresent bool
}
// ParseGPUReport reads the JSON envelope and returns a validated
// GPUReport. All structural problems are caught here so downstream
// consumers (NRAS client, RIM matcher, composite) deal only with
// well-formed input.
func ParseGPUReport(data []byte) (*GPUReport, error) {
if len(data) == 0 {
return nil, ErrReportEmpty
}
var r rawReport
if err := json.Unmarshal(data, &r); err != nil {
return nil, fmt.Errorf("%w: %v", ErrReportBadJSON, err)
}
if !SupportedEvidenceVersions[r.EvidenceVersion] {
return nil, fmt.Errorf("%w: %q", ErrReportBadVersion, r.EvidenceVersion)
}
if r.Architecture == "" {
return nil, ErrReportMissingArch
}
if !SupportedArchitectures[r.Architecture] {
return nil, fmt.Errorf("%w: %q", ErrReportUnknownArch, r.Architecture)
}
if r.NonceHex == "" {
return nil, ErrReportMissingNonce
}
nonceBytes, err := hex.DecodeString(strings.TrimPrefix(r.NonceHex, "0x"))
if err != nil || len(nonceBytes) != 32 {
return nil, ErrReportBadNonce
}
if len(r.Measurements) == 0 {
return nil, ErrReportNoMeasurements
}
for i := range r.Measurements {
raw, err := hex.DecodeString(strings.TrimPrefix(r.Measurements[i].ValueHex, "0x"))
if err != nil {
return nil, fmt.Errorf("nvidia/report: bad measurement[%d] hex: %v", i, err)
}
r.Measurements[i].Value = raw
}
rep := &GPUReport{
EvidenceVersion: r.EvidenceVersion,
UUID: r.GPUUUID,
Architecture: r.Architecture,
DriverVersion: r.DriverVersion,
VBIOSVersion: r.VBIOSVersion,
Measurements: r.Measurements,
CertChain: r.CertChain,
QuoteB64: r.Quote,
NVSwitchPresent: r.NVSwitchPresent,
}
copy(rep.Nonce[:], nonceBytes)
return rep, nil
}
// MeasurementMap returns measurements indexed by Name for fast lookup
// during RIM matching. Names are case-sensitive per spec.
func (r *GPUReport) MeasurementMap() map[string]Measurement {
out := make(map[string]Measurement, len(r.Measurements))
for _, m := range r.Measurements {
out[m.Name] = m
}
return out
}