Files
zeekay c84126eaa1 feat(attest/nvtrust): real NVIDIA GPU CC verification — SPDM report sig + device-chain + RIM, ModeLocal/ModeNRAS
The prior KindNVTrust local path was cryptographically hollow: it matched
PLAINTEXT JSON measurements against the (public) RIM and never parsed or
verified the GPU-signed SPDM report or the device certificate chain — a
host could echo golden values and pass. This makes it production-real.

ModeLocal (cloud-free, full hardware verify), in order, refusing on any
failure:
  1. chain the GPU device leaf cert to an operator-pinned NVIDIA device-
     identity root (no insecure mode; empty root => refuse);
  2. verify the SPDM MEASUREMENTS (0x60) response signature over the
     DSP0274 transcript (request || response[:-sig]) with the leaf key,
     SPDM 1.1 and 1.2, ECDSA P-384/P-256 + RSA-PSS, with a strict
     algo<->key-type binding (substitution defense);
  3. extract measurements from the SIGNED record and match them BY INDEX
     to the NVIDIA-signed RIM (completeness enforced — closes the drop-a-
     bad-measurement attack);
  4. bind the SIGNED requester nonce to the caller's freshness challenge.

ModeNRAS (remote): verify an NVIDIA NRAS EAT/JWT — JWS signature against
pinned signer keys (or x5c chain to a pinned NRAS root), exp/nbf freshness,
eat_nonce binding, positive overall-result. No network in Verify (the POST
stays in NRASClient).

GPU measurement -> VerifiedReport.Measurement (RIM root / EAT root); trust-
score/tier stays caller policy on top. ChipID is the trusted leaf serial,
not the untrusted envelope UUID. Trust anchors are operator-pinned via
NewNVTrust options, never from evidence; the zero value (Dispatch) is fail-
closed. No new dependencies (stdlib crypto only).

New: attest/nvidia/{spdm,devicechain,eat}.go. Extended: gpu_report.go
(spdm_request/response/asym_algo), rim.go (index-keyed MatchSPDM). 30 new
tests: self-consistent SPDM+chain+RIM vectors (1.1 and 1.2), NRAS EAT
vectors, and tamper rejection (bad SPDM sig, tampered signed record, wrong
device root, RIM mismatch, RIM sig tamper, nonce mismatch, missing SPDM,
bad/expired/negative EAT). go build + go test ./... green (race-clean).
2026-06-27 13:51:24 -07:00

122 lines
4.3 KiB
Go

// NVIDIA GPU device-identity certificate chain verification.
//
// The public key that signs the SPDM MEASUREMENTS response is the GPU's
// attestation key. By itself a valid SPDM signature only proves "some key
// signed these measurements". To prove the key belongs to a genuine NVIDIA
// GPU we verify the GPU's certificate chain to an operator-pinned NVIDIA
// device-identity root CA:
//
// leaf (GPU attestation cert, ECDSA P-384)
// -> NVIDIA GPU attestation intermediate(s)
// -> NVIDIA device-identity root CA <-- operator pins this
//
// The root is supplied by the operator (WithNVTrustDeviceRoots), never
// taken from the evidence — exactly as AMD's ARK is pinned for SEV-SNP and
// the RIM signing key is pinned for the RIM. There is no insecure mode: an
// empty root pool means the local SPDM path is refused.
//
// Clean-room: standard X.509 path validation against an operator pool. No
// NVIDIA root certificate is vendored; production wiring loads NVIDIA's
// published device-identity root PEM into the pool at startup.
package nvidia
import (
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"time"
)
// Errors returned by chain verification.
var (
ErrChainEmpty = errors.New("nvidia/chain: empty certificate chain")
ErrChainBadPEM = errors.New("nvidia/chain: certificate is not valid PEM/DER")
ErrChainNoRoots = errors.New("nvidia/chain: no device-identity roots configured")
ErrChainUntrusted = errors.New("nvidia/chain: leaf does not chain to a pinned root")
ErrChainLeafKeyType = errors.New("nvidia/chain: leaf public key type unsupported")
)
// ParseCertChainPEM decodes a leaf-first list of PEM certificate blocks
// into parsed certificates. Each element may itself contain multiple
// concatenated PEM blocks (the typical NVIDIA evidence packs the whole
// chain in one string); all blocks are flattened, preserving order.
func ParseCertChainPEM(pems []string) ([]*x509.Certificate, error) {
if len(pems) == 0 {
return nil, ErrChainEmpty
}
var certs []*x509.Certificate
for _, p := range pems {
rest := []byte(p)
for {
var block *pem.Block
block, rest = pem.Decode(rest)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
continue
}
c, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrChainBadPEM, err)
}
certs = append(certs, c)
}
}
if len(certs) == 0 {
return nil, ErrChainEmpty
}
return certs, nil
}
// VerifyDeviceChain validates a leaf-first certificate chain to the pinned
// device-identity roots and returns the verified leaf. chain[0] is the GPU
// attestation leaf; chain[1:] are intermediates. The root is NOT expected
// in the chain — it is supplied via roots and pinned by the operator.
//
// ExtKeyUsageAny is used because NVIDIA GPU attestation leaves do not
// carry a TLS-server EKU; path, validity window, and signatures are still
// fully checked against the pinned root.
func VerifyDeviceChain(chain []*x509.Certificate, roots *x509.CertPool, now time.Time) (*x509.Certificate, error) {
if len(chain) == 0 {
return nil, ErrChainEmpty
}
if roots == nil {
return nil, ErrChainNoRoots
}
// CertPool offers no public emptiness check; the operator-supplied pool
// is assumed non-empty when non-nil. An empty pool simply fails the
// Verify below with ErrChainUntrusted, which is the correct refusal.
intermediates := x509.NewCertPool()
for _, ic := range chain[1:] {
intermediates.AddCert(ic)
}
leaf := chain[0]
opts := x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
CurrentTime: now,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}
if _, err := leaf.Verify(opts); err != nil {
return nil, fmt.Errorf("%w: %v", ErrChainUntrusted, err)
}
return leaf, nil
}
// LeafSPDMKey returns the leaf's public key in the form the SPDM verifier
// consumes, rejecting key types no NVIDIA GPU attestation key uses.
func LeafSPDMKey(leaf *x509.Certificate) (crypto.PublicKey, error) {
switch leaf.PublicKey.(type) {
case interface{ Equal(crypto.PublicKey) bool }:
// *ecdsa.PublicKey and *rsa.PublicKey both satisfy this; the SPDM
// verifier does the final algo<->key-type binding.
return leaf.PublicKey, nil
default:
return nil, fmt.Errorf("%w: %T", ErrChainLeafKeyType, leaf.PublicKey)
}
}