Files
cc/attest/nvidia/devicechain_test.go
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

163 lines
6.1 KiB
Go

// Unit tests for device-chain parsing/validation and the EAT x5c branch.
package nvidia
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"math/big"
"testing"
"time"
)
func testNow() time.Time { return time.Date(2026, 6, 27, 12, 0, 0, 0, time.UTC) }
func mkTestCert(t *testing.T, tmpl, parent *x509.Certificate, pub, signer any) *x509.Certificate {
t.Helper()
der, err := x509.CreateCertificate(rand.Reader, tmpl, parent, pub, signer)
if err != nil {
t.Fatalf("create: %v", err)
}
c, err := x509.ParseCertificate(der)
if err != nil {
t.Fatalf("parse: %v", err)
}
return c
}
func toPEM(c *x509.Certificate) string {
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw}))
}
// chain3 builds root -> intermediate -> leaf (P-384).
func chain3(t *testing.T, now time.Time) (root, interm, leaf *x509.Certificate) {
t.Helper()
rk, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
ik, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
lk, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
rt := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "root"},
NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), IsCA: true,
KeyUsage: x509.KeyUsageCertSign, BasicConstraintsValid: true}
root = mkTestCert(t, rt, rt, rk.Public(), rk)
it := &x509.Certificate{SerialNumber: big.NewInt(2), Subject: pkix.Name{CommonName: "interm"},
NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), IsCA: true,
KeyUsage: x509.KeyUsageCertSign, BasicConstraintsValid: true}
interm = mkTestCert(t, it, root, ik.Public(), rk)
lt := &x509.Certificate{SerialNumber: big.NewInt(3), Subject: pkix.Name{CommonName: "leaf"},
NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature}
leaf = mkTestCert(t, lt, interm, lk.Public(), ik)
return
}
func TestParseCertChainPEM_Errors(t *testing.T) {
if _, err := ParseCertChainPEM(nil); !errors.Is(err, ErrChainEmpty) {
t.Fatalf("nil: %v", err)
}
if _, err := ParseCertChainPEM([]string{"not pem"}); !errors.Is(err, ErrChainEmpty) {
t.Fatalf("garbage: %v", err)
}
}
func TestVerifyDeviceChain_Valid(t *testing.T) {
now := testNow()
root, interm, leaf := chain3(t, now)
chain, err := ParseCertChainPEM([]string{toPEM(leaf), toPEM(interm)})
if err != nil {
t.Fatalf("parse chain: %v", err)
}
pool := x509.NewCertPool()
pool.AddCert(root)
got, err := VerifyDeviceChain(chain, pool, now)
if err != nil {
t.Fatalf("verify: %v", err)
}
if got.Subject.CommonName != "leaf" {
t.Fatalf("leaf = %q", got.Subject.CommonName)
}
}
func TestVerifyDeviceChain_Untrusted(t *testing.T) {
now := testNow()
_, interm, leaf := chain3(t, now)
chain, _ := ParseCertChainPEM([]string{toPEM(leaf), toPEM(interm)})
// Empty pool -> leaf cannot chain to any pinned root.
if _, err := VerifyDeviceChain(chain, x509.NewCertPool(), now); !errors.Is(err, ErrChainUntrusted) {
t.Fatalf("err = %v, want ErrChainUntrusted", err)
}
if _, err := VerifyDeviceChain(chain, nil, now); !errors.Is(err, ErrChainNoRoots) {
t.Fatalf("nil roots: err = %v, want ErrChainNoRoots", err)
}
}
func TestVerifyDeviceChain_Expired(t *testing.T) {
now := testNow()
root, interm, leaf := chain3(t, now)
chain, _ := ParseCertChainPEM([]string{toPEM(leaf), toPEM(interm)})
pool := x509.NewCertPool()
pool.AddCert(root)
// Two hours later, the 1-hour-validity certs are expired.
if _, err := VerifyDeviceChain(chain, pool, now.Add(2*time.Hour)); !errors.Is(err, ErrChainUntrusted) {
t.Fatalf("err = %v, want ErrChainUntrusted (expired)", err)
}
}
// EAT x5c branch: a token whose x5c chains to the pinned NRAS root pool is
// accepted via the leaf key (no kid pin needed).
func TestVerifyEAT_X5CChain(t *testing.T) {
now := testNow()
// The leaf key signs the JWS; build a dedicated NRAS chain with a known
// leaf key (root -> interm -> leaf) and pin the NRAS root.
lk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
lt := &x509.Certificate{SerialNumber: big.NewInt(4), Subject: pkix.Name{CommonName: "nras-leaf"},
NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature}
ik, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
it := &x509.Certificate{SerialNumber: big.NewInt(5), Subject: pkix.Name{CommonName: "nras-interm"},
NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), IsCA: true,
KeyUsage: x509.KeyUsageCertSign, BasicConstraintsValid: true}
rk, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
rt := &x509.Certificate{SerialNumber: big.NewInt(6), Subject: pkix.Name{CommonName: "nras-root"},
NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), IsCA: true,
KeyUsage: x509.KeyUsageCertSign, BasicConstraintsValid: true}
nrasRoot := mkTestCert(t, rt, rt, rk.Public(), rk)
nrasInterm := mkTestCert(t, it, nrasRoot, ik.Public(), rk)
nrasLeaf := mkTestCert(t, lt, nrasInterm, lk.Public(), ik)
hdr := map[string]any{
"alg": "ES256",
"x5c": []string{
base64.StdEncoding.EncodeToString(nrasLeaf.Raw),
base64.StdEncoding.EncodeToString(nrasInterm.Raw),
},
}
claims := map[string]any{"exp": now.Add(time.Hour).Unix(), "x-nvidia-overall-att-result": true}
hb, _ := json.Marshal(hdr)
cb, _ := json.Marshal(claims)
signedInput := base64.RawURLEncoding.EncodeToString(hb) + "." + base64.RawURLEncoding.EncodeToString(cb)
sum := sha256.Sum256([]byte(signedInput))
r, s, _ := ecdsa.Sign(rand.Reader, lk, sum[:])
token := signedInput + "." + base64.RawURLEncoding.EncodeToString(joseConcat(r, s, 32))
pool := x509.NewCertPool()
pool.AddCert(nrasRoot)
res, err := VerifyEAT(token, nil, pool, nil, now)
if err != nil {
t.Fatalf("eat x5c verify: %v", err)
}
if res.OverallResult != "true" {
t.Fatalf("result = %q", res.OverallResult)
}
// A token whose x5c does NOT chain to the pinned pool is refused.
if _, err := VerifyEAT(token, nil, x509.NewCertPool(), nil, now); !errors.Is(err, ErrEATChainUntrusted) {
t.Fatalf("untrusted x5c: err = %v, want ErrEATChainUntrusted", err)
}
}