mirror of
https://github.com/luxfi/cc.git
synced 2026-09-24 00:24:54 +00:00
The prior KindNVTrust local path was cryptographically hollow: it matched
PLAINTEXT JSON measurements against the (public) RIM and never parsed or
verified the GPU-signed SPDM report or the device certificate chain — a
host could echo golden values and pass. This makes it production-real.
ModeLocal (cloud-free, full hardware verify), in order, refusing on any
failure:
1. chain the GPU device leaf cert to an operator-pinned NVIDIA device-
identity root (no insecure mode; empty root => refuse);
2. verify the SPDM MEASUREMENTS (0x60) response signature over the
DSP0274 transcript (request || response[:-sig]) with the leaf key,
SPDM 1.1 and 1.2, ECDSA P-384/P-256 + RSA-PSS, with a strict
algo<->key-type binding (substitution defense);
3. extract measurements from the SIGNED record and match them BY INDEX
to the NVIDIA-signed RIM (completeness enforced — closes the drop-a-
bad-measurement attack);
4. bind the SIGNED requester nonce to the caller's freshness challenge.
ModeNRAS (remote): verify an NVIDIA NRAS EAT/JWT — JWS signature against
pinned signer keys (or x5c chain to a pinned NRAS root), exp/nbf freshness,
eat_nonce binding, positive overall-result. No network in Verify (the POST
stays in NRASClient).
GPU measurement -> VerifiedReport.Measurement (RIM root / EAT root); trust-
score/tier stays caller policy on top. ChipID is the trusted leaf serial,
not the untrusted envelope UUID. Trust anchors are operator-pinned via
NewNVTrust options, never from evidence; the zero value (Dispatch) is fail-
closed. No new dependencies (stdlib crypto only).
New: attest/nvidia/{spdm,devicechain,eat}.go. Extended: gpu_report.go
(spdm_request/response/asym_algo), rim.go (index-keyed MatchSPDM). 30 new
tests: self-consistent SPDM+chain+RIM vectors (1.1 and 1.2), NRAS EAT
vectors, and tamper rejection (bad SPDM sig, tampered signed record, wrong
device root, RIM mismatch, RIM sig tamper, nonce mismatch, missing SPDM,
bad/expired/negative EAT). go build + go test ./... green (race-clean).
161 lines
5.4 KiB
Go
161 lines
5.4 KiB
Go
// 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)
|
|
}
|
|
}
|