Merge remote-tracking branch 'origin/feat/kind-nvtrust-real'

This commit is contained in:
2026-06-27 14:07:48 -07:00
10 changed files with 2243 additions and 202 deletions
+121
View File
@@ -0,0 +1,121 @@
// 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)
}
}
+162
View File
@@ -0,0 +1,162 @@
// 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)
}
}
+276
View File
@@ -0,0 +1,276 @@
// NRAS EAT (Entity Attestation Token) verification — the remote NVIDIA
// trust path.
//
// In the NRAS deployment model the host POSTs GPU evidence to the NVIDIA
// Remote Attestation Service, which performs the SPDM + device-chain
// verification on its side and returns a signed EAT: a JWS-compact JWT
// whose claims are NVIDIA's attestation verdict (overall result, per-
// measurement results, GPU identity, a freshness nonce). The relying party
// trusts NVIDIA's cloud and only has to verify the token.
//
// VerifyEAT performs exactly that, with no network call (the HTTP fetch is
// NRASClient's job — separation of concerns):
//
// - signature: JWS verified against operator-pinned NRAS signer keys
// (by kid) with a strict alg<->key-type binding; OR, when the token
// carries an x5c header and an NRAS root pool is pinned, the x5c chain
// is validated to that root and the leaf key verifies the JWS.
// - chain: the x5c path anchors the signer to NVIDIA's NRAS root CA.
// - claims: exp/nbf freshness, eat_nonce binding to the caller's
// challenge, and (when present) the overall attestation result must
// be positive.
//
// Clean-room: no NVIDIA keys are vendored; the NRAS signer key / root CA
// is operator-pinned, exactly like the local device root.
package nvidia
import (
"crypto"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
)
// Errors returned by EAT verification.
var (
ErrEATBadJWT = errors.New("nvidia/eat: token is not a valid JWS compact serialization")
ErrEATBadSignature = errors.New("nvidia/eat: token signature verification failed")
ErrEATNoSigner = errors.New("nvidia/eat: no signer key (kid not pinned and no verifiable x5c)")
ErrEATExpired = errors.New("nvidia/eat: token expired")
ErrEATNotYetValid = errors.New("nvidia/eat: token not yet valid (nbf in the future)")
ErrEATNonceMismatch = errors.New("nvidia/eat: eat_nonce does not match challenge")
ErrEATResultNegative = errors.New("nvidia/eat: NVIDIA overall attestation result is negative")
ErrEATChainUntrusted = errors.New("nvidia/eat: x5c chain does not anchor to a pinned NRAS root")
)
// EATResult is the verified projection of an NRAS EAT.
type EATResult struct {
SignerKeyID string // kid that verified the token (or x5c leaf subject)
Nonce [32]byte // eat_nonce, when present
HasNonce bool // whether a 32-byte nonce claim was present
Measurement []byte // measurement root (see note in VerifyEAT)
OverallResult string // x-nvidia-overall-att-result / measres, when present
IssuedAt time.Time // iat
NotBefore time.Time // nbf
ExpiresAt time.Time // exp
Claims map[string]json.RawMessage // full claim set, for audit/policy
}
// VerifyEAT verifies an NRAS-issued EAT token.
//
// roots are the pinned NRAS signer public keys (looked up by the JWS kid).
// rootPool, when non-nil, pins the NRAS root CA so a token carrying an x5c
// header is anchored by chain validation (the leaf key then verifies the
// JWS). challenge, when non-nil, must equal the token's eat_nonce. now is
// the verification clock.
//
// Measurement mapping: NVIDIA's EAT expresses measurement results across
// many submodule claims rather than a single root. VerifyEAT surfaces an
// explicit measurement-root claim when present ("x-nvidia-gpu-measurements-
// root" hex) and otherwise binds Measurement to sha256(token) — a stable,
// signature-covered root the caller can pin. The exact production claim
// name is documented NVIDIA collateral; callers needing a specific
// submeasurement read it from Claims.
func VerifyEAT(token string, roots []TrustRoot, rootPool *x509.CertPool, challenge *[32]byte, now time.Time) (*EATResult, error) {
hdrBytes, payload, sig, signedInput, err := splitJWS(token)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrEATBadJWT, err)
}
var hdr struct {
Alg string `json:"alg"`
Kid string `json:"kid"`
X5c []string `json:"x5c"`
}
if err := json.Unmarshal(hdrBytes, &hdr); err != nil {
return nil, fmt.Errorf("%w: header: %v", ErrEATBadJWT, err)
}
signerPub, signerID, err := eatSignerKey(hdr.Kid, hdr.X5c, roots, rootPool, now)
if err != nil {
return nil, err
}
if err := verifyJWS(signerPub, hdr.Alg, signedInput, sig); err != nil {
return nil, fmt.Errorf("%w: %v", ErrEATBadSignature, err)
}
// Decode claims twice: once into known fields, once as a raw map for
// audit / policy on top.
var claims map[string]json.RawMessage
if err := json.Unmarshal(payload, &claims); err != nil {
return nil, fmt.Errorf("%w: claims: %v", ErrEATBadJWT, err)
}
res := &EATResult{SignerKeyID: signerID, Claims: claims}
if t, ok := eatUnixClaim(claims, "iat"); ok {
res.IssuedAt = t
}
if t, ok := eatUnixClaim(claims, "nbf"); ok {
res.NotBefore = t
if now.Before(t) {
return nil, fmt.Errorf("%w: nbf=%s now=%s", ErrEATNotYetValid, t, now.UTC())
}
}
if t, ok := eatUnixClaim(claims, "exp"); ok {
res.ExpiresAt = t
if now.After(t) {
return nil, fmt.Errorf("%w: exp=%s now=%s", ErrEATExpired, t, now.UTC())
}
}
// Nonce binding. NVIDIA uses "eat_nonce"; accept "nonce" as an alias.
if nb, ok := eatHexClaim(claims, "eat_nonce", "nonce"); ok {
if len(nb) == 32 {
copy(res.Nonce[:], nb)
res.HasNonce = true
}
if challenge != nil {
if len(nb) != 32 || !bytesEqual(nb, challenge[:]) {
return nil, ErrEATNonceMismatch
}
}
} else if challenge != nil {
// A challenge was demanded but the token carries no nonce.
return nil, ErrEATNonceMismatch
}
// Overall result, when present, must be positive.
if r, ok := eatResultClaim(claims); ok {
res.OverallResult = r
if !eatResultPositive(r) {
return nil, fmt.Errorf("%w: %q", ErrEATResultNegative, r)
}
}
// Measurement root: explicit claim if present, else token-bound.
if mb, ok := eatHexClaim(claims, "x-nvidia-gpu-measurements-root", "measurement_root"); ok {
res.Measurement = mb
} else {
h := sha256.Sum256([]byte(token))
res.Measurement = h[:]
}
return res, nil
}
// eatSignerKey resolves the public key that must verify the JWS: by pinned
// kid, or by validating an x5c chain to the pinned NRAS root pool.
func eatSignerKey(kid string, x5c []string, roots []TrustRoot, rootPool *x509.CertPool, now time.Time) (crypto.PublicKey, string, error) {
// Prefer the pinned-kid path: deterministic, no chain processing.
if kid != "" {
if root, ok := findTrustRoot(roots, kid); ok {
return root.Public, root.KeyID, nil
}
}
// x5c path: anchor to the NRAS root pool, use the leaf key.
if len(x5c) > 0 && rootPool != nil {
chain, err := parseX5C(x5c)
if err != nil {
return nil, "", err
}
intermediates := x509.NewCertPool()
for _, ic := range chain[1:] {
intermediates.AddCert(ic)
}
leaf := chain[0]
if _, err := leaf.Verify(x509.VerifyOptions{
Roots: rootPool,
Intermediates: intermediates,
CurrentTime: now,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}); err != nil {
return nil, "", fmt.Errorf("%w: %v", ErrEATChainUntrusted, err)
}
return leaf.PublicKey, leaf.Subject.String(), nil
}
return nil, "", ErrEATNoSigner
}
// parseX5C parses an RFC 7515 x5c header (array of base64-STD DER certs,
// leaf first).
func parseX5C(x5c []string) ([]*x509.Certificate, error) {
out := make([]*x509.Certificate, 0, len(x5c))
for i, b64 := range x5c {
der, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
return nil, fmt.Errorf("%w: x5c[%d]: %v", ErrEATChainUntrusted, i, err)
}
c, err := x509.ParseCertificate(der)
if err != nil {
return nil, fmt.Errorf("%w: x5c[%d]: %v", ErrEATChainUntrusted, i, err)
}
out = append(out, c)
}
if len(out) == 0 {
return nil, ErrEATChainUntrusted
}
return out, nil
}
func eatUnixClaim(claims map[string]json.RawMessage, name string) (time.Time, bool) {
raw, ok := claims[name]
if !ok {
return time.Time{}, false
}
var n int64
if err := json.Unmarshal(raw, &n); err != nil || n <= 0 {
return time.Time{}, false
}
return time.Unix(n, 0).UTC(), true
}
// eatHexClaim returns the first present claim among names, hex-decoded.
func eatHexClaim(claims map[string]json.RawMessage, names ...string) ([]byte, bool) {
for _, name := range names {
raw, ok := claims[name]
if !ok {
continue
}
var s string
if err := json.Unmarshal(raw, &s); err != nil {
continue
}
b, err := hex.DecodeString(strings.TrimPrefix(s, "0x"))
if err != nil {
continue
}
return b, true
}
return nil, false
}
// eatResultClaim extracts NVIDIA's overall attestation result, accepting a
// boolean or a string verdict under a few documented claim names.
func eatResultClaim(claims map[string]json.RawMessage) (string, bool) {
for _, name := range []string{"x-nvidia-overall-att-result", "measres", "overall_result"} {
raw, ok := claims[name]
if !ok {
continue
}
var b bool
if err := json.Unmarshal(raw, &b); err == nil {
if b {
return "true", true
}
return "false", true
}
var s string
if err := json.Unmarshal(raw, &s); err == nil {
return s, true
}
}
return "", false
}
func eatResultPositive(r string) bool {
switch strings.ToLower(strings.TrimSpace(r)) {
case "true", "success", "comparison-successful", "pass", "passed":
return true
default:
return false
}
}
+52 -6
View File
@@ -11,6 +11,7 @@
package nvidia
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
@@ -28,6 +29,7 @@ var (
ErrReportMissingArch = errors.New("nvidia/report: missing architecture")
ErrReportUnknownArch = errors.New("nvidia/report: unknown architecture")
ErrReportNoMeasurements = errors.New("nvidia/report: no measurements present")
ErrReportBadSPDM = errors.New("nvidia/report: malformed SPDM request/response encoding")
)
// SupportedEvidenceVersions enumerates the report-envelope versions
@@ -64,8 +66,11 @@ type rawReport struct {
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
CertChain []string `json:"cert_chain"` // PEM blocks, leaf first
Quote string `json:"attestation_quote"` // legacy alias for spdm_response
SPDMRequest string `json:"spdm_request"` // base64: GET_MEASUREMENTS (0xE0)
SPDMResponse string `json:"spdm_response"` // base64: signed MEASUREMENTS (0x60)
SPDMAsymAlgo string `json:"spdm_asym_algo"` // e.g. "ecdsa-p384-sha384"
NVSwitchPresent bool `json:"nvswitch_present"`
}
@@ -78,9 +83,19 @@ type GPUReport struct {
DriverVersion string
VBIOSVersion string
Nonce [32]byte
Measurements []Measurement
CertChain []string // PEM blocks (verified at NRAS, kept for audit)
QuoteB64 string
Measurements []Measurement // CLAIMED measurements (untrusted labels); the
// trusted values come from the signed SPDM record, not from here.
CertChain []string // leaf-first PEM blocks; chained to the pinned NVIDIA root
// SPDMRequest / SPDMResponse are the binary SPDM GET_MEASUREMENTS (0xE0)
// request and signed MEASUREMENTS (0x60) response. These ARE the GPU's
// cryptographic evidence — the local verifier checks the response
// signature with the leaf key and extracts measurements from the signed
// record. Empty for an NRAS-only envelope.
SPDMRequest []byte
SPDMResponse []byte
SPDMAsymAlgo string // negotiated signature algorithm name (may be empty -> P-384)
NVSwitchPresent bool
}
@@ -123,6 +138,23 @@ func ParseGPUReport(data []byte) (*GPUReport, error) {
r.Measurements[i].Value = raw
}
// The signed SPDM response is carried under "spdm_response"; the legacy
// "attestation_quote" key is accepted as an alias. Decode both SPDM
// fields if present (they are optional at parse time — the local
// verifier enforces their presence, the NRAS path ignores them).
respB64 := r.SPDMResponse
if respB64 == "" {
respB64 = r.Quote
}
spdmResp, err := decodeOptionalB64(respB64)
if err != nil {
return nil, fmt.Errorf("%w: spdm_response: %v", ErrReportBadSPDM, err)
}
spdmReq, err := decodeOptionalB64(r.SPDMRequest)
if err != nil {
return nil, fmt.Errorf("%w: spdm_request: %v", ErrReportBadSPDM, err)
}
rep := &GPUReport{
EvidenceVersion: r.EvidenceVersion,
UUID: r.GPUUUID,
@@ -131,13 +163,27 @@ func ParseGPUReport(data []byte) (*GPUReport, error) {
VBIOSVersion: r.VBIOSVersion,
Measurements: r.Measurements,
CertChain: r.CertChain,
QuoteB64: r.Quote,
SPDMRequest: spdmReq,
SPDMResponse: spdmResp,
SPDMAsymAlgo: r.SPDMAsymAlgo,
NVSwitchPresent: r.NVSwitchPresent,
}
copy(rep.Nonce[:], nonceBytes)
return rep, nil
}
// decodeOptionalB64 decodes a base64 field that may be empty. It accepts
// both standard and raw (unpadded) base64, returning nil for empty input.
func decodeOptionalB64(s string) ([]byte, error) {
if s == "" {
return nil, nil
}
if b, err := base64.StdEncoding.DecodeString(s); err == nil {
return b, nil
}
return base64.RawStdEncoding.DecodeString(s)
}
// 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 {
+59 -2
View File
@@ -36,11 +36,17 @@ var (
ErrRIMVersionMismatch = errors.New("nvidia/rim: driver/vbios version mismatch")
)
// RIMEntry is one row in the manifest: an expected measurement keyed
// by name, with its expected hex-encoded value.
// RIMEntry is one row in the manifest: an expected measurement value.
// Name is a human/audit label. Index, when present, is the SPDM
// measurement index this golden value pins — the binding the signed-record
// path (MatchSPDM) uses, since SPDM measurement blocks are keyed by index,
// not name. Index is a pointer so that index 0 is distinguishable from
// "no index", and omitempty keeps name-only manifests byte-identical on
// the wire (their detached signatures are unaffected).
type RIMEntry struct {
Name string `json:"name"`
ValueHex string `json:"value"`
Index *uint8 `json:"index,omitempty"`
}
// rawRIM is the on-the-wire JSON. The signature block is detachable:
@@ -175,6 +181,57 @@ func (rim *RIM) Match(report *GPUReport) error {
return nil
}
// ErrRIMEntryNoIndex is returned by MatchSPDM when a RIM entry lacks the
// index needed to bind it to a signed SPDM measurement block.
var ErrRIMEntryNoIndex = errors.New("nvidia/rim: entry has no index for SPDM matching")
// MatchSPDM validates that every golden value in the manifest is present
// — by SPDM measurement index — among the cryptographically signed blocks
// extracted from a verified MEASUREMENTS response. This is the trusted
// match: blocks come from VerifySPDMMeasurementSignature, so a passing
// match means "the GPU's signed measurements include exactly NVIDIA's
// golden values for this driver/VBIOS".
//
// arch/driver/vbios are the envelope's claimed preconditions and must
// equal the manifest's; the cryptographic gate, however, is value
// equality against the SIGNED blocks. Extra signed blocks not named by the
// manifest are allowed (NVIDIA reports more indices than any single RIM
// pins); every manifest entry MUST be covered (completeness), which closes
// the drop-a-bad-measurement attack.
func (rim *RIM) MatchSPDM(arch, driver, vbios string, blocks []SPDMMeasurement) error {
if rim == nil {
return errors.New("nvidia/rim: nil rim")
}
if rim.Architecture != arch {
return fmt.Errorf("%w: rim=%s report=%s", ErrRIMArchMismatch, rim.Architecture, arch)
}
if rim.DriverVersion != driver || rim.VBIOSVersion != vbios {
return fmt.Errorf("%w: rim=%s/%s report=%s/%s", ErrRIMVersionMismatch,
rim.DriverVersion, rim.VBIOSVersion, driver, vbios)
}
signed := make(map[uint8][]byte, len(blocks))
for _, b := range blocks {
signed[b.Index] = b.Value
}
for _, e := range rim.Entries {
if e.Index == nil {
return fmt.Errorf("%w: name=%s", ErrRIMEntryNoIndex, e.Name)
}
want, err := hex.DecodeString(strings.TrimPrefix(e.ValueHex, "0x"))
if err != nil {
return fmt.Errorf("nvidia/rim: bad entry hex %q: %w", e.Name, err)
}
got, ok := signed[*e.Index]
if !ok {
return fmt.Errorf("%w: name=%s index=%d (no signed block)", ErrRIMMissingEntry, e.Name, *e.Index)
}
if !bytesEqual(want, got) {
return fmt.Errorf("%w: name=%s index=%d", ErrRIMValueMismatch, e.Name, *e.Index)
}
}
return nil
}
// MeasurementRoot returns sha256(name||value) folded into a single
// root over a deterministic ordering. Used as the canonical
// MeasurementRoot in GPUEvidence.
+458
View File
@@ -0,0 +1,458 @@
// SPDM (DMTF DSP0274) measurement-report parsing and signature
// verification — the cryptographic core of NVIDIA GPU confidential-compute
// local attestation.
//
// An NVIDIA CC-capable GPU (Hopper H100/H200, Blackwell B100/B200/GB200)
// produces an attestation report that is a signed SPDM MEASUREMENTS
// response (RequestResponseCode 0x60, DSP0274). The report carries the
// DMTF measurement blocks the GPU firmware measured, a responder nonce,
// optional opaque data, and a signature produced by the GPU's attestation
// key. The matching GET_MEASUREMENTS request (0xE0) carries the requester
// nonce (the host's freshness challenge). The signature covers the
// transcript request || response[:-signature], so verifying it binds the
// measurement record AND the requester nonce to the GPU attestation key.
//
// This file implements:
// - the DSP0274 wire parse of the 0xE0 request and 0x60 response,
// - extraction of DMTF measurement blocks (index + signed digest),
// - the DSP0274 transcript / signing-context construction for SPDM 1.1
// and 1.2, and
// - signature verification with a strict algorithm <-> key-type binding
// (the same algorithm-substitution defense the NRAS JWS path uses).
//
// What makes the result trustworthy is the COMBINATION with devicechain.go:
// the public key that verifies the SPDM signature is the leaf of a cert
// chain that VerifyDeviceChain anchors to an operator-pinned NVIDIA device-
// identity root. SPDM signature alone proves "some key signed these
// measurements"; the chain proves "that key is a genuine NVIDIA GPU key".
//
// Clean-room: this is an implementation of the publicly documented DMTF
// DSP0274 schema. No NVIDIA proprietary code or keys are vendored.
package nvidia
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"hash"
"math/big"
)
// Errors returned by the SPDM parser / verifier.
var (
ErrSPDMShort = errors.New("nvidia/spdm: message shorter than header")
ErrSPDMBadCode = errors.New("nvidia/spdm: unexpected RequestResponseCode")
ErrSPDMBadVersion = errors.New("nvidia/spdm: unsupported SPDM version")
ErrSPDMTruncated = errors.New("nvidia/spdm: message truncated mid-field")
ErrSPDMNoSignature = errors.New("nvidia/spdm: response carries no signature")
ErrSPDMBadMeasRecord = errors.New("nvidia/spdm: malformed measurement record")
ErrSPDMUnsupportedAlgo = errors.New("nvidia/spdm: unsupported asym/hash algorithm")
ErrSPDMAlgKeyMismatch = errors.New("nvidia/spdm: asym algo does not match leaf key type")
ErrSPDMSignatureInvalid = errors.New("nvidia/spdm: measurement-report signature invalid")
ErrSPDMNonceLength = errors.New("nvidia/spdm: request nonce not present / wrong length")
)
// SPDM message opcodes (DSP0274).
const (
spdmGetMeasurements byte = 0xE0 // request
spdmMeasurements byte = 0x60 // response
)
// Supported SPDM version bytes. The version byte is encoded as
// (major<<4)|minor: 0x11 = SPDM 1.1, 0x12 = SPDM 1.2. Hopper attestation
// uses 1.1; Blackwell uses 1.2.
const (
spdmVersion11 byte = 0x11
spdmVersion12 byte = 0x12
)
// SPDMAsymAlgo names the asymmetric signature algorithm the GPU used to
// sign the MEASUREMENTS response. It binds three things together —
// signature length (needed to split the signed body from the trailing
// signature), the hash used in the transcript, and the public-key type
// the verifier must hold — so an attacker cannot substitute one for
// another. NVIDIA Hopper/Blackwell attestation keys are ECDSA P-384.
type SPDMAsymAlgo uint8
const (
// SPDMAsymECDSAP384SHA384 — ECDSA on NIST P-384 with SHA-384. The
// NVIDIA Hopper/Blackwell GPU attestation-key algorithm. Signature is
// fixed-width r||s, 96 bytes.
SPDMAsymECDSAP384SHA384 SPDMAsymAlgo = iota + 1
// SPDMAsymECDSAP256SHA256 — ECDSA P-256 / SHA-256, 64-byte r||s.
SPDMAsymECDSAP256SHA256
// SPDMAsymRSAPSS3072SHA384 — RSASSA-PSS, 3072-bit, SHA-384, 384-byte sig.
SPDMAsymRSAPSS3072SHA384
)
// ParseSPDMAsymAlgo maps the envelope's wire string to an algo. Unknown
// strings are refused (no silent default — the asym algo is a security
// parameter).
func ParseSPDMAsymAlgo(s string) (SPDMAsymAlgo, error) {
switch s {
case "ecdsa-p384-sha384", "ecdsa-p384", "ECDSA_P384", "":
// Empty string defaults to the NVIDIA production algorithm
// (ECDSA P-384), the only algo current CC GPUs emit. This is a
// convenience for the common case, not an insecure fallback: the
// signature still must verify under a P-384 leaf key, and a non-
// P-384 leaf is refused by the alg<->key binding below.
return SPDMAsymECDSAP384SHA384, nil
case "ecdsa-p256-sha256", "ecdsa-p256", "ECDSA_P256":
return SPDMAsymECDSAP256SHA256, nil
case "rsa-pss-3072-sha384", "rsapss3072":
return SPDMAsymRSAPSS3072SHA384, nil
default:
return 0, fmt.Errorf("%w: %q", ErrSPDMUnsupportedAlgo, s)
}
}
// sigLen returns the fixed signature length in bytes for the algo.
func (a SPDMAsymAlgo) sigLen() int {
switch a {
case SPDMAsymECDSAP384SHA384:
return 96
case SPDMAsymECDSAP256SHA256:
return 64
case SPDMAsymRSAPSS3072SHA384:
return 384
default:
return 0
}
}
// newHash returns a fresh hash for the algo's transcript hash function.
func (a SPDMAsymAlgo) newHash() hash.Hash {
switch a {
case SPDMAsymECDSAP256SHA256:
return sha256.New()
default: // P-384 and RSA-PSS-3072 use SHA-384
return sha512.New384()
}
}
func (a SPDMAsymAlgo) String() string {
switch a {
case SPDMAsymECDSAP384SHA384:
return "ecdsa-p384-sha384"
case SPDMAsymECDSAP256SHA256:
return "ecdsa-p256-sha256"
case SPDMAsymRSAPSS3072SHA384:
return "rsa-pss-3072-sha384"
default:
return "unknown"
}
}
// SPDMMeasurement is one DMTF measurement block extracted from a verified
// MEASUREMENTS response. Index is the SPDM measurement index (the stable
// identifier the RIM keys golden values by). Value is the raw measurement
// value (a SHA-384 digest for the digest value-type, or raw bytes for the
// raw value-type).
type SPDMMeasurement struct {
Index uint8
ValueType uint8 // DMTFSpecMeasurementValueType (bit7 = raw, bits[6:0]=type)
Value []byte
}
// IsRaw reports whether the block carries a raw value (bit7 set) rather
// than a digest.
func (m SPDMMeasurement) IsRaw() bool { return m.ValueType&0x80 != 0 }
// SPDMResponse is a parsed (not yet signature-verified) MEASUREMENTS
// response.
type SPDMResponse struct {
Version byte
NumberOfBlocks uint8
Measurements []SPDMMeasurement
ResponderNonce [32]byte
OpaqueData []byte
Signature []byte
// signedBody is response[:len-sigLen] — the bytes the signature
// covers. Retained so the verifier hashes exactly what was signed.
signedBody []byte
}
// ParseSPDMMeasurementResponse parses a MEASUREMENTS (0x60) response,
// splitting off a trailing signature of sigLen bytes. It validates the
// header, walks the measurement record into typed blocks, and locates the
// nonce / opaque / signature fields by their DSP0274 offsets.
//
// sigLen must be the negotiated signature size; a response shorter than
// header+record+nonce+opaque+sigLen is rejected as truncated.
func ParseSPDMMeasurementResponse(msg []byte, sigLen int) (*SPDMResponse, error) {
if sigLen <= 0 {
return nil, ErrSPDMUnsupportedAlgo
}
// Fixed header: version, code, param1, param2, numblocks, recordlen(3).
if len(msg) < 8 {
return nil, ErrSPDMShort
}
version := msg[0]
if version != spdmVersion11 && version != spdmVersion12 {
return nil, fmt.Errorf("%w: 0x%02x", ErrSPDMBadVersion, version)
}
if msg[1] != spdmMeasurements {
return nil, fmt.Errorf("%w: got 0x%02x want 0x60", ErrSPDMBadCode, msg[1])
}
numBlocks := msg[4]
recordLen := int(msg[5]) | int(msg[6])<<8 | int(msg[7])<<16 // 3-byte LE
off := 8
if off+recordLen+32+2+sigLen > len(msg) {
return nil, fmt.Errorf("%w: record=%d nonce=32 sig=%d total=%d", ErrSPDMTruncated, recordLen, sigLen, len(msg))
}
record := msg[off : off+recordLen]
off += recordLen
var responderNonce [32]byte
copy(responderNonce[:], msg[off:off+32])
off += 32
opaqueLen := int(binary.LittleEndian.Uint16(msg[off : off+2]))
off += 2
if off+opaqueLen+sigLen > len(msg) {
return nil, fmt.Errorf("%w: opaque=%d sig=%d total=%d", ErrSPDMTruncated, opaqueLen, sigLen, len(msg))
}
opaque := msg[off : off+opaqueLen]
off += opaqueLen
// The signature occupies the final sigLen bytes; anything after it is
// trailing garbage and rejected.
if off+sigLen != len(msg) {
return nil, fmt.Errorf("%w: %d trailing bytes after signature", ErrSPDMTruncated, len(msg)-(off+sigLen))
}
sig := msg[off : off+sigLen]
blocks, err := parseMeasurementBlocks(record, int(numBlocks))
if err != nil {
return nil, err
}
return &SPDMResponse{
Version: version,
NumberOfBlocks: numBlocks,
Measurements: blocks,
ResponderNonce: responderNonce,
OpaqueData: append([]byte(nil), opaque...),
Signature: append([]byte(nil), sig...),
signedBody: append([]byte(nil), msg[:len(msg)-sigLen]...),
}, nil
}
// parseMeasurementBlocks walks the DMTF measurement record into numBlocks
// blocks. Each block: Index(1) MeasurementSpecification(1)
// MeasurementSize(2 LE) Measurement(MeasurementSize). The DMTF Measurement
// is itself ValueType(1) ValueSize(2 LE) Value(ValueSize).
func parseMeasurementBlocks(record []byte, numBlocks int) ([]SPDMMeasurement, error) {
out := make([]SPDMMeasurement, 0, numBlocks)
off := 0
for i := 0; i < numBlocks; i++ {
if off+4 > len(record) {
return nil, fmt.Errorf("%w: block %d header past end", ErrSPDMBadMeasRecord, i)
}
index := record[off]
// record[off+1] = MeasurementSpecification; we only support DMTF
// (bit0). We do not hard-fail on the spec byte: NVIDIA always sets
// DMTF, and the value-type below is what we actually consume.
measSize := int(binary.LittleEndian.Uint16(record[off+2 : off+4]))
off += 4
if off+measSize > len(record) {
return nil, fmt.Errorf("%w: block %d value past end", ErrSPDMBadMeasRecord, i)
}
dmtf := record[off : off+measSize]
off += measSize
if len(dmtf) < 3 {
return nil, fmt.Errorf("%w: block %d dmtf shorter than 3", ErrSPDMBadMeasRecord, i)
}
valType := dmtf[0]
valSize := int(binary.LittleEndian.Uint16(dmtf[1:3]))
if 3+valSize != len(dmtf) {
return nil, fmt.Errorf("%w: block %d dmtf value size %d != %d", ErrSPDMBadMeasRecord, i, valSize, len(dmtf)-3)
}
out = append(out, SPDMMeasurement{
Index: index,
ValueType: valType,
Value: append([]byte(nil), dmtf[3:]...),
})
}
if off != len(record) {
return nil, fmt.Errorf("%w: %d trailing record bytes", ErrSPDMBadMeasRecord, len(record)-off)
}
return out, nil
}
// SPDMRequestNonce extracts the requester nonce from a GET_MEASUREMENTS
// (0xE0) request that requested a signature. The nonce is the host's
// freshness challenge; it lives at offset 4..36 when Param1 bit0 (signature
// requested) is set. Returns the nonce and the SPDM version.
//
// The request is part of the signed transcript, so a verified signature
// proves this nonce was bound by the GPU. Callers compare it to the
// challenge they issued (WithExpectedReportData).
func SPDMRequestNonce(req []byte) (version byte, nonce [32]byte, err error) {
if len(req) < 4 {
return 0, nonce, ErrSPDMShort
}
version = req[0]
if version != spdmVersion11 && version != spdmVersion12 {
return 0, nonce, fmt.Errorf("%w: 0x%02x", ErrSPDMBadVersion, version)
}
if req[1] != spdmGetMeasurements {
return 0, nonce, fmt.Errorf("%w: got 0x%02x want 0xE0", ErrSPDMBadCode, req[1])
}
sigRequested := req[2]&0x01 != 0
if !sigRequested {
return 0, nonce, fmt.Errorf("%w: request did not ask for a signature", ErrSPDMNoSignature)
}
if len(req) < 4+32 {
return 0, nonce, ErrSPDMNonceLength
}
copy(nonce[:], req[4:4+32])
return version, nonce, nil
}
// VerifySPDMMeasurementSignature verifies the MEASUREMENTS response
// signature over the DSP0274 transcript request || response[:-sig] using
// leafPub, under the named algo. It returns the parsed, signature-verified
// response on success.
//
// The algo binds the signature length, the hash, AND the leaf key type:
// a P-384 algo against an RSA leaf (or vice versa) is refused before any
// verification primitive runs (ErrSPDMAlgKeyMismatch) — the same
// algorithm-substitution defense the NRAS JWS path applies.
func VerifySPDMMeasurementSignature(leafPub crypto.PublicKey, algo SPDMAsymAlgo, requestMsg, responseMsg []byte) (*SPDMResponse, error) {
sigLen := algo.sigLen()
if sigLen == 0 {
return nil, ErrSPDMUnsupportedAlgo
}
resp, err := ParseSPDMMeasurementResponse(responseMsg, sigLen)
if err != nil {
return nil, err
}
// The request version must match the response version (a transcript
// spanning two SPDM versions is malformed).
reqVersion, _, err := SPDMRequestNonce(requestMsg)
if err != nil {
return nil, err
}
if reqVersion != resp.Version {
return nil, fmt.Errorf("%w: request 0x%02x response 0x%02x", ErrSPDMBadVersion, reqVersion, resp.Version)
}
digest := spdmMeasurementDigest(algo, resp.Version, requestMsg, resp.signedBody)
switch algo {
case SPDMAsymECDSAP384SHA384, SPDMAsymECDSAP256SHA256:
pub, ok := leafPub.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("%w: algo=%s leaf=%T", ErrSPDMAlgKeyMismatch, algo, leafPub)
}
wantCurve := "P-384"
if algo == SPDMAsymECDSAP256SHA256 {
wantCurve = "P-256"
}
if pub.Curve.Params().Name != wantCurve {
return nil, fmt.Errorf("%w: algo=%s leaf curve=%s", ErrSPDMAlgKeyMismatch, algo, pub.Curve.Params().Name)
}
if err := ecdsaVerifyRaw(pub, digest, resp.Signature); err != nil {
return nil, err
}
return resp, nil
case SPDMAsymRSAPSS3072SHA384:
pub, ok := leafPub.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("%w: algo=%s leaf=%T", ErrSPDMAlgKeyMismatch, algo, leafPub)
}
if err := rsa.VerifyPSS(pub, crypto.SHA384, digest, resp.Signature, &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
Hash: crypto.SHA384,
}); err != nil {
return nil, fmt.Errorf("%w: %v", ErrSPDMSignatureInvalid, err)
}
return resp, nil
default:
return nil, ErrSPDMUnsupportedAlgo
}
}
// spdmMeasurementDigest returns the digest the asymmetric primitive
// verifies against, per DSP0274:
//
// M = requestMsg || responseSignedBody (the transcript L)
// 1.1: digest = Hash(M)
// 1.2: digest = Hash( SPDMSignedContext || Hash(M) )
//
// where SPDMSignedContext is the 100-byte responder-measurements signing
// context defined by DSP0274 1.2 (see spdmSigningContext).
func spdmMeasurementDigest(algo SPDMAsymAlgo, version byte, requestMsg, responseSignedBody []byte) []byte {
h := algo.newHash()
h.Write(requestMsg)
h.Write(responseSignedBody)
th := h.Sum(nil)
ctx := spdmSigningContext(version)
if ctx == nil {
return th // SPDM 1.1: sign over Hash(M)
}
h2 := algo.newHash()
h2.Write(ctx)
h2.Write(th)
return h2.Sum(nil) // SPDM 1.2: sign over Hash(ctx || Hash(M))
}
// spdmSigningContext builds the DSP0274 1.2 "SPDMSignedContext" prefix for
// a responder MEASUREMENTS signature. Returns nil for SPDM < 1.2 (which
// signs Hash(transcript) directly, no prefix).
//
// Layout (100 bytes total, per DSP0274 1.2 and libspdm/spdm-rs):
//
// [0:64] four copies of the 16-byte ASCII "dmtf-spdm-v1.2.*"
// [64:70] six zero bytes
// [70:100] the 30-byte context "responder-measurements signing"
//
// NOTE (production integration): this prefix construction matches the DMTF
// spec and reference implementations. Confirming byte-exact agreement with
// a real Blackwell (SPDM 1.2) attestation report — including whether
// NVIDIA's firmware folds the VCA (GET_VERSION/CAPABILITIES/ALGORITHMS)
// transcript into M — requires a real B200/GB200 vector (documented
// collateral). Hopper (SPDM 1.1, the current production target) uses the
// no-prefix path and is fully exercised by the test vectors.
func spdmSigningContext(version byte) []byte {
if version < spdmVersion12 {
return nil
}
const prefixUnit = "dmtf-spdm-v1.2.*" // exactly 16 bytes
const context = "responder-measurements signing"
ctx := make([]byte, 100)
for i := 0; i < 4; i++ {
copy(ctx[i*16:(i+1)*16], prefixUnit)
}
// ctx[64:70] left zero; context is right-aligned in the 100-byte buffer.
copy(ctx[100-len(context):], context)
return ctx
}
// ecdsaVerifyRaw verifies a fixed-width r||s ECDSA signature (big-endian,
// equal halves — the SPDM/JOSE encoding, NOT DER) over a pre-computed
// digest.
func ecdsaVerifyRaw(pub *ecdsa.PublicKey, digest, sig []byte) error {
coord := (pub.Curve.Params().BitSize + 7) / 8
if len(sig) != 2*coord {
return fmt.Errorf("%w: sig len %d != 2*%d", ErrSPDMSignatureInvalid, len(sig), coord)
}
r := new(big.Int).SetBytes(sig[:coord])
s := new(big.Int).SetBytes(sig[coord:])
if !ecdsa.Verify(pub, digest, r, s) {
return ErrSPDMSignatureInvalid
}
return nil
}
+160
View File
@@ -0,0 +1,160 @@
// Unit tests for the DSP0274 SPDM measurement-report parser and signature
// verifier. These exercise the primitive directly (the cc/attest end-to-end
// tests drive it through the NVTrust Kind); the focus here is wire-parse
// robustness and the algorithm <-> key-type substitution defense.
package nvidia
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha512"
"encoding/binary"
"errors"
"testing"
)
func le16b(v int) []byte {
b := make([]byte, 2)
binary.LittleEndian.PutUint16(b, uint16(v))
return b
}
func le24b(v int) []byte { return []byte{byte(v), byte(v >> 8), byte(v >> 16)} }
// buildSPDM builds a (request, response) pair: a single measurement block
// at the given index/value, signed with leafPriv (ECDSA P-384, SPDM 1.1).
func buildSPDM(t *testing.T, leafPriv *ecdsa.PrivateKey, idx uint8, value []byte) (req, resp []byte) {
t.Helper()
var nonce [32]byte
for i := range nonce {
nonce[i] = byte(i)
}
req = []byte{0x11, 0xE0, 0x01, 0xFF}
req = append(req, nonce[:]...)
req = append(req, 0x00)
dmtf := []byte{0x00}
dmtf = append(dmtf, le16b(len(value))...)
dmtf = append(dmtf, value...)
blk := []byte{idx, 0x01}
blk = append(blk, le16b(len(dmtf))...)
blk = append(blk, dmtf...)
var respNonce [32]byte
body := []byte{0x11, 0x60, 0x00, 0x00, 1}
body = append(body, le24b(len(blk))...)
body = append(body, blk...)
body = append(body, respNonce[:]...)
body = append(body, le16b(0)...)
h := sha512.New384()
h.Write(req)
h.Write(body)
digest := h.Sum(nil)
r, s, err := ecdsa.Sign(rand.Reader, leafPriv, digest)
if err != nil {
t.Fatalf("sign: %v", err)
}
sig := joseConcat(r, s, 48)
return req, append(body, sig...)
}
func TestSPDM_VerifyValid_ExtractsMeasurement(t *testing.T) {
priv, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
val := bytes.Repeat([]byte{0xAB}, 48)
req, resp := buildSPDM(t, priv, 7, val)
parsed, err := VerifySPDMMeasurementSignature(&priv.PublicKey, SPDMAsymECDSAP384SHA384, req, resp)
if err != nil {
t.Fatalf("verify: %v", err)
}
if len(parsed.Measurements) != 1 || parsed.Measurements[0].Index != 7 {
t.Fatalf("measurements = %+v", parsed.Measurements)
}
if !bytes.Equal(parsed.Measurements[0].Value, val) {
t.Fatalf("value = %x want %x", parsed.Measurements[0].Value, val)
}
}
func TestSPDM_RejectsTamperedSignature(t *testing.T) {
priv, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
req, resp := buildSPDM(t, priv, 0, bytes.Repeat([]byte{0x01}, 48))
resp[len(resp)-1] ^= 0xFF
_, err := VerifySPDMMeasurementSignature(&priv.PublicKey, SPDMAsymECDSAP384SHA384, req, resp)
if !errors.Is(err, ErrSPDMSignatureInvalid) {
t.Fatalf("err = %v, want ErrSPDMSignatureInvalid", err)
}
}
func TestSPDM_RejectsWrongKey(t *testing.T) {
priv, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
other, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
req, resp := buildSPDM(t, priv, 0, bytes.Repeat([]byte{0x01}, 48))
_, err := VerifySPDMMeasurementSignature(&other.PublicKey, SPDMAsymECDSAP384SHA384, req, resp)
if !errors.Is(err, ErrSPDMSignatureInvalid) {
t.Fatalf("err = %v, want ErrSPDMSignatureInvalid", err)
}
}
// Algorithm-substitution defense: a P-384 algo paired with a non-P-384 key
// is refused before verification (mirrors the JWS alg<->key binding).
func TestSPDM_AlgKeyTypeMismatch(t *testing.T) {
p384, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
req, resp := buildSPDM(t, p384, 0, bytes.Repeat([]byte{0x01}, 48))
// P-384 algo, P-256 key.
p256, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if _, err := VerifySPDMMeasurementSignature(&p256.PublicKey, SPDMAsymECDSAP384SHA384, req, resp); !errors.Is(err, ErrSPDMAlgKeyMismatch) {
t.Fatalf("p256 key: err = %v, want ErrSPDMAlgKeyMismatch", err)
}
// P-384 algo, RSA key.
rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048)
if _, err := VerifySPDMMeasurementSignature(&rsaKey.PublicKey, SPDMAsymECDSAP384SHA384, req, resp); !errors.Is(err, ErrSPDMAlgKeyMismatch) {
t.Fatalf("rsa key: err = %v, want ErrSPDMAlgKeyMismatch", err)
}
}
func TestSPDM_RejectsTruncated(t *testing.T) {
if _, err := ParseSPDMMeasurementResponse([]byte{0x11, 0x60, 0, 0}, 96); !errors.Is(err, ErrSPDMShort) {
t.Fatalf("short header: err = %v, want ErrSPDMShort", err)
}
priv, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
_, resp := buildSPDM(t, priv, 0, bytes.Repeat([]byte{0x01}, 48))
if _, err := ParseSPDMMeasurementResponse(resp[:len(resp)-10], 96); !errors.Is(err, ErrSPDMTruncated) {
t.Fatalf("truncated body: err = %v, want ErrSPDMTruncated", err)
}
}
func TestSPDM_RejectsBadResponseCode(t *testing.T) {
priv, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
_, resp := buildSPDM(t, priv, 0, bytes.Repeat([]byte{0x01}, 48))
resp[1] = 0x61 // not MEASUREMENTS
if _, err := ParseSPDMMeasurementResponse(resp, 96); !errors.Is(err, ErrSPDMBadCode) {
t.Fatalf("err = %v, want ErrSPDMBadCode", err)
}
}
func TestSPDM_RequestNonce(t *testing.T) {
var nonce [32]byte
for i := range nonce {
nonce[i] = byte(i + 100)
}
req := []byte{0x11, 0xE0, 0x01, 0xFF}
req = append(req, nonce[:]...)
req = append(req, 0x00)
v, got, err := SPDMRequestNonce(req)
if err != nil {
t.Fatalf("nonce: %v", err)
}
if v != 0x11 || got != nonce {
t.Fatalf("version=0x%02x nonce=%x", v, got)
}
// A request that did not ask for a signature is refused.
req[2] = 0x00
if _, _, err := SPDMRequestNonce(req); !errors.Is(err, ErrSPDMNoSignature) {
t.Fatalf("no-sig: err = %v, want ErrSPDMNoSignature", err)
}
}
+276 -92
View File
@@ -5,160 +5,344 @@ package attest
import (
"context"
"crypto/sha256"
"crypto/subtle"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"time"
"github.com/luxfi/cc/attest/nvidia"
)
// NVTrust is the NVIDIA GPU confidential-compute attestation verifier.
// NVTrust is the NVIDIA GPU confidential-compute attestation verifier. It
// implements both NVIDIA trust models behind one Kind, selected by Mode:
//
// NVTrust is NVIDIA's open attestation toolkit. It has two trust models:
// - ModeLocal (default, cloud-free): full hardware verification. The GPU
// evidence carries a signed SPDM MEASUREMENTS report plus the GPU's
// device certificate chain. NVTrust (1) chains the leaf to an operator-
// pinned NVIDIA device-identity root, (2) verifies the SPDM report
// signature with that leaf key, (3) extracts measurements from the
// SIGNED record and matches them by index to an NVIDIA-signed Reference
// Integrity Manifest, and (4) binds the signed requester nonce to the
// caller's freshness challenge. This is the mode a decentralized
// control plane requires — the only trust anchors are the operator-
// pinned device root and RIM signing key; no NVIDIA cloud is contacted.
//
// - LOCAL (this verifier, the canonical Kind): the GPU evidence report
// is matched against a Reference Integrity Manifest (RIM) that NVIDIA
// signs per driver+VBIOS. The operator pins the RIM signing key; the
// verifier checks the RIM signature and that every measurement the GPU
// reports equals the signed golden value. No network, no NVIDIA cloud
// dependency — the only trust anchor is the operator-pinned signing key.
// This is the mode a decentralized / blockchain control plane requires.
// - ModeNRAS (remote): the evidence is an NVIDIA Remote Attestation
// Service EAT/JWT. NVTrust verifies the token signature against pinned
// NRAS signer keys (or an x5c chain to a pinned NRAS root), checks
// freshness, and binds the eat_nonce. The relying party trusts NVIDIA's
// cloud verdict. The live POST to NRAS is nvidia.NRASClient's job; this
// Kind only verifies the resulting token (no network in Verify).
//
// - REMOTE (NVIDIA Remote Attestation Service): the evidence is POSTed to
// NRAS, which returns a signed JWS token. That path is the lower-level
// nvidia.NRASClient primitive; it is a distinct deployment choice (trust
// NVIDIA's cloud) and is NOT what this Kind dispatches to. Callers that
// accept a cloud dependency use nvidia.NRASClient directly.
// Trust anchors are NEVER taken from the evidence. They are operator-pinned
// at construction (NewNVTrust + options), exactly as AMD's ARK is pinned
// for SEV-SNP. There is no insecure mode: ModeLocal with no device root, or
// ModeNRAS with no NRAS trust, is refused.
//
// Evidence is the JSON GPU evidence envelope (see nvidia.ParseGPUReport).
// The signed RIM and the trust roots that may sign it are supplied via
// options — WithNVTrustRIM and WithNVTrustTrustRoots. There is no insecure
// mode: a verify with no RIM or no trust roots is refused with ErrPolicy,
// exactly as nvidia.ParseAndVerifyRIM refuses an empty trust-root set.
// The zero value (NVTrust{}, used by Dispatch) is ModeLocal with no pinned
// device root and therefore fail-closed: it refuses every input. Production
// callers construct a configured verifier with NewNVTrust and use it as a
// Verifier; see nvtrust_options.go.
//
// On success the VerifiedReport carries:
//
// - Measurement: the RIM measurement root (sha256 over the signed,
// name-sorted golden measurement list the GPU was proven to match).
// - ReportData: the 32-byte freshness nonce echoed in the GPU report.
// - ChipID: the per-GPU UUID bytes.
// - Extra: driver/VBIOS/architecture/evidence-version + RIM signer.
//
// Honest scope: local mode proves "this GPU reports measurements that match
// an NVIDIA-signed RIM for its driver+VBIOS, bound to a fresh nonce." It
// does not, on its own, verify the GPU device certificate's SPDM signature
// against NVIDIA's device root CA — that anchor is what the REMOTE NRAS path
// (or a future embedded device-root) provides. Local mode is a strict
// upgrade over any unsigned/structural-only check and is the cloud-free
// integrity gate; deployments needing device-cert chaining add the NRAS
// primitive on top.
type NVTrust struct{}
// - Measurement: the matched RIM measurement root (ModeLocal) or the EAT
// measurement root (ModeNRAS). Trust-score / tier is policy ON TOP of
// this — the caller scores an already-verified measurement.
// - ReportData: the 32-byte freshness nonce (the SIGNED SPDM requester
// nonce in ModeLocal; the eat_nonce in ModeNRAS).
// - ChipID: the device leaf-certificate serial (ModeLocal) — a
// trusted GPU identity, not the untrusted envelope UUID.
type NVTrust struct {
mode NVTrustMode
deviceRoots *x509.CertPool // ModeLocal: NVIDIA device-identity root CA
nrasRoots []nvidia.TrustRoot // ModeNRAS: NRAS signer keys (by kid)
nrasRootPool *x509.CertPool // ModeNRAS: NRAS root CA for x5c chaining (optional)
now func() time.Time // injectable clock; nil = per-call WithNow / wall
}
// Verify implements Verifier for the NVIDIA local-RIM attestation path.
func (NVTrust) Verify(ctx context.Context, evidence []byte, opts ...Option) (*VerifiedReport, error) {
// NVTrustMode selects the GPU attestation trust model.
type NVTrustMode uint8
const (
// ModeLocal performs full local hardware SPDM verification. This is
// the zero value, so an unconfigured NVTrust defaults to the strict
// (and, without a pinned device root, fail-closed) local path.
ModeLocal NVTrustMode = iota
// ModeNRAS verifies an NVIDIA Remote Attestation Service EAT token.
ModeNRAS
)
func (m NVTrustMode) String() string {
switch m {
case ModeLocal:
return "local"
case ModeNRAS:
return "nras"
default:
return "invalid"
}
}
// Verify implements Verifier, dispatching on the configured mode.
func (n NVTrust) Verify(ctx context.Context, evidence []byte, opts ...Option) (*VerifiedReport, error) {
cfg := applyOptions(opts...)
switch n.mode {
case ModeLocal:
return n.verifyLocal(evidence, cfg)
case ModeNRAS:
return n.verifyNRAS(evidence, cfg)
default:
return nil, fmt.Errorf("%w: nvtrust mode %d", ErrUnsupportedKind, n.mode)
}
}
// verifyLocal runs the full SPDM + device-chain + RIM verification.
func (n NVTrust) verifyLocal(evidence []byte, cfg config) (*VerifiedReport, error) {
now := n.clock(cfg)
// Parse the GPU evidence envelope. ParseGPUReport validates the
// envelope version, architecture, nonce shape, and decodes every
// measurement — structural problems fail here before any trust
// decision is made.
report, err := nvidia.ParseGPUReport(evidence)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
}
// No insecure mode: a signed RIM and the keys that may sign it are
// mandatory. Refuse rather than fall back to "trust the report".
// No insecure mode: the device-identity root must be pinned.
if n.deviceRoots == nil {
return nil, fmt.Errorf("%w: nvtrust local mode requires a pinned NVIDIA device root (NewNVTrust + WithNVTrustDeviceRoots)", ErrPolicy)
}
// The signed SPDM evidence (request + response) is mandatory in local
// mode — without it there is nothing cryptographic to verify.
if len(report.SPDMResponse) == 0 || len(report.SPDMRequest) == 0 {
return nil, fmt.Errorf("%w: nvtrust local mode requires spdm_request and spdm_response", ErrInvalidEvidence)
}
// 1) Chain the GPU device cert to the pinned NVIDIA root, get the leaf.
chain, err := nvidia.ParseCertChainPEM(report.CertChain)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
}
leaf, err := nvidia.VerifyDeviceChain(chain, n.deviceRoots, now)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrChainInvalid, err)
}
leafKey, err := nvidia.LeafSPDMKey(leaf)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrChainInvalid, err)
}
// 2) Verify the SPDM measurement-report signature with the leaf key.
algo, err := nvidia.ParseSPDMAsymAlgo(report.SPDMAsymAlgo)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
}
spdm, err := nvidia.VerifySPDMMeasurementSignature(leafKey, algo, report.SPDMRequest, report.SPDMResponse)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrSignatureInvalid, err)
}
// 3) The SIGNED requester nonce is the freshness anchor. It must match
// the envelope's claimed nonce (consistency) and is what policy binds.
_, reqNonce, err := nvidia.SPDMRequestNonce(report.SPDMRequest)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
}
if reqNonce != report.Nonce {
return nil, fmt.Errorf("%w: envelope nonce %x != signed request nonce %x",
ErrPolicy, report.Nonce[:], reqNonce[:])
}
// 4) Match the SIGNED measurements against the NVIDIA-signed RIM.
if len(cfg.nvtrustRIM) == 0 {
return nil, fmt.Errorf("%w: nvtrust requires a signed RIM (WithNVTrustRIM)", ErrPolicy)
}
if len(cfg.nvtrustTrustRoots) == 0 {
return nil, fmt.Errorf("%w: nvtrust requires RIM trust roots (WithNVTrustTrustRoots)", ErrPolicy)
}
// Verify the RIM's detached signature against the operator-pinned
// trust roots. A bad signature means the golden values are not vendor-
// authentic — treat as a vendor-chain failure (refuse).
rim, err := nvidia.ParseAndVerifyRIM(cfg.nvtrustRIM, cfg.nvtrustTrustRoots)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrChainInvalid, err)
}
// Match every reported measurement against the signed RIM, and assert
// the RIM's driver/VBIOS/architecture preconditions equal the report.
// A cryptographically valid RIM that does not cover this GPU's actual
// measurements is a policy refusal, not a chain failure.
if err := rim.Match(report); err != nil {
if err := rim.MatchSPDM(report.Architecture, report.DriverVersion, report.VBIOSVersion, spdm.Measurements); err != nil {
return nil, fmt.Errorf("%w: %v", ErrPolicy, err)
}
measRoot := rim.MeasurementRoot()
// Caller-supplied policy: freshness nonce binding.
// 5) Caller policy: freshness nonce and measurement-root pin.
if cfg.expectedReportData != nil {
if subtle.ConstantTimeCompare(cfg.expectedReportData, report.Nonce[:]) != 1 {
return nil, fmt.Errorf("%w: nonce mismatch (got %s)",
ErrPolicy, hex.EncodeToString(report.Nonce[:]))
if subtle.ConstantTimeCompare(cfg.expectedReportData, reqNonce[:]) != 1 {
return nil, fmt.Errorf("%w: nonce mismatch (got %s)", ErrPolicy, hex.EncodeToString(reqNonce[:]))
}
}
// Caller-supplied policy: pin the expected measurement root.
if cfg.expectedMeasurement != nil {
if subtle.ConstantTimeCompare(cfg.expectedMeasurement, measRoot[:]) != 1 {
return nil, fmt.Errorf("%w: measurement root mismatch (got %s)",
ErrPolicy, hex.EncodeToString(measRoot[:]))
return nil, fmt.Errorf("%w: measurement root mismatch (got %s)", ErrPolicy, hex.EncodeToString(measRoot[:]))
}
}
out := &VerifiedReport{
Kind: KindNVTrust,
Vendor: "nvidia.nvtrust",
Vendor: "nvidia.nvtrust.local",
Measurement: append([]byte(nil), measRoot[:]...),
ReportData: append([]byte(nil), report.Nonce[:]...),
ChipID: []byte(report.UUID),
IssuedAt: cfg.nowOrWall().UTC(),
Extra: buildNVTrustExtra(report, rim),
ReportData: append([]byte(nil), reqNonce[:]...),
ChipID: leaf.SerialNumber.Bytes(),
IssuedAt: now.UTC(),
Extra: buildNVTrustLocalExtra(report, rim, leaf, algo),
}
out.CompositeHash = computeCompositeHash(KindNVTrust, canonicalNVTrustBytes(report, rim))
out.CompositeHash = computeCompositeHash(KindNVTrust, canonicalNVTrustLocalBytes(leaf, reqNonce, measRoot, rim.SignerKeyID, spdm.Signature))
return out, nil
}
// buildNVTrustExtra collects NVIDIA-specific fields that don't fit the
// common shape. Keys are stable wire identifiers prefixed "nvtrust.".
func buildNVTrustExtra(report *nvidia.GPUReport, rim *nvidia.RIM) map[string]string {
extra := make(map[string]string, 6)
extra["nvtrust.gpu_uuid"] = report.UUID
extra["nvtrust.architecture"] = report.Architecture
extra["nvtrust.driver_version"] = report.DriverVersion
extra["nvtrust.vbios_version"] = report.VBIOSVersion
extra["nvtrust.evidence_version"] = report.EvidenceVersion
extra["nvtrust.rim_signer"] = rim.SignerKeyID
// verifyNRAS verifies an NVIDIA Remote Attestation Service EAT token.
func (n NVTrust) verifyNRAS(evidence []byte, cfg config) (*VerifiedReport, error) {
now := n.clock(cfg)
if len(n.nrasRoots) == 0 && n.nrasRootPool == nil {
return nil, fmt.Errorf("%w: nvtrust NRAS mode requires pinned NRAS trust (WithNVTrustNRASRoots / WithNVTrustNRASRootCAs)", ErrPolicy)
}
if len(evidence) == 0 {
return nil, fmt.Errorf("%w: empty NRAS token", ErrInvalidEvidence)
}
// Freshness challenge from policy: must be 32 bytes if supplied.
var challenge *[32]byte
if cfg.expectedReportData != nil {
if len(cfg.expectedReportData) != 32 {
return nil, fmt.Errorf("%w: NRAS challenge must be 32 bytes, got %d", ErrPolicy, len(cfg.expectedReportData))
}
var c [32]byte
copy(c[:], cfg.expectedReportData)
challenge = &c
}
eat, err := nvidia.VerifyEAT(string(evidence), n.nrasRoots, n.nrasRootPool, challenge, now)
if err != nil {
return nil, classifyEATError(err)
}
if cfg.expectedMeasurement != nil {
if subtle.ConstantTimeCompare(cfg.expectedMeasurement, eat.Measurement) != 1 {
return nil, fmt.Errorf("%w: measurement mismatch (got %s)", ErrPolicy, hex.EncodeToString(eat.Measurement))
}
}
out := &VerifiedReport{
Kind: KindNVTrust,
Vendor: "nvidia.nvtrust.nras",
Measurement: append([]byte(nil), eat.Measurement...),
IssuedAt: now.UTC(),
Extra: buildNVTrustNRASExtra(eat),
}
if eat.HasNonce {
out.ReportData = append([]byte(nil), eat.Nonce[:]...)
}
tokenHash := sha256.Sum256(evidence)
out.CompositeHash = computeCompositeHash(KindNVTrust, canonicalNVTrustNRASBytes(eat, tokenHash))
return out, nil
}
// clock resolves the verification time: per-call WithNow wins (tests),
// then the instance clock, then wall.
func (n NVTrust) clock(cfg config) time.Time {
if !cfg.now.IsZero() {
return cfg.now
}
if n.now != nil {
return n.now()
}
return time.Now()
}
// classifyEATError maps EAT verification failures onto the refusal
// taxonomy: bad token/structure -> ErrInvalidEvidence, untrusted chain or
// signature -> ErrChainInvalid/ErrSignatureInvalid, everything else
// (freshness, nonce, negative verdict) -> ErrPolicy. Every path refuses.
func classifyEATError(err error) error {
switch {
case errors.Is(err, nvidia.ErrEATBadJWT):
return fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
case errors.Is(err, nvidia.ErrEATChainUntrusted):
return fmt.Errorf("%w: %v", ErrChainInvalid, err)
case errors.Is(err, nvidia.ErrEATBadSignature), errors.Is(err, nvidia.ErrEATNoSigner):
return fmt.Errorf("%w: %v", ErrSignatureInvalid, err)
default:
return fmt.Errorf("%w: %v", ErrPolicy, err)
}
}
// buildNVTrustLocalExtra collects local-mode audit fields. The envelope
// UUID/driver/VBIOS are CLAIMED (untrusted labels) and tagged as such; the
// device identity is the verified leaf subject.
func buildNVTrustLocalExtra(report *nvidia.GPUReport, rim *nvidia.RIM, leaf *x509.Certificate, algo nvidia.SPDMAsymAlgo) map[string]string {
extra := map[string]string{
"nvtrust.mode": "local",
"nvtrust.claimed_uuid": report.UUID,
"nvtrust.architecture": report.Architecture,
"nvtrust.driver_version": report.DriverVersion,
"nvtrust.vbios_version": report.VBIOSVersion,
"nvtrust.evidence_ver": report.EvidenceVersion,
"nvtrust.rim_signer": rim.SignerKeyID,
"nvtrust.spdm_asym_algo": algo.String(),
"nvtrust.device_subject": leaf.Subject.String(),
"nvtrust.device_serial": leaf.SerialNumber.Text(16),
"nvtrust.device_issuer": leaf.Issuer.String(),
"nvtrust.device_notafter": leaf.NotAfter.UTC().Format(time.RFC3339),
}
if report.NVSwitchPresent {
extra["nvtrust.nvswitch_present"] = "true"
}
return extra
}
// canonicalNVTrustBytes returns the bytes that participate in
// CompositeHash. We hash the verifier-extracted fields (UUID, arch,
// driver, VBIOS, nonce, signed measurement root, RIM signer) in a fixed,
// null-separated order so that re-deriving the hash on the consumer side
// reproduces the same value iff the same evidence was matched against the
// same signed RIM. UUID/version strings cannot contain NUL, so the
// separator is unambiguous.
func canonicalNVTrustBytes(report *nvidia.GPUReport, rim *nvidia.RIM) []byte {
measRoot := rim.MeasurementRoot()
buf := make([]byte, 0, 256)
func buildNVTrustNRASExtra(eat *nvidia.EATResult) map[string]string {
extra := map[string]string{
"nvtrust.mode": "nras",
"nvtrust.nras_signer": eat.SignerKeyID,
}
if eat.OverallResult != "" {
extra["nvtrust.nras_result"] = eat.OverallResult
}
if !eat.ExpiresAt.IsZero() {
extra["nvtrust.nras_exp"] = eat.ExpiresAt.UTC().Format(time.RFC3339)
}
return extra
}
// canonicalNVTrustLocalBytes is the deterministic, verified-fields blob
// that CompositeHash commits to. It binds the device identity (leaf SPKI),
// the freshness nonce, the matched RIM measurement root + signer, and the
// SPDM signature (which uniquely commits to the exact signed measurement
// record under the device key). NUL-separated; SPKI/sig are fixed-shape
// DER/bytes so the separator is unambiguous across the string fields.
func canonicalNVTrustLocalBytes(leaf *x509.Certificate, nonce [32]byte, measRoot [32]byte, rimSigner string, spdmSig []byte) []byte {
buf := make([]byte, 0, 512)
appendField := func(b []byte) {
buf = append(buf, b...)
buf = append(buf, 0x00)
}
appendField([]byte(report.UUID))
appendField([]byte(report.Architecture))
appendField([]byte(report.DriverVersion))
appendField([]byte(report.VBIOSVersion))
appendField(report.Nonce[:])
appendField(leaf.RawSubjectPublicKeyInfo)
appendField(nonce[:])
appendField(measRoot[:])
appendField([]byte(rim.SignerKeyID))
appendField([]byte(rimSigner))
appendField(spdmSig)
return buf
}
func canonicalNVTrustNRASBytes(eat *nvidia.EATResult, tokenHash [32]byte) []byte {
buf := make([]byte, 0, 160)
appendField := func(b []byte) {
buf = append(buf, b...)
buf = append(buf, 0x00)
}
appendField(tokenHash[:])
appendField(eat.Measurement)
appendField([]byte(eat.SignerKeyID))
if eat.HasNonce {
appendField(eat.Nonce[:])
} else {
appendField(nil)
}
return buf
}
+91
View File
@@ -0,0 +1,91 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package attest
import (
"crypto/x509"
"errors"
"time"
"github.com/luxfi/cc/attest/nvidia"
)
// NVTrustOption configures a NVTrust verifier at construction. These set
// the per-DEPLOYMENT trust anchors and mode — distinct from the per-CALL
// policy Options (WithNVTrustRIM, WithExpectedReportData, ...) passed to
// Verify. Trust anchors live on the instance because they are pinned once
// at startup and never vary per request; the RIM blob and freshness
// challenge vary per request and stay per-call.
type NVTrustOption func(*NVTrust)
// NewNVTrust builds a configured NVTrust verifier. The result implements
// Verifier and is the production entry point; the zero value (used by
// Dispatch) is intentionally fail-closed.
//
// Registration note: this module dispatches Kinds through the switch in
// Dispatch (KindNVTrust -> NVTrust{}). There is no init()-based registry in
// the base module, so a configured verifier is wired by the caller (the kms
// release gate / ai trust-tier policy) constructing NewNVTrust and using it
// as a Verifier. If the dispatch layer later becomes an init-registry, the
// nvtrust registration is one line — register NewNVTrust(defaults) — and
// belongs in this file.
func NewNVTrust(opts ...NVTrustOption) NVTrust {
var n NVTrust // zero value: ModeLocal, no roots (fail-closed)
for _, o := range opts {
o(&n)
}
return n
}
// WithNVTrustMode selects ModeLocal (default) or ModeNRAS.
func WithNVTrustMode(m NVTrustMode) NVTrustOption {
return func(n *NVTrust) { n.mode = m }
}
// WithNVTrustDeviceRoots pins the NVIDIA device-identity root CA pool that
// the GPU attestation leaf certificate must chain to in ModeLocal. The
// operator loads NVIDIA's published device root PEM here at startup. An
// unset pool makes ModeLocal refuse — there is no insecure mode.
func WithNVTrustDeviceRoots(pool *x509.CertPool) NVTrustOption {
return func(n *NVTrust) { n.deviceRoots = pool }
}
// WithNVTrustNRASRoots pins the NRAS EAT signer public keys (looked up by
// the token's JWS kid) used in ModeNRAS.
func WithNVTrustNRASRoots(roots []nvidia.TrustRoot) NVTrustOption {
return func(n *NVTrust) {
n.nrasRoots = append([]nvidia.TrustRoot(nil), roots...)
}
}
// WithNVTrustNRASRootCAs pins the NRAS root CA pool used to validate an EAT
// token's x5c certificate chain in ModeNRAS. Optional: when set and a token
// carries x5c, the chain is anchored here and the leaf key verifies the
// token; otherwise the kid-pinned WithNVTrustNRASRoots path is used.
func WithNVTrustNRASRootCAs(pool *x509.CertPool) NVTrustOption {
return func(n *NVTrust) { n.nrasRootPool = pool }
}
// WithNVTrustClock sets the instance verification clock. Production leaves
// this unset (wall clock); tests pin it. Per-call WithNow still overrides.
func WithNVTrustClock(fn func() time.Time) NVTrustOption {
return func(n *NVTrust) { n.now = fn }
}
// ErrNVTrustNoRootPEM is returned when a device-root PEM contains no
// usable certificate.
var ErrNVTrustNoRootPEM = errors.New("nvtrust: no certificate found in device-root PEM")
// NVTrustDeviceRootsFromPEM is an operator convenience that builds a
// device-root pool from one or more concatenated PEM certificates (NVIDIA's
// published device-identity root). It refuses an empty/garbage PEM so a
// misconfiguration fails loud at startup rather than silently producing an
// empty pool.
func NVTrustDeviceRootsFromPEM(pemBytes []byte) (*x509.CertPool, error) {
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(pemBytes) {
return nil, ErrNVTrustNoRootPEM
}
return pool, nil
}
+588 -102
View File
@@ -6,54 +6,302 @@ package attest
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"math/big"
"testing"
"time"
"github.com/luxfi/cc/attest/nvidia"
)
// nvTrustFixture builds a matched (GPU report, signed RIM, trust roots)
// triple. The RIM is signed with a fresh ed25519 key; the report's
// measurements equal the RIM golden values so RIM.Match passes. Callers
// mutate the returned bytes to drive negative cases.
func nvTrustFixture(t *testing.T) (report, rim []byte, roots []nvidia.TrustRoot, pub ed25519.PublicKey, priv ed25519.PrivateKey) {
t.Helper()
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("genkey: %v", err)
}
roots = []nvidia.TrustRoot{{KeyID: "nvidia-rim-1", Public: pub}}
entries := []nvidia.RIMEntry{
{Name: "FW_RUNTIME", ValueHex: "deadbeef00112233"},
{Name: "VBIOS_RT", ValueHex: "cafebabe44556677"},
}
rim = signRIMEd25519(t, priv, "nvidia-rim-1", "Hopper", "535.104.05", "96.00.74.00.01", entries)
report = []byte(`{
"evidence_version": "2.1",
"gpu_uuid": "GPU-1234-5678",
"architecture": "Hopper",
"driver_version": "535.104.05",
"vbios_version": "96.00.74.00.01",
"nonce": "` + hex.EncodeToString(make([]byte, 32)) + `",
"measurements": [
{"pcr_index": 0, "name": "FW_RUNTIME", "value": "deadbeef00112233"},
{"pcr_index": 1, "name": "VBIOS_RT", "value": "cafebabe44556677"}
],
"attestation_quote": "AAAA",
"nvswitch_present": false
}`)
return report, rim, roots, pub, priv
// =============================================================================
// Self-consistent ModeLocal vector: a real SPDM-signed measurement report,
// a device cert chain (root -> intermediate -> leaf, ECDSA P-384) that the
// leaf key signs the report under, and an NVIDIA-signed RIM whose index-
// keyed golden values equal the signed measurements.
//
// The verification logic exercised is production-real (real ECDSA P-384
// over the real DSP0274 transcript, real X.509 path validation, real RIM
// signature + index match). Only the trust anchor is a test root, exactly
// as go-sev-guest's tests use a test signer and production pins AMD's root.
// =============================================================================
const (
testFixedNow = "2026-06-27T12:00:00Z"
testArch = "Hopper"
testDriver = "535.104.05"
testVBIOS = "96.00.74.00.01"
testLeafSerial = 0x4e56494449413031 // "NVIDIA01" as an int serial
)
// localVector is everything a ModeLocal Verify needs, plus the knobs to
// tamper for negative cases.
type localVector struct {
evidence []byte // the GPU evidence envelope JSON
rim []byte // signed RIM
rimRoots []nvidia.TrustRoot
devRoots *x509.CertPool
nonce [32]byte
leafCert *x509.Certificate
leafPriv *ecdsa.PrivateKey
intermPEM string
now time.Time
}
// signRIMEd25519 reproduces the nvidia RIM wire format: the signature is
// over json.Marshal of the body (architecture, driver_version,
// vbios_version, entries — in that field order, matching the package's
// canonicalization), and the signed blob appends signer_key_id + signature.
func mustTime(t *testing.T, s string) time.Time {
t.Helper()
tm, err := time.Parse(time.RFC3339, s)
if err != nil {
t.Fatalf("parse time: %v", err)
}
return tm
}
func u8ptr(v uint8) *uint8 { return &v }
func mkCert(t *testing.T, tmpl, parent *x509.Certificate, pub crypto.PublicKey, signer crypto.Signer) *x509.Certificate {
t.Helper()
der, err := x509.CreateCertificate(rand.Reader, tmpl, parent, pub, signer)
if err != nil {
t.Fatalf("create cert: %v", err)
}
c, err := x509.ParseCertificate(der)
if err != nil {
t.Fatalf("parse cert: %v", err)
}
return c
}
func certPEM(c *x509.Certificate) string {
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw}))
}
func sha256Sum(b []byte) []byte {
h := sha256.Sum256(b)
return h[:]
}
func le16(v int) []byte {
b := make([]byte, 2)
binary.LittleEndian.PutUint16(b, uint16(v))
return b
}
func le24(v int) []byte {
return []byte{byte(v), byte(v >> 8), byte(v >> 16)}
}
// joseConcat encodes (r,s) as fixed-width big-endian r||s (the SPDM/JOSE form).
func joseConcatLocal(r, s *big.Int, size int) []byte {
out := make([]byte, size*2)
rB := r.Bytes()
sB := s.Bytes()
copy(out[size-len(rB):size], rB)
copy(out[size*2-len(sB):], sB)
return out
}
// buildSPDMRequest builds a GET_MEASUREMENTS (0xE0) request asking for all
// blocks with a signature, carrying the requester nonce.
func buildSPDMRequest(version byte, nonce [32]byte) []byte {
req := []byte{version, 0xE0, 0x01 /*sig requested*/, 0xFF /*all blocks*/}
req = append(req, nonce[:]...)
req = append(req, 0x00) // SlotIDParam
return req
}
type measBlock struct {
index uint8
value []byte // 48-byte digest
}
// buildSignedSPDMResponse builds a MEASUREMENTS (0x60) response over the
// given blocks and signs the DSP0274 transcript (request || responseBody)
// with leafPriv. Returns the full response message (body || signature).
func buildSignedSPDMResponse(t *testing.T, version byte, blocks []measBlock, respNonce [32]byte, leafPriv *ecdsa.PrivateKey, requestMsg []byte) []byte {
t.Helper()
var record []byte
for _, b := range blocks {
dmtf := []byte{0x00} // value type: digest (bit7=0), type 0
dmtf = append(dmtf, le16(len(b.value))...)
dmtf = append(dmtf, b.value...)
blk := []byte{b.index, 0x01 /*DMTF spec*/}
blk = append(blk, le16(len(dmtf))...)
blk = append(blk, dmtf...)
record = append(record, blk...)
}
body := []byte{version, 0x60, 0x00, 0x00, byte(len(blocks))}
body = append(body, le24(len(record))...)
body = append(body, record...)
body = append(body, respNonce[:]...)
body = append(body, le16(0)...) // opaque length 0
digest := spdmDigestForTest(version, requestMsg, body)
r, s, err := ecdsa.Sign(rand.Reader, leafPriv, digest)
if err != nil {
t.Fatalf("spdm sign: %v", err)
}
sig := joseConcatLocal(r, s, 48) // P-384 -> 96-byte r||s
return append(body, sig...)
}
// spdmDigestForTest independently reproduces the DSP0274 transcript digest
// the verifier checks (SHA-384). 1.1: SHA384(req||body). 1.2: SHA384(ctx ||
// SHA384(req||body)).
func spdmDigestForTest(version byte, requestMsg, body []byte) []byte {
h := sha512.New384()
h.Write(requestMsg)
h.Write(body)
th := h.Sum(nil)
if version < 0x12 {
return th
}
const prefixUnit = "dmtf-spdm-v1.2.*"
const context = "responder-measurements signing"
ctx := make([]byte, 100)
for i := 0; i < 4; i++ {
copy(ctx[i*16:(i+1)*16], prefixUnit)
}
copy(ctx[100-len(context):], context)
h2 := sha512.New384()
h2.Write(ctx)
h2.Write(th)
return h2.Sum(nil)
}
// buildLocalVector assembles a complete, valid ModeLocal vector for the
// given SPDM version.
func buildLocalVector(t *testing.T, version byte) localVector {
t.Helper()
now := mustTime(t, testFixedNow)
// --- device chain: root -> intermediate -> leaf (all P-384) ---
rootPriv, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
intermPriv, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
leafPriv, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
rootTmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "NVIDIA Test Device Identity Root"},
NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(24 * time.Hour),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
}
rootCert := mkCert(t, rootTmpl, rootTmpl, rootPriv.Public(), rootPriv)
intermTmpl := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: "NVIDIA Test GPU Attestation CA"},
NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(24 * time.Hour),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
}
intermCert := mkCert(t, intermTmpl, rootCert, intermPriv.Public(), rootPriv)
leafTmpl := &x509.Certificate{
SerialNumber: big.NewInt(testLeafSerial),
Subject: pkix.Name{CommonName: "GPU-TEST-0001"},
NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
}
leafCert := mkCert(t, leafTmpl, intermCert, leafPriv.Public(), intermPriv)
rootPool := x509.NewCertPool()
rootPool.AddCert(rootCert)
// --- SPDM request + signed response ---
var nonce [32]byte
for i := range nonce {
nonce[i] = byte(i + 1)
}
var respNonce [32]byte
for i := range respNonce {
respNonce[i] = byte(0xA0 + i)
}
val0 := bytes.Repeat([]byte{0x11}, 48)
val1 := bytes.Repeat([]byte{0x22}, 48)
req := buildSPDMRequest(version, nonce)
resp := buildSignedSPDMResponse(t, version, []measBlock{
{index: 0, value: val0},
{index: 1, value: val1},
}, respNonce, leafPriv, req)
// --- RIM: index-keyed golden values == the signed measurements ---
rimPub, rimPriv, _ := ed25519.GenerateKey(rand.Reader)
rimRoots := []nvidia.TrustRoot{{KeyID: "nvidia-rim-1", Public: rimPub}}
entries := []nvidia.RIMEntry{
{Name: "FW_RT", ValueHex: hex.EncodeToString(val0), Index: u8ptr(0)},
{Name: "VBIOS_RT", ValueHex: hex.EncodeToString(val1), Index: u8ptr(1)},
}
rim := signRIMEd25519(t, rimPriv, "nvidia-rim-1", testArch, testDriver, testVBIOS, entries)
// --- evidence envelope ---
env := map[string]any{
"evidence_version": "2.1",
"gpu_uuid": "GPU-TEST-0001",
"architecture": testArch,
"driver_version": testDriver,
"vbios_version": testVBIOS,
"nonce": hex.EncodeToString(nonce[:]),
"cert_chain": []string{certPEM(leafCert), certPEM(intermCert)},
"spdm_request": base64.StdEncoding.EncodeToString(req),
"spdm_response": base64.StdEncoding.EncodeToString(resp),
"spdm_asym_algo": "ecdsa-p384-sha384",
"measurements": []map[string]any{
{"pcr_index": 0, "name": "FW_RT", "value": hex.EncodeToString(val0)},
{"pcr_index": 1, "name": "VBIOS_RT", "value": hex.EncodeToString(val1)},
},
}
evidence, err := json.Marshal(env)
if err != nil {
t.Fatalf("marshal envelope: %v", err)
}
return localVector{
evidence: evidence,
rim: rim,
rimRoots: rimRoots,
devRoots: rootPool,
nonce: nonce,
leafCert: leafCert,
leafPriv: leafPriv,
intermPEM: certPEM(intermCert),
now: now,
}
}
func (v localVector) verifier() NVTrust {
return NewNVTrust(
WithNVTrustDeviceRoots(v.devRoots),
WithNVTrustClock(func() time.Time { return v.now }),
)
}
// =============================================================================
// signRIMEd25519 — reproduces the nvidia RIM wire format (canonical body =
// json.Marshal of architecture/driver/vbios/entries; signed blob appends
// signer_key_id + signature). Entries may carry an index (omitempty).
// =============================================================================
func signRIMEd25519(t *testing.T, priv ed25519.PrivateKey, kid, arch, driver, vbios string, entries []nvidia.RIMEntry) []byte {
t.Helper()
body := struct {
@@ -82,115 +330,353 @@ func signRIMEd25519(t *testing.T, priv ed25519.PrivateKey, kid, arch, driver, vb
return out
}
func TestNVTrust_LocalRIM_HappyPath(t *testing.T) {
report, rim, roots, _, _ := nvTrustFixture(t)
// =============================================================================
// ModeLocal happy paths
// =============================================================================
rep, err := Dispatch(context.Background(), KindNVTrust, report,
WithNVTrustRIM(rim), WithNVTrustTrustRoots(roots))
func TestNVTrustLocal_HappyPath_SPDM11(t *testing.T) {
v := buildLocalVector(t, 0x11)
rep, err := v.verifier().Verify(context.Background(), v.evidence,
WithNVTrustRIM(v.rim), WithNVTrustTrustRoots(v.rimRoots),
WithExpectedReportData(v.nonce[:]))
if err != nil {
t.Fatalf("verify: %v", err)
}
if rep.Kind != KindNVTrust {
t.Fatalf("kind = %q, want %q", rep.Kind, KindNVTrust)
if rep.Kind != KindNVTrust || rep.Vendor != "nvidia.nvtrust.local" {
t.Fatalf("kind/vendor = %q/%q", rep.Kind, rep.Vendor)
}
if rep.Vendor != "nvidia.nvtrust" {
t.Fatalf("vendor = %q", rep.Vendor)
// ReportData is the SIGNED requester nonce.
if !bytes.Equal(rep.ReportData, v.nonce[:]) {
t.Fatalf("report_data = %x want %x", rep.ReportData, v.nonce[:])
}
if string(rep.ChipID) != "GPU-1234-5678" {
t.Fatalf("chip_id = %q", string(rep.ChipID))
// ChipID is the trusted leaf serial, NOT the envelope UUID.
if !bytes.Equal(rep.ChipID, big.NewInt(testLeafSerial).Bytes()) {
t.Fatalf("chip_id = %x want %x", rep.ChipID, big.NewInt(testLeafSerial).Bytes())
}
// ReportData is the 32-byte nonce (all zero in the fixture).
if len(rep.ReportData) != 32 || !bytes.Equal(rep.ReportData, make([]byte, 32)) {
t.Fatalf("report_data = %x", rep.ReportData)
}
// Measurement is the signed RIM measurement root.
parsedRIM, err := nvidia.ParseAndVerifyRIM(rim, roots)
// Measurement equals the signed-RIM measurement root.
parsed, err := nvidia.ParseAndVerifyRIM(v.rim, v.rimRoots)
if err != nil {
t.Fatalf("re-parse rim: %v", err)
t.Fatalf("reparse rim: %v", err)
}
wantRoot := parsedRIM.MeasurementRoot()
wantRoot := parsed.MeasurementRoot()
if !bytes.Equal(rep.Measurement, wantRoot[:]) {
t.Fatalf("measurement = %x, want %x", rep.Measurement, wantRoot[:])
t.Fatalf("measurement = %x want %x", rep.Measurement, wantRoot[:])
}
if rep.CompositeHash == [32]byte{} {
t.Fatal("composite hash must not be zero")
}
if rep.Extra["nvtrust.driver_version"] != "535.104.05" {
t.Fatalf("extra driver = %q", rep.Extra["nvtrust.driver_version"])
if rep.Extra["nvtrust.mode"] != "local" {
t.Fatalf("extra mode = %q", rep.Extra["nvtrust.mode"])
}
if rep.Extra["nvtrust.rim_signer"] != "nvidia-rim-1" {
t.Fatalf("extra rim_signer = %q", rep.Extra["nvtrust.rim_signer"])
if rep.Extra["nvtrust.spdm_asym_algo"] != "ecdsa-p384-sha384" {
t.Fatalf("extra algo = %q", rep.Extra["nvtrust.spdm_asym_algo"])
}
}
func TestNVTrust_RefusesWithoutRIM(t *testing.T) {
report, _, roots, _, _ := nvTrustFixture(t)
_, err := Dispatch(context.Background(), KindNVTrust, report,
WithNVTrustTrustRoots(roots))
func TestNVTrustLocal_HappyPath_SPDM12(t *testing.T) {
v := buildLocalVector(t, 0x12)
_, err := v.verifier().Verify(context.Background(), v.evidence,
WithNVTrustRIM(v.rim), WithNVTrustTrustRoots(v.rimRoots))
if err != nil {
t.Fatalf("verify 1.2: %v", err)
}
}
// =============================================================================
// ModeLocal fail-closed / tamper rejection
// =============================================================================
// Dispatch uses the zero value (no pinned device root) -> must refuse.
func TestNVTrustLocal_Dispatch_FailClosed(t *testing.T) {
v := buildLocalVector(t, 0x11)
_, err := Dispatch(context.Background(), KindNVTrust, v.evidence,
WithNVTrustRIM(v.rim), WithNVTrustTrustRoots(v.rimRoots))
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy (no insecure mode)", err)
t.Fatalf("err = %v, want ErrPolicy (no device root)", err)
}
}
func TestNVTrust_RefusesWithoutTrustRoots(t *testing.T) {
report, rim, _, _, _ := nvTrustFixture(t)
_, err := Dispatch(context.Background(), KindNVTrust, report,
WithNVTrustRIM(rim))
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy (no insecure mode)", err)
func TestNVTrustLocal_RejectsTamperedSPDMSignature(t *testing.T) {
v := buildLocalVector(t, 0x11)
// Flip a byte inside the base64 spdm_response so the signature breaks.
var env map[string]any
if err := json.Unmarshal(v.evidence, &env); err != nil {
t.Fatal(err)
}
resp, _ := base64.StdEncoding.DecodeString(env["spdm_response"].(string))
resp[len(resp)-1] ^= 0xFF // last byte of the signature
env["spdm_response"] = base64.StdEncoding.EncodeToString(resp)
tampered, _ := json.Marshal(env)
_, err := v.verifier().Verify(context.Background(), tampered,
WithNVTrustRIM(v.rim), WithNVTrustTrustRoots(v.rimRoots))
if !errors.Is(err, ErrSignatureInvalid) {
t.Fatalf("err = %v, want ErrSignatureInvalid", err)
}
}
func TestNVTrust_RejectsTamperedRIMSignature(t *testing.T) {
report, rim, roots, _, _ := nvTrustFixture(t)
// Flip a measurement value byte in the RIM body — the ed25519
// signature no longer covers the manifest. Must be a CHAIN failure
// (the golden values are not vendor-authentic), not silently accepted.
tampered := bytes.Replace(rim, []byte("deadbeef00112233"), []byte("deadbeef00112234"), 1)
if bytes.Equal(tampered, rim) {
t.Fatal("fixture did not mutate")
func TestNVTrustLocal_RejectsTamperedMeasurementInSignedRecord(t *testing.T) {
v := buildLocalVector(t, 0x11)
// Flip a measurement byte in the signed record (not the signature). The
// SPDM signature no longer covers the record -> signature failure.
var env map[string]any
json.Unmarshal(v.evidence, &env)
resp, _ := base64.StdEncoding.DecodeString(env["spdm_response"].(string))
// The first measurement digest sits well inside the record; flipping any
// record byte invalidates the signature.
resp[12] ^= 0x01
env["spdm_response"] = base64.StdEncoding.EncodeToString(resp)
tampered, _ := json.Marshal(env)
_, err := v.verifier().Verify(context.Background(), tampered,
WithNVTrustRIM(v.rim), WithNVTrustTrustRoots(v.rimRoots))
if !errors.Is(err, ErrSignatureInvalid) {
t.Fatalf("err = %v, want ErrSignatureInvalid", err)
}
_, err := Dispatch(context.Background(), KindNVTrust, report,
WithNVTrustRIM(tampered), WithNVTrustTrustRoots(roots))
}
func TestNVTrustLocal_RejectsWrongDeviceRoot(t *testing.T) {
v := buildLocalVector(t, 0x11)
// A device-root pool that does NOT contain the chain's root.
otherPriv, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
otherTmpl := &x509.Certificate{
SerialNumber: big.NewInt(99),
Subject: pkix.Name{CommonName: "Unrelated Root"},
NotBefore: v.now.Add(-time.Hour),
NotAfter: v.now.Add(time.Hour),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
otherRoot := mkCert(t, otherTmpl, otherTmpl, otherPriv.Public(), otherPriv)
pool := x509.NewCertPool()
pool.AddCert(otherRoot)
verifier := NewNVTrust(
WithNVTrustDeviceRoots(pool),
WithNVTrustClock(func() time.Time { return v.now }),
)
_, err := verifier.Verify(context.Background(), v.evidence,
WithNVTrustRIM(v.rim), WithNVTrustTrustRoots(v.rimRoots))
if !errors.Is(err, ErrChainInvalid) {
t.Fatalf("err = %v, want ErrChainInvalid", err)
}
}
func TestNVTrust_RejectsMeasurementMismatch(t *testing.T) {
report, _, roots, _, priv := nvTrustFixture(t)
// A cryptographically VALID RIM whose golden value differs from what
// the GPU reports — must be a POLICY refusal (signed, but the GPU's
// actual measurement is not covered).
badEntries := []nvidia.RIMEntry{
{Name: "FW_RUNTIME", ValueHex: "0000000000000000"},
{Name: "VBIOS_RT", ValueHex: "cafebabe44556677"},
func TestNVTrustLocal_RejectsRIMMismatch(t *testing.T) {
v := buildLocalVector(t, 0x11)
// A cryptographically valid RIM whose index-0 golden differs from the
// signed measurement -> POLICY refusal (signed, but does not match).
rimPub2, rimPriv2, _ := ed25519.GenerateKey(rand.Reader)
roots2 := []nvidia.TrustRoot{{KeyID: "nvidia-rim-2", Public: rimPub2}}
bad := []nvidia.RIMEntry{
{Name: "FW_RT", ValueHex: hex.EncodeToString(bytes.Repeat([]byte{0xEE}, 48)), Index: u8ptr(0)},
{Name: "VBIOS_RT", ValueHex: hex.EncodeToString(bytes.Repeat([]byte{0x22}, 48)), Index: u8ptr(1)},
}
badRIM := signRIMEd25519(t, priv, "nvidia-rim-1", "Hopper", "535.104.05", "96.00.74.00.01", badEntries)
_, err := Dispatch(context.Background(), KindNVTrust, report,
WithNVTrustRIM(badRIM), WithNVTrustTrustRoots(roots))
badRIM := signRIMEd25519(t, rimPriv2, "nvidia-rim-2", testArch, testDriver, testVBIOS, bad)
_, err := v.verifier().Verify(context.Background(), v.evidence,
WithNVTrustRIM(badRIM), WithNVTrustTrustRoots(roots2))
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy", err)
}
}
func TestNVTrust_RejectsBadEvidence(t *testing.T) {
_, rim, roots, _, _ := nvTrustFixture(t)
_, err := Dispatch(context.Background(), KindNVTrust, []byte("{not json"),
WithNVTrustRIM(rim), WithNVTrustTrustRoots(roots))
func TestNVTrustLocal_RejectsTamperedRIMSignature(t *testing.T) {
v := buildLocalVector(t, 0x11)
tampered := bytes.Replace(v.rim,
[]byte(hex.EncodeToString(bytes.Repeat([]byte{0x11}, 48))),
[]byte(hex.EncodeToString(bytes.Repeat([]byte{0x33}, 48))), 1)
if bytes.Equal(tampered, v.rim) {
t.Fatal("rim did not mutate")
}
_, err := v.verifier().Verify(context.Background(), v.evidence,
WithNVTrustRIM(tampered), WithNVTrustTrustRoots(v.rimRoots))
if !errors.Is(err, ErrChainInvalid) {
t.Fatalf("err = %v, want ErrChainInvalid", err)
}
}
func TestNVTrustLocal_RejectsWrongExpectedNonce(t *testing.T) {
v := buildLocalVector(t, 0x11)
wrong := make([]byte, 32)
wrong[0] = 0xFF
_, err := v.verifier().Verify(context.Background(), v.evidence,
WithNVTrustRIM(v.rim), WithNVTrustTrustRoots(v.rimRoots),
WithExpectedReportData(wrong))
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy (nonce binding)", err)
}
}
func TestNVTrustLocal_RejectsEnvelopeNonceMismatch(t *testing.T) {
v := buildLocalVector(t, 0x11)
// Change the envelope nonce so it no longer equals the SIGNED request
// nonce -> ErrPolicy (consistency).
var env map[string]any
json.Unmarshal(v.evidence, &env)
other := make([]byte, 32)
other[31] = 0x09
env["nonce"] = hex.EncodeToString(other)
tampered, _ := json.Marshal(env)
_, err := v.verifier().Verify(context.Background(), tampered,
WithNVTrustRIM(v.rim), WithNVTrustTrustRoots(v.rimRoots))
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy", err)
}
}
func TestNVTrustLocal_RejectsMissingSPDM(t *testing.T) {
v := buildLocalVector(t, 0x11)
var env map[string]any
json.Unmarshal(v.evidence, &env)
delete(env, "spdm_response")
delete(env, "attestation_quote")
noSPDM, _ := json.Marshal(env)
_, err := v.verifier().Verify(context.Background(), noSPDM,
WithNVTrustRIM(v.rim), WithNVTrustTrustRoots(v.rimRoots))
if !errors.Is(err, ErrInvalidEvidence) {
t.Fatalf("err = %v, want ErrInvalidEvidence", err)
}
}
func TestNVTrust_EnforcesExpectedNonce(t *testing.T) {
report, rim, roots, _, _ := nvTrustFixture(t)
wrongNonce := make([]byte, 32)
wrongNonce[0] = 0xFF
_, err := Dispatch(context.Background(), KindNVTrust, report,
WithNVTrustRIM(rim), WithNVTrustTrustRoots(roots),
WithExpectedReportData(wrongNonce))
func TestNVTrustLocal_RefusesWithoutRIM(t *testing.T) {
v := buildLocalVector(t, 0x11)
_, err := v.verifier().Verify(context.Background(), v.evidence,
WithNVTrustTrustRoots(v.rimRoots))
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy (nonce binding)", err)
t.Fatalf("err = %v, want ErrPolicy", err)
}
}
// =============================================================================
// ModeNRAS: EAT/JWT verification
// =============================================================================
func b64url(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) }
// buildEATES256 builds an ES256-signed EAT compact JWS over the claims.
func buildEATES256(t *testing.T, priv *ecdsa.PrivateKey, kid string, claims map[string]any) string {
t.Helper()
hdr := map[string]string{"alg": "ES256", "kid": kid}
hb, _ := json.Marshal(hdr)
cb, _ := json.Marshal(claims)
signedInput := b64url(hb) + "." + b64url(cb)
sum := sha256Sum([]byte(signedInput))
r, s, err := ecdsa.Sign(rand.Reader, priv, sum)
if err != nil {
t.Fatalf("eat sign: %v", err)
}
sig := joseConcatLocal(r, s, 32)
return signedInput + "." + b64url(sig)
}
func nrasVerifier(t *testing.T, pub *ecdsa.PublicKey, kid string, now time.Time) NVTrust {
return NewNVTrust(
WithNVTrustMode(ModeNRAS),
WithNVTrustNRASRoots([]nvidia.TrustRoot{{KeyID: kid, Public: pub}}),
WithNVTrustClock(func() time.Time { return now }),
)
}
func TestNVTrustNRAS_HappyPath(t *testing.T) {
now := mustTime(t, testFixedNow)
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
var nonce [32]byte
for i := range nonce {
nonce[i] = byte(i + 1)
}
measRoot := bytes.Repeat([]byte{0x5A}, 32)
token := buildEATES256(t, priv, "nras-1", map[string]any{
"eat_nonce": hex.EncodeToString(nonce[:]),
"exp": now.Add(time.Hour).Unix(),
"iat": now.Add(-time.Minute).Unix(),
"x-nvidia-overall-att-result": true,
"x-nvidia-gpu-measurements-root": hex.EncodeToString(measRoot),
})
rep, err := nrasVerifier(t, &priv.PublicKey, "nras-1", now).Verify(
context.Background(), []byte(token), WithExpectedReportData(nonce[:]))
if err != nil {
t.Fatalf("nras verify: %v", err)
}
if rep.Vendor != "nvidia.nvtrust.nras" {
t.Fatalf("vendor = %q", rep.Vendor)
}
if !bytes.Equal(rep.ReportData, nonce[:]) {
t.Fatalf("report_data = %x want %x", rep.ReportData, nonce[:])
}
if !bytes.Equal(rep.Measurement, measRoot) {
t.Fatalf("measurement = %x want %x", rep.Measurement, measRoot)
}
if rep.Extra["nvtrust.nras_result"] != "true" {
t.Fatalf("nras_result = %q", rep.Extra["nvtrust.nras_result"])
}
}
func TestNVTrustNRAS_RejectsBadSignature(t *testing.T) {
now := mustTime(t, testFixedNow)
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
token := buildEATES256(t, priv, "nras-1", map[string]any{
"exp": now.Add(time.Hour).Unix(),
})
// Corrupt the final signature segment.
bad := token[:len(token)-4] + "AAAA"
_, err := nrasVerifier(t, &priv.PublicKey, "nras-1", now).Verify(
context.Background(), []byte(bad))
if !errors.Is(err, ErrSignatureInvalid) {
t.Fatalf("err = %v, want ErrSignatureInvalid", err)
}
}
func TestNVTrustNRAS_RejectsExpired(t *testing.T) {
now := mustTime(t, testFixedNow)
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
token := buildEATES256(t, priv, "nras-1", map[string]any{
"exp": now.Add(-time.Hour).Unix(),
})
_, err := nrasVerifier(t, &priv.PublicKey, "nras-1", now).Verify(
context.Background(), []byte(token))
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy (expired)", err)
}
}
func TestNVTrustNRAS_RejectsNonceMismatch(t *testing.T) {
now := mustTime(t, testFixedNow)
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
var nonce [32]byte
nonce[0] = 0x01
token := buildEATES256(t, priv, "nras-1", map[string]any{
"eat_nonce": hex.EncodeToString(nonce[:]),
"exp": now.Add(time.Hour).Unix(),
})
wrong := make([]byte, 32)
wrong[0] = 0x02
_, err := nrasVerifier(t, &priv.PublicKey, "nras-1", now).Verify(
context.Background(), []byte(token), WithExpectedReportData(wrong))
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy (nonce)", err)
}
}
func TestNVTrustNRAS_RejectsNegativeResult(t *testing.T) {
now := mustTime(t, testFixedNow)
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
token := buildEATES256(t, priv, "nras-1", map[string]any{
"exp": now.Add(time.Hour).Unix(),
"x-nvidia-overall-att-result": false,
})
_, err := nrasVerifier(t, &priv.PublicKey, "nras-1", now).Verify(
context.Background(), []byte(token))
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy (negative result)", err)
}
}
func TestNVTrustNRAS_RefusesWithoutTrust(t *testing.T) {
now := mustTime(t, testFixedNow)
v := NewNVTrust(WithNVTrustMode(ModeNRAS), WithNVTrustClock(func() time.Time { return now }))
_, err := v.Verify(context.Background(), []byte("x.y.z"))
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy (no NRAS trust)", err)
}
}