Files
zeekay b9686b5837 decomplect(attest): one registry, no switch — every Kind self-registers
Fold Dispatch's hard Kind switch into the init() registry SGX introduced.
The registry (registry map, registerVerifier, RegisteredVerifier) moves out
of sgx.go into verifier.go as framework; Dispatch becomes one registry
lookup, fail-closed (ErrUnsupportedKind) for an unregistered Kind. The
switch — including Nitro's added case — is deleted: there is now exactly one
routing path.

Each Kind file is self-contained: its string const, its Verifier impl, its
per-call options, and its 'func init() { registerVerifier(...) }' live
together. SEVSNP/TDX/NVTrust/Nitro consts move out of verifier.go beside
their verifiers (SGX already did this); NVTrust's per-call WithNVTrustRIM /
WithNVTrustTrustRoots move to nvtrust.go for symmetry with WithNitro*. The
shared config keeps the per-Kind per-call policy fields (kdsGetter is shared
by SEV+TDX; nvtrust RIM/roots; nitro roots/PCRs/maxAge); NVTrust's
deployment trust anchors stay on the struct via NewNVTrust. NVTrust
registers its fail-closed zero value, matching prior Dispatch semantics.

registerVerifier panics on a duplicate Kind — a duplicate registration is a
programmer error that must fail loud at startup, never silently shadow a
verifier on the trust path.

doc.go: drop the now-false TDX/NVTrust STUB labels (both real), document all
five shipped Kinds + the registry + the pending ARMCCA slot, and the TDX
MRTD+RTMR measurement fold. verifier_test.go: retire the stub tests (done by
the TDX merge) and add TestRegistry_RoutesEveryKind asserting registry
routing for all five Kinds and fail-closed for the unregistered armcca slot.

go build/vet/test + go test -race ./attest/... all green; gofmt clean.
2026-06-27 14:17:24 -07:00

387 lines
15 KiB
Go

// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package attest
import (
"context"
"crypto/sha256"
"crypto/subtle"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"time"
"github.com/luxfi/cc/attest/nvidia"
)
// KindNVTrust is the NVIDIA GPU confidential-compute framing: either a signed
// SPDM measurement record plus device cert chain (ModeLocal) or an NVIDIA
// Remote Attestation Service EAT token (ModeNRAS).
const KindNVTrust Kind = "nvtrust"
// NVTrust is the NVIDIA GPU confidential-compute attestation verifier. It
// implements both NVIDIA trust models behind one Kind, selected by Mode:
//
// - 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.
//
// - 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).
//
// 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.
//
// 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 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
}
// 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)
report, err := nvidia.ParseGPUReport(evidence)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
}
// 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)
}
rim, err := nvidia.ParseAndVerifyRIM(cfg.nvtrustRIM, cfg.nvtrustTrustRoots)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrChainInvalid, err)
}
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()
// 5) Caller policy: freshness nonce and measurement-root pin.
if cfg.expectedReportData != nil {
if subtle.ConstantTimeCompare(cfg.expectedReportData, reqNonce[:]) != 1 {
return nil, fmt.Errorf("%w: nonce mismatch (got %s)", ErrPolicy, hex.EncodeToString(reqNonce[:]))
}
}
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[:]))
}
}
out := &VerifiedReport{
Kind: KindNVTrust,
Vendor: "nvidia.nvtrust.local",
Measurement: append([]byte(nil), measRoot[:]...),
ReportData: append([]byte(nil), reqNonce[:]...),
ChipID: leaf.SerialNumber.Bytes(),
IssuedAt: now.UTC(),
Extra: buildNVTrustLocalExtra(report, rim, leaf, algo),
}
out.CompositeHash = computeCompositeHash(KindNVTrust, canonicalNVTrustLocalBytes(leaf, reqNonce, measRoot, rim.SignerKeyID, spdm.Signature))
return out, nil
}
// 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
}
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(leaf.RawSubjectPublicKeyInfo)
appendField(nonce[:])
appendField(measRoot[:])
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
}
// -----------------------------------------------------------------------------
// Per-call policy options. These set fields on the shared config for a single
// Verify call. They are distinct from the construction-time NVTrustOptions in
// nvtrust_options.go, which pin per-deployment trust anchors on the verifier:
// the RIM blob and its signing roots roll as drivers/VBIOS update, so they are
// supplied per request, not pinned once at startup.
// -----------------------------------------------------------------------------
// WithNVTrustRIM supplies the signed NVIDIA Reference Integrity Manifest that
// KindNVTrust evidence is matched against in ModeLocal. The RIM's detached
// signature is verified against the roots from WithNVTrustTrustRoots before any
// measurement is compared. Required for ModeLocal.
func WithNVTrustRIM(rim []byte) Option {
return func(c *config) {
buf := make([]byte, len(rim))
copy(buf, rim)
c.nvtrustRIM = buf
}
}
// WithNVTrustTrustRoots pins the public keys that may sign the NVIDIA RIM. The
// operator wires their NVIDIA RIM signing key(s) here. An empty set causes
// ModeLocal to refuse — there is no insecure mode.
func WithNVTrustTrustRoots(roots []nvidia.TrustRoot) Option {
return func(c *config) {
c.nvtrustTrustRoots = append([]nvidia.TrustRoot(nil), roots...)
}
}
func init() { registerVerifier(KindNVTrust, NVTrust{}) }
// Compile-time guard: NVTrust satisfies Verifier.
var _ Verifier = NVTrust{}