refactor(attest): consume the github.com/luxfi/cc leaf; KindNRAS→KindNVTrust

The canonical attestation verifier (cc/attest + pkg/attestation/nvidia) was
extracted into the orthogonal leaf module github.com/luxfi/cc so mpc, the TEE
custody extension, and the confidential-AI system can share ONE verifier
coupled to none of their domains.

- pkg/attestation/composite.go, pkg/kms/release.go: import the leaf
  github.com/luxfi/cc/attest directly; map the NVIDIA issuer to the real
  KindNVTrust (was the KindNRAS stub).
- pkg/attestation/nvidia + cc/attest/{sev,tdx,verifier,...}.go: moved to the
  leaf (clean stdlib/go-sev-guest leaves, zero mpc-internal imports).
- cc/attest now a thin TRANSITION ADAPTER: a zero-logic re-export of the leaf
  (type aliases ⇒ identical types, so the kms.CompositeAttestation release
  gate is satisfied from either import path) plus the deprecated KindNRAS
  symbol that pinned github.com/luxfi/threshold v1.9.9 still references. To be
  removed once threshold republishes against the leaf (its main already has)
  and mpc's threshold/protocols/tfhe dep is resolved.

go.mod: require github.com/luxfi/cc (local replace ../cc until tagged);
threshold stays v1.9.9 (cannot bump: main drops protocols/tfhe that pkg/mpc
needs). go vet ./... clean; attestation/kms/scheduler tests green.
This commit is contained in:
2026-06-27 12:33:40 -07:00
parent 4508cb3f2a
commit 5e04d411e2
25 changed files with 97 additions and 3155 deletions
+71
View File
@@ -0,0 +1,71 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
// Package attest is a TRANSITION ADAPTER. The canonical attestation
// verifier was extracted into the orthogonal leaf module
// github.com/luxfi/cc (one verifier, depended on by mpc/tee/ai, coupled to
// none). New code — including all mpc-internal consumers (pkg/kms,
// pkg/attestation) — imports github.com/luxfi/cc/attest directly.
//
// This package exists ONLY so the single external consumer that is still
// pinned to a pre-extraction luxfi/mpc — github.com/luxfi/threshold v1.9.9,
// whose pkg/thresholdd/tee_options.go imports github.com/luxfi/mpc/cc/attest
// and references attest.KindNRAS — keeps building. threshold's main branch
// has already dropped this import (the TEE custody code moved to luxfi/tee);
// mpc cannot bump to it yet because mpc/pkg/mpc still imports
// threshold/protocols/tfhe, which that same threshold branch removed.
//
// Every symbol here is a zero-logic re-export (type aliases preserve type
// identity with the leaf — a value produced by github.com/luxfi/cc/attest is
// the SAME type as the one named here). REMOVE this package once threshold
// republishes against the leaf and mpc's tfhe dependency is resolved.
package attest
import cc "github.com/luxfi/cc/attest"
// Re-exported types. Type aliases ⇒ identical types to the leaf, so the
// kms.CompositeAttestation release-gate interface is satisfied by values
// from either import path.
type (
Kind = cc.Kind
Option = cc.Option
Verifier = cc.Verifier
VerifiedReport = cc.VerifiedReport
SEVSNP = cc.SEVSNP
TDX = cc.TDX
NVTrust = cc.NVTrust
)
// Re-exported kinds. KindNRAS is the deprecated name for the NVIDIA GPU
// kind; it is retained ONLY for threshold v1.9.9 (which references it) and
// resolves to the canonical KindNVTrust value. New code uses KindNVTrust.
const (
KindSEVSNP = cc.KindSEVSNP
KindTDX = cc.KindTDX
KindNVTrust = cc.KindNVTrust
// Deprecated: transitional alias for github.com/luxfi/threshold v1.9.9.
KindNRAS = cc.KindNVTrust
)
// Re-exported sentinel errors. Same values ⇒ errors.Is works across both
// import paths.
var (
ErrUnsupportedKind = cc.ErrUnsupportedKind
ErrInvalidEvidence = cc.ErrInvalidEvidence
ErrChainInvalid = cc.ErrChainInvalid
ErrSignatureInvalid = cc.ErrSignatureInvalid
ErrPolicy = cc.ErrPolicy
ErrNotImplemented = cc.ErrNotImplemented
)
// Re-exported constructors / entry points.
var (
Dispatch = cc.Dispatch
WithExpectedReportData = cc.WithExpectedReportData
WithExpectedMeasurement = cc.WithExpectedMeasurement
WithNow = cc.WithNow
WithKDSGetter = cc.WithKDSGetter
WithNVTrustRIM = cc.WithNVTrustRIM
WithNVTrustTrustRoots = cc.WithNVTrustTrustRoots
)
-39
View File
@@ -1,39 +0,0 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
// Package attest is the canonical TEE attestation verifier for the Lux
// confidential-compute control plane.
//
// Status:
// - SEV-SNP: PRODUCTION (live VCEK fetch from AMD KDS, full chain verify)
// - TDX: STUB (tracked at #222 stage 2; use go-tdx-guest + Intel PCS)
// - NRAS: STUB (tracked at #222 stage 3; use NRAS JWT + JWKS cache)
//
// luxcpp/crypto/attestation provides parser-only primitives. THIS package
// is where chain verification + trust anchor management lives. The two
// are connected via Go cgo binding into the C-ABI parser, then verifying
// the bytes here BEFORE accepting the parser's measurement output.
//
// Layering:
//
// caller (kms release gate, scheduler, indexer)
// └── cc/attest.Verifier.Verify(ctx, evidence, opts...) ◄── this package
// ├── SEV-SNP → google/go-sev-guest + AMD KDS (PROD)
// ├── Intel TDX → google/go-tdx-guest + Intel PCS (STUB)
// └── NVIDIA GPU → NRAS JWT + JWKS rotation (STUB)
//
// Wire format dispatch is by the leading magic / kind byte stamped by the
// C-ABI parser. The parser's output is NOT trusted until Verify returns
// without error: the parser only normalises framing; this package
// validates the chain to the vendor root and extracts measurements.
//
// Security invariants:
//
// - Trust anchors are pinned per-vendor in code (AMD ARK/ASK ship
// embedded with go-sev-guest; Intel/NVIDIA roots are fetched
// once and cached, never trusted from the evidence itself).
// - Tests do not hit the network. KDS responses are pre-fetched
// into testdata/ and replayed via a SimpleGetter map.
// - A failed verify never falls back to "best effort"; callers must
// treat (nil, err) as "refuse the request".
package attest
-48
View File
@@ -1,48 +0,0 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package attest
import (
"context"
"fmt"
)
// NRAS is the NVIDIA Remote Attestation Service verifier.
//
// Status: STUB — tracked at issue #222 stage 3.
//
// Production plan (intentionally not yet implemented):
//
// - Parse the NRAS-signed JWT (RFC 7519) carrying the GPU device-state
// report.
// - Validate the JWS signature against the NRAS signing key fetched
// from the NRAS JWKS endpoint, with a TTL'd cache and a force-rotate
// hook for emergency key rotation.
// - Verify the EAT/JWT claims: nbf, exp, nonce binding, audience.
// - Cross-check the embedded device-state report against the RIM
// (Reference Integrity Manifest) for the asserted driver+VBIOS pair
// (see pkg/attestation/nvidia/rim.go for the existing RIM type).
// - Extract the per-GPU UUID, driver version, VBIOS version, and the
// per-GPU measurement digest.
// - Compose CompositeHash via the same domain-separated SHA-256 as
// the SEV path.
//
// pkg/attestation/nvidia already implements NRAS request/response shaping
// and JWT signature verification against a caller-supplied trust root.
// Stage 3 lifts that into the cc/attest contract: a single Verify(ctx,
// evidence, opts...) entry point with an internal JWKS cache and the
// canonical VerifiedReport output shape.
//
// Verify returns ErrNotImplemented so the call site refuses the release
// without crashing the process. See TDX.Verify for the rationale.
type NRAS struct{}
// Verify implements Verifier. NOT YET IMPLEMENTED — do not enable NRAS
// dispatch in production until #222 stage 3 lands.
func (NRAS) Verify(ctx context.Context, evidence []byte, opts ...Option) (*VerifiedReport, error) {
return nil, fmt.Errorf("%w: NRAS attestation envelope verification not configured for this build (tracked at #222 stage 3: NRAS JWT + JWKS cache)",
ErrNotImplemented)
}
var _ Verifier = NRAS{}
-197
View File
@@ -1,197 +0,0 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package attest
import (
"context"
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
"github.com/google/go-sev-guest/abi"
spb "github.com/google/go-sev-guest/proto/sevsnp"
"github.com/google/go-sev-guest/verify"
"github.com/google/go-sev-guest/verify/trust"
)
// SEVSNP is the production SEV-SNP verifier. It accepts a raw 0x4A0-byte
// AMD SEV-SNP attestation report (no embedded cert chain) and:
//
// 1. Parses the report via go-sev-guest/abi.ReportToProto.
// 2. Fetches the VCEK leaf from the AMD KDS at
// kdsintf.amd.com/vcek/v1/<product>/<hwid>?... using the report's
// CHIP_ID + reported TCB. The product CA bundle (ARK→ASK) is
// embedded in go-sev-guest/verify/trust.
// 3. Validates the chain to AMD's pinned root and checks the report
// signature against the VCEK public key.
// 4. Extracts MEASUREMENT, REPORT_DATA, HOST_DATA, CHIP_ID, TCB.
// 5. Enforces caller-supplied policy options (expected report data,
// expected measurement).
//
// Production path requires network reachability to kdsintf.amd.com. In
// tests the WithKDSGetter option installs a SimpleGetter that replays
// pre-fetched bytes from testdata/.
type SEVSNP struct{}
// Verify implements Verifier for SEV-SNP raw reports.
func (SEVSNP) Verify(ctx context.Context, evidence []byte, opts ...Option) (*VerifiedReport, error) {
if len(evidence) != abi.ReportSize {
return nil, fmt.Errorf("%w: sev-snp expects %d bytes, got %d",
ErrInvalidEvidence, abi.ReportSize, len(evidence))
}
cfg := applyOptions(opts...)
// Parse first so we fail fast on malformed framing.
report, err := abi.ReportToProto(evidence)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
}
// Build verify.Options. We rely on the embedded ARK/ASK trusted
// roots inside go-sev-guest; the only thing we override is the
// HTTPS getter (so tests can replay KDS responses) and Now.
vopts := &verify.Options{}
if cfg.kdsGetter != nil {
g, ok := cfg.kdsGetter.(trust.HTTPSGetter)
if !ok {
return nil, fmt.Errorf("%w: kds getter does not implement trust.HTTPSGetter",
ErrInvalidEvidence)
}
vopts.Getter = g
} else {
vopts.Getter = trust.DefaultHTTPSGetter()
}
if !cfg.now.IsZero() {
vopts.Now = cfg.now
} else {
vopts.Now = time.Now()
}
// Chain + signature verify. This:
// - fetches VCEK from KDS by CHIP_ID + TCB
// - validates ARK→ASK→VCEK
// - validates VCEK extensions match the report's TCB
// - validates the report signature against VCEK
if err := verify.RawSnpReportContext(ctx, evidence, vopts); err != nil {
// go-sev-guest reports a single error string. Classify by
// substring so callers can distinguish chain vs signature
// failures via errors.Is. This is conservative — when in
// doubt, treat as ChainInvalid (refuse).
msg := err.Error()
switch {
case strings.Contains(msg, "could not interpret report bytes"):
return nil, fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
case strings.Contains(msg, "signature"):
return nil, fmt.Errorf("%w: %v", ErrSignatureInvalid, err)
default:
return nil, fmt.Errorf("%w: %v", ErrChainInvalid, err)
}
}
// Apply caller policy on the verified report fields.
if cfg.expectedReportData != nil {
if subtle.ConstantTimeCompare(cfg.expectedReportData, report.GetReportData()) != 1 {
return nil, fmt.Errorf("%w: report_data mismatch (got %s)",
ErrPolicy, hex.EncodeToString(report.GetReportData()))
}
}
if cfg.expectedMeasurement != nil {
if subtle.ConstantTimeCompare(cfg.expectedMeasurement, report.GetMeasurement()) != 1 {
return nil, fmt.Errorf("%w: measurement mismatch (got %s)",
ErrPolicy, hex.EncodeToString(report.GetMeasurement()))
}
}
out := &VerifiedReport{
Kind: KindSEVSNP,
Vendor: "amd.sev.snp",
Measurement: cloneBytes(report.GetMeasurement()),
ReportData: cloneBytes(report.GetReportData()),
HostData: cloneBytes(report.GetHostData()),
ChipID: cloneBytes(report.GetChipId()),
IssuedAt: vopts.Now.UTC(),
Extra: buildSEVExtra(report),
}
// CompositeHash binds verifier kind + the canonical fields the
// chain validated. Re-deriving on the consumer side reproduces the
// same hash iff the same evidence was verified by an honest
// verifier with the same policy.
out.CompositeHash = computeCompositeHash(KindSEVSNP, canonicalSEVBytes(report))
return out, nil
}
// buildSEVExtra collects sev-snp-specific fields that don't fit the
// common shape. Keys are stable wire identifiers; values are hex.
func buildSEVExtra(report *spb.Report) map[string]string {
extra := make(map[string]string, 6)
extra["sev_snp.policy"] = fmt.Sprintf("%#x", report.GetPolicy())
extra["sev_snp.vmpl"] = fmt.Sprintf("%d", report.GetVmpl())
extra["sev_snp.current_tcb"] = fmt.Sprintf("%#x", report.GetCurrentTcb())
extra["sev_snp.committed_tcb"] = fmt.Sprintf("%#x", report.GetCommittedTcb())
extra["sev_snp.platform_info"] = fmt.Sprintf("%#x", report.GetPlatformInfo())
if id := report.GetFamilyId(); len(id) > 0 {
extra["sev_snp.family_id"] = hex.EncodeToString(id)
}
if id := report.GetImageId(); len(id) > 0 {
extra["sev_snp.image_id"] = hex.EncodeToString(id)
}
return extra
}
// canonicalSEVBytes returns the bytes that participate in CompositeHash.
// We hash the verifier-extracted fields (not the raw report) so that any
// pre-verify field shuffling at lower layers cannot change the root
// without changing what the verifier saw. Field order is fixed and
// matches the public ABI ordering of the report.
func canonicalSEVBytes(report *spb.Report) []byte {
// Length: 8(policy) + 8(current_tcb) + 8(committed_tcb)
// + 16(family) + 16(image) + 32(host) + 48(meas)
// + 64(report_data) + 64(chip_id) = 264 bytes max.
buf := make([]byte, 0, 264)
appendU64 := func(v uint64) {
var b [8]byte
for i := 0; i < 8; i++ {
b[i] = byte(v >> (8 * i))
}
buf = append(buf, b[:]...)
}
appendU64(report.GetPolicy())
appendU64(report.GetCurrentTcb())
appendU64(report.GetCommittedTcb())
buf = append(buf, fixedSize(report.GetFamilyId(), 16)...)
buf = append(buf, fixedSize(report.GetImageId(), 16)...)
buf = append(buf, fixedSize(report.GetHostData(), 32)...)
buf = append(buf, fixedSize(report.GetMeasurement(), 48)...)
buf = append(buf, fixedSize(report.GetReportData(), 64)...)
buf = append(buf, fixedSize(report.GetChipId(), 64)...)
return buf
}
// fixedSize returns b padded or truncated to size n. Zero-pads when
// short. This guarantees canonicalSEVBytes is a fixed-shape blob.
func fixedSize(b []byte, n int) []byte {
out := make([]byte, n)
copy(out, b)
return out
}
func cloneBytes(b []byte) []byte {
if len(b) == 0 {
return nil
}
out := make([]byte, len(b))
copy(out, b)
return out
}
// Compile-time guard: SEVSNP satisfies Verifier.
var _ Verifier = SEVSNP{}
// Sentinel for unused import resilience: keep the errors import live in
// case future SEV-specific sentinels are added here. (No-op at runtime.)
var _ = errors.New
-46
View File
@@ -1,46 +0,0 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package attest
import (
"context"
"fmt"
)
// TDX is the Intel TDX verifier.
//
// Status: STUB — tracked at issue #222 stage 2.
//
// Production plan (intentionally not yet implemented):
//
// - Parse the TDREPORT / quote via github.com/google/go-tdx-guest/abi.
// - Fetch the PCK (Provisioning Certification Key) chain from the
// Intel PCS at api.trustedservices.intel.com using the report's
// FMSPC + PCEID.
// - Validate the chain to Intel's pinned root certificate and check
// the report signature against the PCK.
// - Cross-check the QE Identity and TDX Module Identity against the
// pinned values published by Intel.
// - Extract MRTD, RTMRs, REPORTDATA, and the platform identity.
// - Compose CompositeHash via the same domain-separated SHA-256 as
// the SEV path (see verifier.go::computeCompositeHash).
//
// Verify returns ErrNotImplemented so the call site refuses the release
// without crashing the process. Callers MUST treat this exactly like
// any other refusal: do not release, do not fall back. A silent no-op
// or success would let the release gate accidentally accept evidence
// the verifier has not actually checked; the explicit error-return path
// preserves the same hard-refusal guarantee while letting Dispatch be
// invoked from request-handling goroutines without taking the whole
// process down.
type TDX struct{}
// Verify implements Verifier. NOT YET IMPLEMENTED — do not enable TDX
// dispatch in production until #222 stage 2 lands.
func (TDX) Verify(ctx context.Context, evidence []byte, opts ...Option) (*VerifiedReport, error) {
return nil, fmt.Errorf("%w: TDX attestation envelope verification not configured for this build (tracked at #222 stage 2: go-tdx-guest + Intel PCS)",
ErrNotImplemented)
}
var _ Verifier = TDX{}
Binary file not shown.
Binary file not shown.
-221
View File
@@ -1,221 +0,0 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package attest
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"time"
)
// Kind tags the evidence framing the verifier should dispatch to.
type Kind string
const (
KindSEVSNP Kind = "sev_snp" // AMD SEV-SNP raw attestation report (ABI 0x4A0 bytes)
KindTDX Kind = "tdx" // Intel TDX TDREPORT / quote
KindNRAS Kind = "nras" // NVIDIA NRAS device-state JWT
)
// Errors returned by the verifier package. Callers should switch on
// errors.Is to distinguish refusal classes.
var (
// ErrUnsupportedKind is returned when evidence carries a Kind no
// installed verifier can handle.
ErrUnsupportedKind = errors.New("attest: unsupported evidence kind")
// ErrInvalidEvidence is returned when the bytes do not parse as the
// claimed Kind. The C-ABI parser usually catches this earlier; we
// recheck because the parser is C and we are about to make trust
// decisions on its output.
ErrInvalidEvidence = errors.New("attest: evidence failed to parse")
// ErrChainInvalid is returned when the cryptographic chain from the
// signing key (VCEK / PCK / NRAS signer) up to the vendor root
// fails to validate.
ErrChainInvalid = errors.New("attest: vendor chain validation failed")
// ErrSignatureInvalid is returned when the report signature does not
// match the verified leaf certificate.
ErrSignatureInvalid = errors.New("attest: report signature invalid")
// ErrPolicy is returned when the caller-supplied policy (expected
// measurement, expected report data, max age) rejects the evidence
// despite a cryptographically valid chain.
ErrPolicy = errors.New("attest: policy rejected verified evidence")
// ErrNotImplemented is returned by stub verifiers. Callers must
// treat this exactly like any other refusal: do not release, do not
// fall back. The error exists so #222-tracked verifiers fail loud
// instead of silently passing.
ErrNotImplemented = errors.New("attest: verifier not yet implemented")
)
// VerifiedReport is the canonical post-verification view of an evidence
// blob. Fields are kind-agnostic where possible; kind-specific fields
// land in the dedicated maps and are interpreted by the caller.
//
// CompositeHash is sha256(kind || verified-canonical-bytes). It is the
// integrity anchor a release gate, scheduler, or indexer logs as the
// attestation root. Two evidences with the same CompositeHash are
// guaranteed to have produced identical verifier output.
type VerifiedReport struct {
// Kind echoes the verifier that produced this report.
Kind Kind
// Vendor is the canonical issuer ("amd.sev.snp", "intel.tdx",
// "nvidia.nras.v1"). Stable across the wire.
Vendor string
// Measurement is the launch-measurement bytes the TEE attests to.
// SEV-SNP: 48 bytes of LD digest. TDX: MRTD. NRAS: a derived
// per-driver+VBIOS digest (defined per-verifier).
Measurement []byte
// ReportData is the 64-byte caller-supplied challenge field
// (REPORT_DATA on SEV-SNP, REPORTDATA on TDX). The caller proved
// freshness by binding their nonce here BEFORE attestation.
ReportData []byte
// HostData is the 32-byte host-supplied data field on SEV-SNP, or
// equivalent on TDX. nil for verifiers that don't define it.
HostData []byte
// ChipID identifies the silicon: VCEK CHIP_ID on SEV-SNP, the PCK
// platform descriptor on TDX, or per-GPU UUID on NRAS.
ChipID []byte
// IssuedAt is the verifier's wall-clock at successful verify. The
// evidence itself does not in general carry an issued-at; this is
// the time the chain check completed.
IssuedAt time.Time
// CompositeHash is sha256(string(Kind) || canonical(verified-bytes)).
// See package doc. Logged by callers as the attestation root.
CompositeHash [32]byte
// Extra carries kind-specific fields that don't fit the common
// shape. Keys are stable strings prefixed by the kind ("sev_snp.tcb",
// "nras.driver_version", ...). Treated opaquely by callers that
// don't recognise them.
Extra map[string]string
}
// Option mutates verification policy for a single Verify call. Options
// compose; later options win on conflict.
type Option func(*config)
type config struct {
// expectedReportData, if set, is compared byte-for-byte against the
// REPORT_DATA / REPORTDATA field of the evidence. Length must match
// the kind's natural size or the verifier returns ErrPolicy.
expectedReportData []byte
// expectedMeasurement, if set, must equal the verified Measurement.
expectedMeasurement []byte
// now is the wall clock to use for revocation / not-after checks.
// Tests pin this; production leaves it zero so verifiers use
// time.Now.
now time.Time
// kdsGetter is an HTTPSGetter override for SEV-SNP. Tests inject a
// pre-populated map so the test suite does not hit the AMD KDS.
// Production leaves this nil so the live AMD KDS is used.
kdsGetter any // verify/trust.HTTPSGetter; typed in sev.go to avoid leaking the dep here
}
// WithExpectedReportData binds the verifier to refuse evidence whose
// REPORT_DATA does not equal want. Use this to enforce that the
// challenge nonce the gate issued was actually included in the quote.
func WithExpectedReportData(want []byte) Option {
return func(c *config) {
buf := make([]byte, len(want))
copy(buf, want)
c.expectedReportData = buf
}
}
// WithExpectedMeasurement binds the verifier to refuse evidence whose
// MEASUREMENT does not equal want. Use to pin a known-good launch
// digest.
func WithExpectedMeasurement(want []byte) Option {
return func(c *config) {
buf := make([]byte, len(want))
copy(buf, want)
c.expectedMeasurement = buf
}
}
// WithNow pins the verification clock. Production callers should not
// use this; it exists for deterministic tests against committed
// fixtures whose certificates have a fixed validity window.
func WithNow(t time.Time) Option {
return func(c *config) { c.now = t }
}
// WithKDSGetter installs a custom HTTPSGetter for the SEV-SNP path.
// Used by tests to replay AMD KDS responses from disk. The argument
// must implement github.com/google/go-sev-guest/verify/trust.HTTPSGetter;
// the type is checked at the SEV verifier boundary.
func WithKDSGetter(g any) Option {
return func(c *config) { c.kdsGetter = g }
}
// Verifier verifies a single evidence blob.
//
// Verify means: bytes parse as the claimed Kind, the signing key chains
// to the pinned vendor root, the report signature is cryptographically
// valid, and any caller-supplied policy options pass. On success a
// VerifiedReport is returned with measurements extracted and the
// composite hash computed; on any failure the error is non-nil and the
// pointer is nil.
//
// Implementations MUST NOT return a partial VerifiedReport on error.
// Callers MUST refuse the originating request on any non-nil error.
type Verifier interface {
Verify(ctx context.Context, evidence []byte, opts ...Option) (*VerifiedReport, error)
}
// Dispatch routes evidence to the verifier registered for kind. Kind is
// supplied out-of-band (e.g. by the C-ABI parser's framing field) so
// the verifier never has to guess from byte heuristics.
func Dispatch(ctx context.Context, kind Kind, evidence []byte, opts ...Option) (*VerifiedReport, error) {
switch kind {
case KindSEVSNP:
return SEVSNP{}.Verify(ctx, evidence, opts...)
case KindTDX:
return TDX{}.Verify(ctx, evidence, opts...)
case KindNRAS:
return NRAS{}.Verify(ctx, evidence, opts...)
default:
return nil, fmt.Errorf("%w: %q", ErrUnsupportedKind, string(kind))
}
}
// applyOptions builds an immutable config from defaults + opts.
func applyOptions(opts ...Option) config {
c := config{}
for _, o := range opts {
o(&c)
}
return c
}
// computeCompositeHash returns sha256(kind || canonical-bytes). The
// canonical bytes are the verifier-extracted, deterministic
// representation — never the raw evidence. This guarantees that a
// caller logging CompositeHash logs a value that was attested-to AND
// chain-validated, not whatever the wire happened to carry.
func computeCompositeHash(kind Kind, canonical []byte) [32]byte {
h := sha256.New()
h.Write([]byte(kind))
h.Write([]byte{0x00}) // domain separator: zero byte between fields
h.Write(canonical)
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
-290
View File
@@ -1,290 +0,0 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package attest
import (
"bytes"
"context"
_ "embed"
"errors"
"testing"
"time"
test "github.com/google/go-sev-guest/testing"
"github.com/google/go-sev-guest/verify/trust"
)
// sevSnpAttestationMilan is a real AMD Milan SEV-SNP attestation report
// (0x4A0 bytes) captured via the SNP_GUEST_REQUEST ioctl. Sourced from
// the upstream go-sev-guest test corpus and committed verbatim so this
// package's tests do not require live hardware or network access.
//
//go:embed testdata/sev_snp_attestation_milan.bin
var sevSnpAttestationMilan []byte
// sevSnpVcekMilan is the VCEK leaf certificate that signed the report
// above, as issued by the AMD KDS for the matching CHIP_ID + TCB.
//
//go:embed testdata/sev_snp_vcek_milan.cer
var sevSnpVcekMilan []byte
// kdsResponses is the pre-fetched KDS replay map. The keys are the exact
// URLs go-sev-guest hits when verifying this report; the values are the
// committed bytes. Tests inject this via WithKDSGetter so verification
// runs offline.
//
// The product CA bundle URL is the one the lib resolves for Milan
// (encoded into the Milan product chain), and the VCEK URL is computed
// from the report's CHIP_ID + reported TCB.
func newKDSReplay() trust.HTTPSGetter {
return test.SimpleGetter(map[string][]byte{
"https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": trust.AskArkMilanVcekBytes,
"https://kdsintf.amd.com/vcek/v1/Milan/3ac3fe21e13fb0990eb28a802e3fb6a29483a6b0753590c951bdd3b8e53786184ca39e359669a2b76a1936776b564ea464cdce40c05f63c9b610c5068b006b5d?blSPL=2&teeSPL=0&snpSPL=5&ucodeSPL=68": sevSnpVcekMilan,
})
}
// fixedNow pins the verification clock inside the validity window of
// the committed VCEK so tests are reproducible regardless of when CI
// runs. The committed VCEK is valid roughly 2023–2030; we pick a date
// safely inside that window.
func fixedNow() time.Time {
return time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)
}
// -----------------------------------------------------------------------------
// SEV-SNP — production path against committed real-vendor fixtures
// -----------------------------------------------------------------------------
func TestSEVSNP_Verify_HappyPath(t *testing.T) {
// Reset the package-level cert cache so prior tests don't leak.
trust.ClearProductCertCache()
rep, err := SEVSNP{}.Verify(
context.Background(),
sevSnpAttestationMilan,
WithKDSGetter(newKDSReplay()),
WithNow(fixedNow()),
)
if err != nil {
t.Fatalf("verify: %v", err)
}
if rep == nil {
t.Fatal("verify returned nil report")
}
if rep.Kind != KindSEVSNP {
t.Errorf("kind = %q, want %q", rep.Kind, KindSEVSNP)
}
if rep.Vendor != "amd.sev.snp" {
t.Errorf("vendor = %q, want amd.sev.snp", rep.Vendor)
}
if len(rep.Measurement) != 48 {
t.Errorf("measurement length = %d, want 48", len(rep.Measurement))
}
if len(rep.ReportData) != 64 {
t.Errorf("report_data length = %d, want 64", len(rep.ReportData))
}
if len(rep.ChipID) != 64 {
t.Errorf("chip_id length = %d, want 64", len(rep.ChipID))
}
if rep.IssuedAt.IsZero() {
t.Error("issued_at not set")
}
if rep.CompositeHash == ([32]byte{}) {
t.Error("composite hash is zero")
}
// Extra fields must include the canonical SEV identifiers.
for _, key := range []string{"sev_snp.policy", "sev_snp.vmpl", "sev_snp.current_tcb"} {
if _, ok := rep.Extra[key]; !ok {
t.Errorf("missing extra key %q", key)
}
}
}
func TestSEVSNP_Verify_DispatchRoutesToSEV(t *testing.T) {
trust.ClearProductCertCache()
rep, err := Dispatch(
context.Background(),
KindSEVSNP,
sevSnpAttestationMilan,
WithKDSGetter(newKDSReplay()),
WithNow(fixedNow()),
)
if err != nil {
t.Fatalf("dispatch: %v", err)
}
if rep.Kind != KindSEVSNP {
t.Errorf("kind = %q, want %q", rep.Kind, KindSEVSNP)
}
}
func TestSEVSNP_Verify_RejectsWrongLength(t *testing.T) {
_, err := SEVSNP{}.Verify(
context.Background(),
[]byte("too short"),
WithKDSGetter(newKDSReplay()),
WithNow(fixedNow()),
)
if !errors.Is(err, ErrInvalidEvidence) {
t.Fatalf("err = %v, want ErrInvalidEvidence", err)
}
}
func TestSEVSNP_Verify_RejectsTamperedSignature(t *testing.T) {
trust.ClearProductCertCache()
tampered := append([]byte(nil), sevSnpAttestationMilan...)
// Flip a bit inside the ECDSA signature R-component (0x2A0 is the
// start of the signature region; offset +0x10 lands inside R, not
// in the MBZ tail at 0x330+). Any flip here makes the signature
// check fail without tripping the parser's MBZ guard.
tampered[0x2A0+0x10] ^= 0x01
_, err := SEVSNP{}.Verify(
context.Background(),
tampered,
WithKDSGetter(newKDSReplay()),
WithNow(fixedNow()),
)
if err == nil {
t.Fatal("expected error on tampered signature, got nil")
}
// Tampering inside the signature region must be classified as a
// signature failure or a chain failure (either is a refusal). We
// also accept ErrInvalidEvidence in case go-sev-guest's parser
// catches the corruption first — what matters is the verifier
// refuses and never returns a *VerifiedReport.
if !errors.Is(err, ErrSignatureInvalid) &&
!errors.Is(err, ErrChainInvalid) &&
!errors.Is(err, ErrInvalidEvidence) {
t.Errorf("err = %v, want one of ErrSignatureInvalid|ErrChainInvalid|ErrInvalidEvidence", err)
}
}
func TestSEVSNP_Verify_RejectsWrongReportData(t *testing.T) {
trust.ClearProductCertCache()
wrong := bytes.Repeat([]byte{0xFF}, 64)
_, err := SEVSNP{}.Verify(
context.Background(),
sevSnpAttestationMilan,
WithKDSGetter(newKDSReplay()),
WithNow(fixedNow()),
WithExpectedReportData(wrong),
)
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy", err)
}
}
func TestSEVSNP_Verify_RejectsWrongMeasurement(t *testing.T) {
trust.ClearProductCertCache()
wrong := bytes.Repeat([]byte{0xAB}, 48)
_, err := SEVSNP{}.Verify(
context.Background(),
sevSnpAttestationMilan,
WithKDSGetter(newKDSReplay()),
WithNow(fixedNow()),
WithExpectedMeasurement(wrong),
)
if !errors.Is(err, ErrPolicy) {
t.Fatalf("err = %v, want ErrPolicy", err)
}
}
func TestSEVSNP_Verify_DeterministicCompositeHash(t *testing.T) {
trust.ClearProductCertCache()
rep1, err := SEVSNP{}.Verify(
context.Background(),
sevSnpAttestationMilan,
WithKDSGetter(newKDSReplay()),
WithNow(fixedNow()),
)
if err != nil {
t.Fatalf("verify 1: %v", err)
}
trust.ClearProductCertCache()
rep2, err := SEVSNP{}.Verify(
context.Background(),
sevSnpAttestationMilan,
WithKDSGetter(newKDSReplay()),
WithNow(fixedNow()),
)
if err != nil {
t.Fatalf("verify 2: %v", err)
}
if rep1.CompositeHash != rep2.CompositeHash {
t.Errorf("composite hash not deterministic: %x vs %x",
rep1.CompositeHash, rep2.CompositeHash)
}
}
// -----------------------------------------------------------------------------
// Dispatch errors
// -----------------------------------------------------------------------------
func TestDispatch_RejectsUnknownKind(t *testing.T) {
_, err := Dispatch(context.Background(), Kind("madeup"), []byte{0x00})
if !errors.Is(err, ErrUnsupportedKind) {
t.Fatalf("err = %v, want ErrUnsupportedKind", err)
}
}
// TestStubsReturnNotImplemented confirms the TDX/NRAS stubs FAIL LOUD —
// returning ErrNotImplemented (a hard refusal callers must not paper
// over) rather than silently returning a nil error. This guards against
// an accidental future change that silently accepts evidence the
// verifier hasn't actually checked, while keeping the failure
// recoverable so request-handling goroutines do not crash the whole
// process. When stages 2 + 3 of #222 land, replace this test with real
// verify cases.
func TestStubsReturnNotImplemented(t *testing.T) {
cases := []struct {
name string
v Verifier
}{
{"tdx", TDX{}},
{"nras", NRAS{}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("%s.Verify panicked (must error-return instead): %v", c.name, r)
}
}()
rep, err := c.v.Verify(context.Background(), []byte{0x00})
if rep != nil {
t.Fatalf("%s.Verify returned non-nil report: %+v", c.name, rep)
}
if !errors.Is(err, ErrNotImplemented) {
t.Fatalf("%s.Verify returned %v, want ErrNotImplemented", c.name, err)
}
})
}
}
// TestDispatch_StubKindsReturnNotImplemented covers the Dispatch path
// for the stub kinds: the entry point must surface ErrNotImplemented
// instead of crashing.
func TestDispatch_StubKindsReturnNotImplemented(t *testing.T) {
for _, kind := range []Kind{KindTDX, KindNRAS} {
t.Run(string(kind), func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("Dispatch(%s) panicked: %v", kind, r)
}
}()
rep, err := Dispatch(context.Background(), kind, []byte{0x00})
if rep != nil {
t.Fatalf("Dispatch(%s) returned non-nil report: %+v", kind, rep)
}
if !errors.Is(err, ErrNotImplemented) {
t.Fatalf("Dispatch(%s) returned %v, want ErrNotImplemented", kind, err)
}
})
}
}
+10 -11
View File
@@ -23,7 +23,6 @@ require (
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/go-sev-guest v0.14.1
github.com/google/uuid v1.6.0
github.com/gtank/ristretto255 v0.2.0
github.com/hanzoai/base v0.39.1
@@ -31,12 +30,13 @@ require (
github.com/hanzoai/orm v0.5.2
github.com/hanzos3/go-sdk v1.0.2
github.com/hashicorp/consul/api v1.33.7
github.com/luxfi/cc v0.0.0-00010101000000-000000000000
github.com/luxfi/crypto v1.19.17
github.com/luxfi/database v1.18.3
github.com/luxfi/fhe v1.8.2
github.com/luxfi/hsm v1.1.3
github.com/luxfi/lattice/v7 v7.1.4
github.com/luxfi/log v1.4.1
github.com/luxfi/log v1.4.3
github.com/luxfi/metric v1.5.7
github.com/luxfi/threshold v1.9.9
github.com/luxfi/zap v0.8.1
@@ -85,7 +85,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -113,6 +112,7 @@ require (
github.com/golang/mock v1.6.0 // indirect
github.com/google/flatbuffers v25.12.19+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-sev-guest v0.14.1 // indirect
github.com/google/logger v1.1.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
@@ -167,7 +167,7 @@ require (
github.com/luxfi/utils v1.1.4 // indirect
github.com/luxfi/zapdb v1.8.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
@@ -177,21 +177,15 @@ require (
github.com/minio/minio-go/v7 v7.0.100 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/montanaflynn/stats v0.9.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/nexus-rpc/sdk-go v0.6.0 // indirect
github.com/onsi/ginkgo/v2 v2.28.1 // indirect
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/rs/xid v1.6.0 // indirect
@@ -218,7 +212,6 @@ require (
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/image v0.38.0 // indirect
@@ -250,6 +243,12 @@ replace github.com/agl/ed25519 => github.com/luxfi/edwards25519 v0.1.0
replace github.com/luxfi/log v1.3.0 => github.com/luxfi/log v1.4.1
// cc/attest + pkg/attestation/nvidia were extracted into the orthogonal
// leaf module github.com/luxfi/cc (one attestation verifier, depended on by
// mpc/tee/ai, coupled to none). Local replace until the leaf is published;
// drop this once github.com/luxfi/cc is tagged.
replace github.com/luxfi/cc => ../cc
// luxfi/genesis v1.5.21 / v1.5.22 are missing from proxy.golang.org; no
// luxfi/mpc code path imports them, but transitive resolution can pick them
// up. Excluded until genesis retags.
+6 -74
View File
@@ -416,16 +416,12 @@ filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
github.com/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM=
github.com/ALTree/bigfloat v0.2.0/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
@@ -471,52 +467,32 @@ github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHS
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
github.com/aws/aws-sdk-go-v2/config v1.32.13 h1:5KgbxMaS2coSWRrx9TX/QtWbqzgQkOdEa3sZPhBhCSg=
github.com/aws/aws-sdk-go-v2/config v1.32.13/go.mod h1:8zz7wedqtCbw5e9Mi2doEwDyEgHcEE9YOJp6a8jdSMY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.13/go.mod h1:yoTXOQKea18nrM69wGF9jBdG4WocSZA1h38A+t/MAsk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.4 h1:PgD1y0ZagPokGIZPmejCBUySBzOFDN+leZxCOfb1OEQ=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.4/go.mod h1:FfXDb5nXrsoGgxsBFxwxr3vdHXheC2tV+6lmuLghhjQ=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.5 h1:z2ayoK3pOvf8ODj/vPR0FgAS5ONruBq0F94SRoW/BIU=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.5/go.mod h1:mpZB5HAl4ZIISod9qCi12xZ170TbHX9CCJV5y7nb7QU=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 h1:GcLE9ba5ehAQma6wlopUesYg/hbcOhFNWTjELkiWkh4=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 h1:mP49nTpfKtpXLt5SLn8Uv8z6W+03jYVoOSAl/c02nog=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
@@ -524,7 +500,6 @@ github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqx
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
@@ -903,10 +878,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/luxfi/accel v1.1.2 h1:V3vZOWTcOmR5mm7I27jRvXmpEPyb3oRf7lLnL6R+pFI=
github.com/luxfi/accel v1.1.2/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
github.com/luxfi/accel v1.1.8 h1:dFD1MSrVV7T4wrLcQbj+7vjfNSPxlRId3mJcTxMKoBM=
github.com/luxfi/accel v1.1.8/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
github.com/luxfi/accel v1.1.9 h1:Tsk6gXj2uKE19501bD0ajRYdeCHIlTGb6jYyLc+F8hc=
github.com/luxfi/accel v1.1.9/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
github.com/luxfi/age v1.4.0 h1:T8OKO/r3xg9xiZx2I/NzzdITTs//1lOKgi7nPISyRio=
@@ -925,12 +896,8 @@ github.com/luxfi/constants v1.4.7 h1:e/Qs+DQP3pugle3Zncq6fZCxKgqqtbyD/z7Gm4ZjsYg
github.com/luxfi/constants v1.4.7/go.mod h1:hOszZ2NDQ8gMZKncfcZ67PXkb5OIbnwAzXC3oFbQwW0=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
github.com/luxfi/corona v0.4.1 h1:DGMdZMrENl6vmJf9CgiQMD8c5ORSTMymL0z9KgcZstk=
github.com/luxfi/corona v0.4.1/go.mod h1:4aD7+ZqnlZ2aVuU/DBQ5aspIagv5ux45LW2sJ4+siY8=
github.com/luxfi/corona v0.7.6 h1:CJP6smygD55dL0HHkKkWryL9H24a+wXvs+L+WchK7Nc=
github.com/luxfi/corona v0.7.6/go.mod h1:4aD7+ZqnlZ2aVuU/DBQ5aspIagv5ux45LW2sJ4+siY8=
github.com/luxfi/crypto v1.19.10 h1:2tdnBafuysfTkMJyLdz6+W181YmJJO1vndOatKmr5JQ=
github.com/luxfi/crypto v1.19.10/go.mod h1:88cJ59mF/e199BYZK/p8UHmo4bSBPSVNOiHeY6AhMBw=
github.com/luxfi/crypto v1.19.17 h1:l2LLu7UFyICtJVfraLDLRi+lFGiDXKHSL18M9/m1gsQ=
github.com/luxfi/crypto v1.19.17/go.mod h1:INjdZtke85k8hX/QAmTMAY8bbZ4gzGZQLqURg3xf6Gk=
github.com/luxfi/database v1.18.3 h1:gg+xwhKUxXa7fDoOD8IS91E71QqoEtcDCl2nfS61Jgg=
@@ -939,30 +906,24 @@ github.com/luxfi/edwards25519 v0.1.0 h1:YPoT831TZMslvNyy/KuTtpHi4BGZWgpwak06RCaY
github.com/luxfi/edwards25519 v0.1.0/go.mod h1:MBoV+bPEz1tSMADPMSUaYdZ+agY3syG8B6z7RmJBH0A=
github.com/luxfi/fhe v1.8.2 h1:QllnObNFbi6D4mvFI6uQkepW8HgLtdy4RMR1TKYAInA=
github.com/luxfi/fhe v1.8.2/go.mod h1:16yxwhcnCez/rNcd/C9JjH9IjbEz73X+0tvlsONyLeA=
github.com/luxfi/geth v1.16.79 h1:MtP8ZUuSVZDjmZDa1kTJCl0PpG8+wGbQft652GO6a3A=
github.com/luxfi/geth v1.16.79/go.mod h1:6xNi4sHoh0v1kBf0TOjmFhBg5+4umbSqrwiFZsDanZ0=
github.com/luxfi/geth v1.16.98 h1:w187TtKuGStf3tm2bshuHVKBv2Frjx0lT54kQVXyNHA=
github.com/luxfi/geth v1.16.98/go.mod h1:6kEzSExdk9CPQDPXALt6P3HfQqBq7KF1Jrrv9gBpxbU=
github.com/luxfi/hsm v1.1.3 h1:ke36qmz5zqapBrnN3ij7gU9fFPaPBrnzcdrKX4lV/rk=
github.com/luxfi/hsm v1.1.3/go.mod h1:2DwOpBG1yzo9zHbsw2t0p/CbGjbyHYXjPx7uxEIp8wo=
github.com/luxfi/ids v1.2.9 h1:+yjdhXW99drnd2Zlp1u/p8k3G23W3/1btJQ4ogHawUI=
github.com/luxfi/ids v1.2.9/go.mod h1:khJOEdOPxd22yn0jcVrnbX1ADa0GHn5Y74gvCzN5BYc=
github.com/luxfi/lattice/v7 v7.1.0 h1:mr3HvN6olNTS2LT/xAW/JBhTqfvpsGmsopDMeR7BSJs=
github.com/luxfi/lattice/v7 v7.1.0/go.mod h1:IaaUN+3ysnBG4BA8ILRYG0j80+qtYDP4C5lkaDb2pDE=
github.com/luxfi/lattice/v7 v7.1.4 h1:hQR02M6cHTAV5+joOPi9gb9Gm+z/hKJnhJF4IlciIJs=
github.com/luxfi/lattice/v7 v7.1.4/go.mod h1:DmIQFi3mJiehVsR235l1NKYEU0JhU649OX5p7gMEW2c=
github.com/luxfi/lens v0.1.4 h1:goGjGDXx2BNdjzXDunL5QT8elK2ZyCcc0z8TAbtWYrg=
github.com/luxfi/lens v0.1.4/go.mod h1:mL+G8IK+9L41d78/2FYRgfhEzAjcr5+VEXB8SGuHbus=
github.com/luxfi/log v1.4.1 h1:rIfFRodb9jrD/w7KayaUk0Oc+37PaQQdKEEMJCjR8gw=
github.com/luxfi/log v1.4.1/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72jk=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/luxfi/magnetar v1.2.0 h1:bsxHmBnJiswc/A6ElQ0pWz5g6ogqewIEKKqR26VgizA=
github.com/luxfi/magnetar v1.2.0/go.mod h1:7J9YP9jByWbwCjssMFJNUkTU8tcPlSUoVSSiYShtvFs=
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
github.com/luxfi/mdns v0.1.0 h1:VB3mQcETc9j5SY1S6lAgFtuGr/rjWuDgPYnxS+OKWMQ=
github.com/luxfi/mdns v0.1.0/go.mod h1:/3dheKVjUk2yiS/ocH1IDzeLXOIe+kpVsErIGDOZdiQ=
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
github.com/luxfi/metric v1.5.7 h1:LoSPEUpak2SLcynF+LT2cXjl9ECp4nY+Lia9zudmDv4=
@@ -975,14 +936,10 @@ github.com/luxfi/pulsar v1.1.1 h1:jo1jEgUsGiVxpT17Eg7Gw4Ax+07pbKFzq4NhHMLmDpI=
github.com/luxfi/pulsar v1.1.1/go.mod h1:U7tPleeAHJ9dZ61ymtstzLKKoZjxM2zFeGZ+RSjHyRw=
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
github.com/luxfi/threshold v1.8.2 h1:GxkOm4R8BWTKyVn0teo8hLii5zuif/YrR06VcIfenGU=
github.com/luxfi/threshold v1.8.2/go.mod h1:RRT64ObcAyD7JweqVnGDVzmu6KeKGadFV3WiJaxvX+Y=
github.com/luxfi/threshold v1.9.9 h1:zsEuMASTbyiLi7DkbjXBw3hsKIcqvpQt3Xu/MpZA5RQ=
github.com/luxfi/threshold v1.9.9/go.mod h1:8zO1a2f3UMMsM1TkOVUoUbCR9h1sPhWH/ibblf13+h4=
github.com/luxfi/utils v1.1.4 h1:8OY0jXCkpp6OotN1Y++6DATJkvtEwzq2k0p56imJlAk=
github.com/luxfi/utils v1.1.4/go.mod h1:c3yz1RjzrB+cs5GZm+q1T3/2cCKElO9vxm9yRRtgSEM=
github.com/luxfi/zap v0.2.0 h1:RzvOkp3EoN5UCkpnqfObHLM1sEHy7YcxXKuermhE/VA=
github.com/luxfi/zap v0.2.0/go.mod h1:2hydPSa/XMCMtfW6/DC9M5Bt03N5h75QwCV5Vypsqr4=
github.com/luxfi/zap v0.8.1 h1:IzYWf4T35QjsUJhoNbuhew8MDDRYmlZUVx4gKBNLjMA=
github.com/luxfi/zap v0.8.1/go.mod h1:gUoQ/V9nTLLIb4/n0bY/L2zQDYK6pDBG121rK4t8bpg=
github.com/luxfi/zapdb v1.8.0 h1:E9hXMW1MTuS6kmDVs07j62gIq5qI5gZOdyzVfiBc1wg=
@@ -999,8 +956,8 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@@ -1030,8 +987,6 @@ github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq2
github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/nats.go v1.50.0 h1:5zAeQrTvyrKrWLJ0fu02W3br8ym57qf7csDzgLOpcds=
@@ -1074,27 +1029,21 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
@@ -1222,8 +1171,6 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1236,8 +1183,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1284,8 +1229,6 @@ golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1343,8 +1286,6 @@ golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfS
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1474,10 +1415,7 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1485,8 +1423,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1503,8 +1439,6 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1573,8 +1507,6 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+7 -5
View File
@@ -46,7 +46,7 @@ import (
"github.com/fxamacker/cbor/v2"
lattice "github.com/luxfi/lattice/v7/types"
"github.com/luxfi/mpc/cc/attest"
"github.com/luxfi/cc/attest"
)
// EnvelopeVersion is the schema version of the CompositeAttestation
@@ -308,9 +308,11 @@ func (c *CompositeAttestation) validate() error {
//
// Behaviour by issuer:
//
// - "amd.sev.snp" → cc/attest.SEVSNP{} (PRODUCTION)
// - "intel.tdx" → cc/attest.TDX{} (panics; tracked at #222 stage 2)
// - "nvidia.nras.v1" → cc/attest.NRAS{} (panics; tracked at #222 stage 3)
// - "amd.sev.snp" → cc/attest.SEVSNP{} (PRODUCTION)
// - "intel.tdx" → cc/attest.TDX{} (ErrNotImplemented; #222 stage 2)
// - "nvidia.nras.v1" → cc/attest.NVTrust{} (PRODUCTION local-RIM verify;
// supply the signed RIM + trust roots via attest.WithNVTrustRIM /
// attest.WithNVTrustTrustRoots in the AttestOptions)
// - other issuers → out[i] is nil (no verifier registered yet)
//
// The returned slice has the same length and order as c.Evidence;
@@ -355,7 +357,7 @@ func evidenceKindForIssuer(issuer string) (attest.Kind, bool) {
case "intel.tdx":
return attest.KindTDX, true
case "nvidia.nras.v1":
return attest.KindNRAS, true
return attest.KindNVTrust, true
default:
return "", false
}
-206
View File
@@ -1,206 +0,0 @@
// Package nvidia provides NVIDIA Confidential-Compute attestation
// primitives: the GPU evidence-report parser, NVIDIA Remote
// Attestation Service (NRAS) HTTP client, Reference Integrity
// Manifest (RIM) validator, and the verified-bundle root that
// AND-combines a CPU TEE quote (SEV-SNP / TDX) with the GPU result.
//
// Wire format and field names mirror the public nvTrust / NVIDIA
// Confidential Compute documentation (Apache-2 licensed). No NVIDIA
// proprietary code is vendored — every parser is a clean-room
// implementation of the publicly documented schema.
//
// Relationship to pkg/attestation.CompositeAttestation (sibling v0.60):
// the canonical wire envelope lives in pkg/attestation. This package
// is the NVIDIA-specific verifier that turns one of those envelopes'
// EvidenceGPUNRASReport blobs into a verified GPUEvidence record.
// VerifiedBundle is the post-verification CPU+GPU bind that is folded
// into the scheduler's Worker fields — it is NOT the canonical wire
// envelope.
//
// The root entry point is VerifiedBundle.Verify(): it returns
// nil iff every component verifies AND every reference value matches.
// Callers feed the resulting VerifiedResult into the scheduler's
// Worker fields (TrustMode, GPUAttested, CPUAttested,
// KernelManifestRoot).
package nvidia
import (
"crypto/sha256"
"errors"
"fmt"
"time"
)
// TEEKind identifies the CPU confidential-compute platform.
type TEEKind uint8
const (
// TEENone — no CPU TEE; only acceptable for development.
TEENone TEEKind = iota
// TEESEVSNP — AMD SEV-SNP.
TEESEVSNP
// TEETDX — Intel TDX.
TEETDX
)
// String returns the canonical name for the TEE kind.
func (t TEEKind) String() string {
switch t {
case TEENone:
return "none"
case TEESEVSNP:
return "sev_snp"
case TEETDX:
return "tdx"
}
return "invalid"
}
// CPUQuote is the verified output of a CPU TEE attestation. The bytes
// of the raw quote (SEV-SNP attestation report or TDX TD report) are
// not retained here; this struct holds only the post-verification
// projection that downstream gates need.
type CPUQuote struct {
Kind TEEKind // SEV-SNP or TDX
Verified bool // verifier confirmed the signature chain
MeasurementHash [32]byte // launch measurement (MRTD or LAUNCH_MEASUREMENT)
ReportData [32]byte // freshness binding (e.g. nonce hash)
VerifiedAt time.Time // when the verifier signed off
VendorCertChain [][]byte // AMD or Intel vendor cert chain (DER)
}
// GPUEvidence is the verified output of an NVIDIA GPU attestation.
// Populated by GPUReport.Parse + NRASClient.Verify + RIM.Match.
type GPUEvidence struct {
UUID string // GPU UUID, e.g. "GPU-1234..."
DriverVersion string // e.g. "535.104.05"
VBIOSVersion string // e.g. "96.00.74.00.01"
Architecture string // "Hopper" | "Blackwell"
Verified bool // NRAS returned a valid token
NRASTokenHash [32]byte // sha256 of the NRAS token bytes
MeasurementRoot [32]byte // root over the measurement list
RIMMatched bool // every measurement matched the RIM
Nonce [32]byte // freshness nonce echoed in report
VerifiedAt time.Time // when NRAS signed the token
NVSwitchVerified bool // multi-GPU NVSwitch attested (optional)
}
// VerifiedBundle binds a CPU TEE quote to a GPU evidence record
// for a single worker, plus the kernel manifest root that the
// scheduler matches against the workload's approved set.
//
// The composite is what the scheduler ultimately consumes: a single
// struct that carries every fact the workload-policy gate needs.
type VerifiedBundle struct {
WorkerID string
CPU CPUQuote
GPU GPUEvidence
KernelManifestRoot [32]byte // root of the approved kernel manifest
BuiltAt time.Time
}
// VerifiedResult is the projection a Worker is built from. Callers
// pass these fields into scheduler.Worker.
type VerifiedResult struct {
WorkerID string
CPUAttested bool
GPUAttested bool
NVSwitchAttested bool
KernelManifestRoot [32]byte
}
// Errors returned by Verify, in the order they are checked.
var (
ErrBundleMissingWorker = errors.New("nvidia: composite missing worker ID")
ErrBundleNoCPUQuote = errors.New("nvidia: composite missing CPU quote")
ErrBundleBadCPU = errors.New("nvidia: CPU quote not verified")
ErrBundleBadGPU = errors.New("nvidia: GPU evidence not verified")
ErrBundleRIMMismatch = errors.New("nvidia: RIM mismatch on GPU measurements")
ErrBundleBindingMissing = errors.New("nvidia: CPU report_data does not bind GPU nonce")
ErrBundleStale = errors.New("nvidia: composite older than freshness window")
)
// Verify checks every component of the composite. The freshness
// argument bounds how old the CPU/GPU verifications may be; pass 0
// to disable the freshness check (test-only).
//
// A successful Verify returns the projection ready for scheduler
// consumption.
func (c *VerifiedBundle) Verify(now time.Time, freshness time.Duration) (VerifiedResult, error) {
var zero VerifiedResult
if c == nil {
return zero, errors.New("nvidia: nil composite")
}
if c.WorkerID == "" {
return zero, ErrBundleMissingWorker
}
if c.CPU.Kind == TEENone {
return zero, ErrBundleNoCPUQuote
}
if !c.CPU.Verified {
return zero, ErrBundleBadCPU
}
if !c.GPU.Verified {
return zero, ErrBundleBadGPU
}
if !c.GPU.RIMMatched {
return zero, ErrBundleRIMMismatch
}
// Bind GPU nonce to CPU report_data. Standard pattern: report_data
// is sha256(workerID || gpuUUID || gpuNonce). If a different shape
// is required, callers can override binding by setting the CPU's
// ReportData themselves and skipping the helper.
expected := bindCPUReportData(c.WorkerID, c.GPU.UUID, c.GPU.Nonce)
if c.CPU.ReportData != expected {
return zero, ErrBundleBindingMissing
}
if freshness > 0 {
if c.CPU.VerifiedAt.IsZero() || now.Sub(c.CPU.VerifiedAt) > freshness {
return zero, fmt.Errorf("%w: cpu age=%v", ErrBundleStale, now.Sub(c.CPU.VerifiedAt))
}
if c.GPU.VerifiedAt.IsZero() || now.Sub(c.GPU.VerifiedAt) > freshness {
return zero, fmt.Errorf("%w: gpu age=%v", ErrBundleStale, now.Sub(c.GPU.VerifiedAt))
}
}
return VerifiedResult{
WorkerID: c.WorkerID,
CPUAttested: true,
GPUAttested: true,
NVSwitchAttested: c.GPU.NVSwitchVerified,
KernelManifestRoot: c.KernelManifestRoot,
}, nil
}
// bindCPUReportData computes the canonical binding hash that ties the
// CPU TEE report_data field to the GPU evidence nonce. Callers MUST
// use this when constructing the CPU quote so the composite verifies.
//
// Layout: sha256( "luxfi/mpc/composite\x00" || workerID || "\x00" ||
//
// gpuUUID || "\x00" || gpuNonce ).
//
// The leading domain string + null separators prevent cross-domain
// reuse of the same hash (a SEV-SNP report bound this way cannot be
// repurposed for any other binding).
func bindCPUReportData(workerID, gpuUUID string, gpuNonce [32]byte) [32]byte {
h := sha256.New()
_, _ = h.Write([]byte("luxfi/mpc/composite\x00"))
_, _ = h.Write([]byte(workerID))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(gpuUUID))
_, _ = h.Write([]byte{0})
_, _ = h.Write(gpuNonce[:])
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
// BindCPUReportData is the exported helper for callers building a CPU
// quote. The returned 32-byte digest must be placed in REPORT_DATA
// (SEV-SNP) or REPORTDATA (TDX) before the quote is fetched.
func BindCPUReportData(workerID, gpuUUID string, gpuNonce [32]byte) [32]byte {
return bindCPUReportData(workerID, gpuUUID, gpuNonce)
}
-149
View File
@@ -1,149 +0,0 @@
package nvidia
import (
"errors"
"testing"
"time"
)
// helper — fully verified composite.
func okBundle() VerifiedBundle {
gpuNonce := [32]byte{1, 2, 3, 4}
workerID := "worker-7"
gpuUUID := "GPU-DEAD-BEEF"
cpuRD := BindCPUReportData(workerID, gpuUUID, gpuNonce)
return VerifiedBundle{
WorkerID: workerID,
CPU: CPUQuote{
Kind: TEESEVSNP,
Verified: true,
MeasurementHash: [32]byte{0x11},
ReportData: cpuRD,
VerifiedAt: time.Now().Add(-1 * time.Minute),
},
GPU: GPUEvidence{
UUID: gpuUUID,
DriverVersion: "535.104.05",
VBIOSVersion: "96.00.74.00.01",
Architecture: "Hopper",
Verified: true,
RIMMatched: true,
Nonce: gpuNonce,
VerifiedAt: time.Now().Add(-2 * time.Minute),
MeasurementRoot: [32]byte{0x22},
},
KernelManifestRoot: [32]byte{0x33, 0x44},
BuiltAt: time.Now(),
}
}
func TestVerifiedBundle_HappyPath(t *testing.T) {
c := okBundle()
res, err := c.Verify(time.Now(), time.Hour)
if err != nil {
t.Fatalf("verify: %v", err)
}
if !res.CPUAttested || !res.GPUAttested {
t.Fatalf("expected attested, got %+v", res)
}
if res.KernelManifestRoot != c.KernelManifestRoot {
t.Fatalf("kernel root drift")
}
}
func TestVerifiedBundle_NilReceiver(t *testing.T) {
var c *VerifiedBundle
if _, err := c.Verify(time.Now(), 0); err == nil {
t.Fatal("expected error")
}
}
func TestVerifiedBundle_RejectsTEENone(t *testing.T) {
c := okBundle()
c.CPU.Kind = TEENone
if _, err := c.Verify(time.Now(), 0); !errors.Is(err, ErrBundleNoCPUQuote) {
t.Fatalf("got %v", err)
}
}
func TestVerifiedBundle_RejectsUnverifiedCPU(t *testing.T) {
c := okBundle()
c.CPU.Verified = false
if _, err := c.Verify(time.Now(), 0); !errors.Is(err, ErrBundleBadCPU) {
t.Fatalf("got %v", err)
}
}
func TestVerifiedBundle_RejectsUnverifiedGPU(t *testing.T) {
c := okBundle()
c.GPU.Verified = false
if _, err := c.Verify(time.Now(), 0); !errors.Is(err, ErrBundleBadGPU) {
t.Fatalf("got %v", err)
}
}
func TestVerifiedBundle_RejectsRIMMismatch(t *testing.T) {
c := okBundle()
c.GPU.RIMMatched = false
if _, err := c.Verify(time.Now(), 0); !errors.Is(err, ErrBundleRIMMismatch) {
t.Fatalf("got %v", err)
}
}
func TestVerifiedBundle_RejectsBindingMismatch(t *testing.T) {
c := okBundle()
c.CPU.ReportData = [32]byte{0xff} // wrong binding
if _, err := c.Verify(time.Now(), 0); !errors.Is(err, ErrBundleBindingMissing) {
t.Fatalf("got %v", err)
}
}
func TestVerifiedBundle_RejectsStaleCPU(t *testing.T) {
c := okBundle()
c.CPU.VerifiedAt = time.Now().Add(-25 * time.Hour)
if _, err := c.Verify(time.Now(), time.Hour); !errors.Is(err, ErrBundleStale) {
t.Fatalf("got %v", err)
}
}
func TestVerifiedBundle_RejectsStaleGPU(t *testing.T) {
c := okBundle()
c.GPU.VerifiedAt = time.Now().Add(-25 * time.Hour)
if _, err := c.Verify(time.Now(), time.Hour); !errors.Is(err, ErrBundleStale) {
t.Fatalf("got %v", err)
}
}
func TestVerifiedBundle_RejectsMissingWorker(t *testing.T) {
c := okBundle()
c.WorkerID = ""
if _, err := c.Verify(time.Now(), 0); !errors.Is(err, ErrBundleMissingWorker) {
t.Fatalf("got %v", err)
}
}
func TestBindCPUReportData_StableAcrossInputs(t *testing.T) {
a := BindCPUReportData("w", "u", [32]byte{1})
b := BindCPUReportData("w", "u", [32]byte{1})
c := BindCPUReportData("w", "u", [32]byte{2})
if a != b {
t.Fatalf("not deterministic")
}
if a == c {
t.Fatalf("nonce collision")
}
}
func TestTEEKind_String(t *testing.T) {
cases := map[TEEKind]string{
TEENone: "none",
TEESEVSNP: "sev_snp",
TEETDX: "tdx",
TEEKind(99): "invalid",
}
for k, want := range cases {
if k.String() != want {
t.Fatalf("k=%d got %s want %s", k, k.String(), want)
}
}
}
-149
View File
@@ -1,149 +0,0 @@
// GPU evidence-report parser. The NVIDIA Confidential Compute stack
// produces a JSON-encoded "evidence" envelope that the host forwards
// to the NVIDIA Remote Attestation Service (NRAS). The envelope wraps
// a binary attestation report signed by the GPU's confidential-compute
// firmware plus the GPU's certificate chain.
//
// This parser is a clean-room implementation of the publicly
// documented schema (see nvTrust, NVIDIA Apache-2 release). We do NOT
// vendor NVIDIA proprietary code. Field names mirror the public spec
// so downstream code reads naturally against the documentation.
package nvidia
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
)
// Errors returned by the parser.
var (
ErrReportEmpty = errors.New("nvidia/report: empty input")
ErrReportBadJSON = errors.New("nvidia/report: bad JSON envelope")
ErrReportBadVersion = errors.New("nvidia/report: unsupported evidence_version")
ErrReportMissingNonce = errors.New("nvidia/report: missing nonce")
ErrReportBadNonce = errors.New("nvidia/report: nonce must be 32 bytes hex")
ErrReportMissingArch = errors.New("nvidia/report: missing architecture")
ErrReportUnknownArch = errors.New("nvidia/report: unknown architecture")
ErrReportNoMeasurements = errors.New("nvidia/report: no measurements present")
)
// SupportedEvidenceVersions enumerates the report-envelope versions
// this parser understands. Bumped when NVIDIA ships a new schema.
var SupportedEvidenceVersions = map[string]bool{
"2.0": true, // Hopper H100 GA
"2.1": true, // Hopper post-launch fixes
"3.0": true, // Blackwell B100/B200
}
// SupportedArchitectures enumerates the GPU architectures we recognise.
var SupportedArchitectures = map[string]bool{
"Hopper": true,
"Blackwell": true,
}
// Measurement is one entry in the GPU's measurement list. PCRIndex
// follows the spec's natural numbering. Value is the raw measurement
// bytes (typically a 48-byte SHA-384 digest).
type Measurement struct {
PCRIndex uint8 `json:"pcr_index"`
Name string `json:"name"`
Value []byte `json:"-"` // decoded from ValueHex
ValueHex string `json:"value"`
}
// rawReport is the on-the-wire envelope. Field names are the
// canonical lower-snake spelling used by the NVIDIA evidence schema.
type rawReport struct {
EvidenceVersion string `json:"evidence_version"`
GPUUUID string `json:"gpu_uuid"`
Architecture string `json:"architecture"`
DriverVersion string `json:"driver_version"`
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
NVSwitchPresent bool `json:"nvswitch_present"`
}
// GPUReport is the parsed, validated GPU evidence report — every field
// downstream verification logic needs, with no JSON-specific shape.
type GPUReport struct {
EvidenceVersion string
UUID string
Architecture string
DriverVersion string
VBIOSVersion string
Nonce [32]byte
Measurements []Measurement
CertChain []string // PEM blocks (verified at NRAS, kept for audit)
QuoteB64 string
NVSwitchPresent bool
}
// ParseGPUReport reads the JSON envelope and returns a validated
// GPUReport. All structural problems are caught here so downstream
// consumers (NRAS client, RIM matcher, composite) deal only with
// well-formed input.
func ParseGPUReport(data []byte) (*GPUReport, error) {
if len(data) == 0 {
return nil, ErrReportEmpty
}
var r rawReport
if err := json.Unmarshal(data, &r); err != nil {
return nil, fmt.Errorf("%w: %v", ErrReportBadJSON, err)
}
if !SupportedEvidenceVersions[r.EvidenceVersion] {
return nil, fmt.Errorf("%w: %q", ErrReportBadVersion, r.EvidenceVersion)
}
if r.Architecture == "" {
return nil, ErrReportMissingArch
}
if !SupportedArchitectures[r.Architecture] {
return nil, fmt.Errorf("%w: %q", ErrReportUnknownArch, r.Architecture)
}
if r.NonceHex == "" {
return nil, ErrReportMissingNonce
}
nonceBytes, err := hex.DecodeString(strings.TrimPrefix(r.NonceHex, "0x"))
if err != nil || len(nonceBytes) != 32 {
return nil, ErrReportBadNonce
}
if len(r.Measurements) == 0 {
return nil, ErrReportNoMeasurements
}
for i := range r.Measurements {
raw, err := hex.DecodeString(strings.TrimPrefix(r.Measurements[i].ValueHex, "0x"))
if err != nil {
return nil, fmt.Errorf("nvidia/report: bad measurement[%d] hex: %v", i, err)
}
r.Measurements[i].Value = raw
}
rep := &GPUReport{
EvidenceVersion: r.EvidenceVersion,
UUID: r.GPUUUID,
Architecture: r.Architecture,
DriverVersion: r.DriverVersion,
VBIOSVersion: r.VBIOSVersion,
Measurements: r.Measurements,
CertChain: r.CertChain,
QuoteB64: r.Quote,
NVSwitchPresent: r.NVSwitchPresent,
}
copy(rep.Nonce[:], nonceBytes)
return rep, nil
}
// 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 {
out := make(map[string]Measurement, len(r.Measurements))
for _, m := range r.Measurements {
out[m.Name] = m
}
return out
}
-115
View File
@@ -1,115 +0,0 @@
package nvidia
import (
"encoding/hex"
"errors"
"testing"
)
func goodReportJSON() []byte {
return []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"}
],
"cert_chain": ["-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----"],
"attestation_quote": "AAAA",
"nvswitch_present": false
}`)
}
func TestParseGPUReport_HappyPath(t *testing.T) {
r, err := ParseGPUReport(goodReportJSON())
if err != nil {
t.Fatalf("parse: %v", err)
}
if r.UUID != "GPU-1234-5678" {
t.Fatalf("uuid: %s", r.UUID)
}
if r.Architecture != "Hopper" {
t.Fatalf("arch: %s", r.Architecture)
}
if len(r.Measurements) != 2 {
t.Fatalf("measurements: %d", len(r.Measurements))
}
if r.Measurements[0].Name != "FW_RUNTIME" {
t.Fatalf("name: %s", r.Measurements[0].Name)
}
if hex.EncodeToString(r.Measurements[0].Value) != "deadbeef00112233" {
t.Fatalf("value decode: %x", r.Measurements[0].Value)
}
}
func TestParseGPUReport_RejectsEmpty(t *testing.T) {
if _, err := ParseGPUReport(nil); !errors.Is(err, ErrReportEmpty) {
t.Fatalf("got %v", err)
}
}
func TestParseGPUReport_RejectsBadJSON(t *testing.T) {
if _, err := ParseGPUReport([]byte("{not json")); !errors.Is(err, ErrReportBadJSON) {
t.Fatalf("got %v", err)
}
}
func TestParseGPUReport_RejectsBadVersion(t *testing.T) {
in := []byte(`{"evidence_version":"0.9","gpu_uuid":"x","architecture":"Hopper","nonce":"` + hex.EncodeToString(make([]byte, 32)) + `","measurements":[{"name":"a","value":"00"}]}`)
if _, err := ParseGPUReport(in); !errors.Is(err, ErrReportBadVersion) {
t.Fatalf("got %v", err)
}
}
func TestParseGPUReport_RejectsUnknownArch(t *testing.T) {
in := []byte(`{"evidence_version":"2.1","gpu_uuid":"x","architecture":"Pascal","nonce":"` + hex.EncodeToString(make([]byte, 32)) + `","measurements":[{"name":"a","value":"00"}]}`)
if _, err := ParseGPUReport(in); !errors.Is(err, ErrReportUnknownArch) {
t.Fatalf("got %v", err)
}
}
func TestParseGPUReport_RejectsMissingNonce(t *testing.T) {
in := []byte(`{"evidence_version":"2.1","gpu_uuid":"x","architecture":"Hopper","measurements":[{"name":"a","value":"00"}]}`)
if _, err := ParseGPUReport(in); !errors.Is(err, ErrReportMissingNonce) {
t.Fatalf("got %v", err)
}
}
func TestParseGPUReport_RejectsBadNonce(t *testing.T) {
in := []byte(`{"evidence_version":"2.1","gpu_uuid":"x","architecture":"Hopper","nonce":"deadbeef","measurements":[{"name":"a","value":"00"}]}`)
if _, err := ParseGPUReport(in); !errors.Is(err, ErrReportBadNonce) {
t.Fatalf("got %v", err)
}
}
func TestParseGPUReport_RejectsNoMeasurements(t *testing.T) {
in := []byte(`{"evidence_version":"2.1","gpu_uuid":"x","architecture":"Hopper","nonce":"` + hex.EncodeToString(make([]byte, 32)) + `","measurements":[]}`)
if _, err := ParseGPUReport(in); !errors.Is(err, ErrReportNoMeasurements) {
t.Fatalf("got %v", err)
}
}
func TestParseGPUReport_RejectsBadMeasurementHex(t *testing.T) {
in := []byte(`{"evidence_version":"2.1","gpu_uuid":"x","architecture":"Hopper","nonce":"` + hex.EncodeToString(make([]byte, 32)) + `","measurements":[{"name":"a","value":"GG"}]}`)
if _, err := ParseGPUReport(in); err == nil {
t.Fatal("expected error on bad hex")
}
}
func TestMeasurementMap(t *testing.T) {
r, err := ParseGPUReport(goodReportJSON())
if err != nil {
t.Fatalf("parse: %v", err)
}
m := r.MeasurementMap()
if len(m) != 2 {
t.Fatalf("map size %d", len(m))
}
if _, ok := m["FW_RUNTIME"]; !ok {
t.Fatal("missing FW_RUNTIME")
}
}
-141
View File
@@ -1,141 +0,0 @@
//go:build hwattest
// Real-hardware attestation tests. Build with `-tags hwattest` on a
// host with a Hopper (H100) or Blackwell (B100/B200) GPU plus the
// NVIDIA confidential-compute stack. The default build excludes this
// file so CI does not fault on hardware that is not present.
//
// Set environment:
//
// NRAS_ENDPOINT — full URL to NRAS (default: production NRAS)
// NRAS_TRUST_ROOT_PEM — path to a PEM file containing the NRAS
// signing public key
// GPU_REPORT_PATH — path to a JSON evidence report produced by
// the local nvidia-cc-host binary
// RIM_PATH — path to a signed RIM JSON for this driver
// and VBIOS combination
// RIM_TRUST_ROOT_PEM — path to a PEM file containing the RIM
// signing public key (may equal NRAS root)
//
// All tests skip when their inputs are missing — running with the
// build tag on a host that lacks a configured GPU is harmless.
package nvidia
import (
"context"
"crypto/x509"
"encoding/pem"
"os"
"testing"
"time"
)
func loadPubKeyPEM(t *testing.T, path string) interface{} {
t.Helper()
if path == "" {
t.Skipf("PEM path not set")
}
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read PEM %s: %v", path, err)
}
block, _ := pem.Decode(raw)
if block == nil {
t.Fatalf("no PEM block in %s", path)
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
t.Fatalf("parse PKIX %s: %v", path, err)
}
return pub
}
func TestHardware_ParseLocalGPUReport(t *testing.T) {
path := os.Getenv("GPU_REPORT_PATH")
if path == "" {
t.Skip("GPU_REPORT_PATH not set")
}
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read evidence: %v", err)
}
rep, err := ParseGPUReport(raw)
if err != nil {
t.Fatalf("parse evidence: %v", err)
}
if rep.Architecture != "Hopper" && rep.Architecture != "Blackwell" {
t.Fatalf("unexpected arch: %q", rep.Architecture)
}
t.Logf("parsed report: arch=%s driver=%s vbios=%s",
rep.Architecture, rep.DriverVersion, rep.VBIOSVersion)
}
func TestHardware_VerifyAgainstRealNRAS(t *testing.T) {
endpoint := os.Getenv("NRAS_ENDPOINT")
if endpoint == "" {
t.Skip("NRAS_ENDPOINT not set")
}
reportPath := os.Getenv("GPU_REPORT_PATH")
if reportPath == "" {
t.Skip("GPU_REPORT_PATH not set")
}
rootPath := os.Getenv("NRAS_TRUST_ROOT_PEM")
if rootPath == "" {
t.Skip("NRAS_TRUST_ROOT_PEM not set")
}
raw, err := os.ReadFile(reportPath)
if err != nil {
t.Fatalf("read evidence: %v", err)
}
rep, err := ParseGPUReport(raw)
if err != nil {
t.Fatalf("parse evidence: %v", err)
}
pub := loadPubKeyPEM(t, rootPath)
c := NewNRASClient(endpoint, []TrustRoot{{KeyID: "nras", Public: pub}})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
res, err := c.Verify(ctx, raw, rep.Nonce, VerifyOptions{})
if err != nil {
t.Fatalf("nras verify: %v", err)
}
if res.Token == "" {
t.Fatal("empty token")
}
}
func TestHardware_RIMMatchAgainstReport(t *testing.T) {
reportPath := os.Getenv("GPU_REPORT_PATH")
rimPath := os.Getenv("RIM_PATH")
rimRoot := os.Getenv("RIM_TRUST_ROOT_PEM")
if reportPath == "" || rimPath == "" || rimRoot == "" {
t.Skip("GPU_REPORT_PATH / RIM_PATH / RIM_TRUST_ROOT_PEM not all set")
}
rep, err := readReport(reportPath)
if err != nil {
t.Fatalf("read report: %v", err)
}
rimRaw, err := os.ReadFile(rimPath)
if err != nil {
t.Fatalf("read rim: %v", err)
}
pub := loadPubKeyPEM(t, rimRoot)
rim, err := ParseAndVerifyRIM(rimRaw, []TrustRoot{{KeyID: "rim", Public: pub}})
if err != nil {
t.Fatalf("rim parse+verify: %v", err)
}
if err := rim.Match(rep); err != nil {
t.Fatalf("rim match: %v", err)
}
}
func readReport(path string) (*GPUReport, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return ParseGPUReport(raw)
}
-426
View File
@@ -1,426 +0,0 @@
// NRAS HTTP client. The NVIDIA Remote Attestation Service ingests a
// GPU evidence envelope and returns a signed attestation token (a
// JWS-style JWT) that downstream verifiers can carry forward.
//
// We do not embed any NVIDIA-published verification keys. The token
// is treated as opaque-but-bound: callers verify the token's
// signature against trust roots they configure (same TrustRoot type
// the RIM uses). This keeps the package license-clean.
//
// Wire shape (verified against publicly documented NRAS request/response):
//
// POST /v1/attest/gpu HTTP/1.1
// Content-Type: application/json
// {
// "evidence": "<base64 of the JSON evidence envelope>",
// "nonce": "<32-byte hex>",
// "policy": "<NRAS policy ID, optional>"
// }
//
// 200 OK
// Content-Type: application/json
// {
// "token": "<JWT>",
// "expires_at": "<RFC3339>"
// }
package nvidia
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"strings"
"time"
)
// Errors returned by the NRAS client.
var (
ErrNRASNoEndpoint = errors.New("nvidia/nras: endpoint not configured")
ErrNRASNoEvidence = errors.New("nvidia/nras: empty evidence")
ErrNRASBadStatus = errors.New("nvidia/nras: non-2xx response")
ErrNRASBadResponse = errors.New("nvidia/nras: malformed response body")
ErrNRASEmptyToken = errors.New("nvidia/nras: token field missing")
ErrNRASNonceMismatch = errors.New("nvidia/nras: token nonce does not match request")
ErrNRASTokenExpired = errors.New("nvidia/nras: token already expired")
ErrNRASBadJWT = errors.New("nvidia/nras: token not a valid JWS compact serialization")
ErrNRASBadSignature = errors.New("nvidia/nras: token signature verification failed")
// Algorithm-substitution defenses (Red audit F3 / CVE-2015-9235 family).
// verifyJWS enforces a strict alg ↔ key-type binding at the dispatch
// layer; mismatches are rejected before any verification primitive runs.
ErrAlgKeyTypeMismatch = errors.New("nvidia/nras: jws alg does not match trust-root key type")
ErrAlgNoneRefused = errors.New("nvidia/nras: jws alg=none refused")
ErrUnknownAlg = errors.New("nvidia/nras: jws alg unknown")
)
// NRASClient posts GPU evidence to NRAS and returns a verified
// VerifyResult. The HTTP client is injectable so tests use httptest
// directly.
type NRASClient struct {
Endpoint string // e.g. "https://nras.attestation.nvidia.com"
HTTPClient *http.Client // nil = http.DefaultClient with 15s timeout
TrustRoots []TrustRoot // public keys that may sign NRAS tokens
Now func() time.Time
}
// NewNRASClient builds a client. trustRoots may be empty for tests
// that bypass token-signature verification (see VerifyOptions).
func NewNRASClient(endpoint string, trustRoots []TrustRoot) *NRASClient {
return &NRASClient{
Endpoint: strings.TrimRight(endpoint, "/"),
HTTPClient: &http.Client{Timeout: 15 * time.Second},
TrustRoots: trustRoots,
}
}
// VerifyOptions tunes a single Verify call.
type VerifyOptions struct {
// PolicyID is the NRAS-side policy the request opts into. Empty
// uses NRAS's default policy.
PolicyID string
}
// VerifyResult captures the post-verification facts. Callers fold
// these into GPUEvidence.
type VerifyResult struct {
Token string // raw JWS compact form
TokenHash [32]byte // sha256 of the token bytes
Nonce [32]byte // echoed from the request — must match GPU report
ExpiresAt time.Time // honored if non-zero
IssuedAt time.Time // RFC3339 from the response
SignerKeyID string // which trust root verified the JWS
Architecture string // mirrored back from the report; for audit
}
// Verify uploads evidence and returns the parsed result.
//
// `report` is the GPU evidence (the same JSON ParseGPUReport accepts).
// `nonce` is the freshness binding — typically the value placed in
// the GPU report's nonce field. They MUST match; the function asserts
// this before any HTTP call.
func (c *NRASClient) Verify(ctx context.Context, report []byte, nonce [32]byte, opt VerifyOptions) (*VerifyResult, error) {
if c == nil || c.Endpoint == "" {
return nil, ErrNRASNoEndpoint
}
if len(report) == 0 {
return nil, ErrNRASNoEvidence
}
// Local consistency: report.nonce must equal request nonce.
parsed, err := ParseGPUReport(report)
if err != nil {
return nil, fmt.Errorf("nvidia/nras: parse evidence: %w", err)
}
if parsed.Nonce != nonce {
return nil, fmt.Errorf("%w: report=%x request=%x", ErrNRASNonceMismatch, parsed.Nonce[:], nonce[:])
}
body := map[string]string{
"evidence": base64.StdEncoding.EncodeToString(report),
"nonce": hex.EncodeToString(nonce[:]),
}
if opt.PolicyID != "" {
body["policy"] = opt.PolicyID
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("nvidia/nras: marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
c.Endpoint+"/v1/attest/gpu", bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("nvidia/nras: build request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
hc := c.HTTPClient
if hc == nil {
hc = http.DefaultClient
}
resp, err := hc.Do(req)
if err != nil {
return nil, fmt.Errorf("nvidia/nras: http: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1 MB cap
if err != nil {
return nil, fmt.Errorf("nvidia/nras: read body: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("%w: status=%d body=%s", ErrNRASBadStatus, resp.StatusCode, truncate(string(respBody), 256))
}
var rr struct {
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
}
if err := json.Unmarshal(respBody, &rr); err != nil {
return nil, fmt.Errorf("%w: %v", ErrNRASBadResponse, err)
}
if rr.Token == "" {
return nil, ErrNRASEmptyToken
}
res := &VerifyResult{
Token: rr.Token,
TokenHash: sha256.Sum256([]byte(rr.Token)),
Nonce: nonce,
Architecture: parsed.Architecture,
}
// Parse JWT header + claims to recover iat/exp/nonce/kid.
hdr, payload, sigBytes, signedInput, err := splitJWS(rr.Token)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrNRASBadJWT, err)
}
var jwtClaims struct {
Nonce string `json:"nonce"`
Iat int64 `json:"iat"`
Exp int64 `json:"exp"`
}
if err := json.Unmarshal(payload, &jwtClaims); err != nil {
return nil, fmt.Errorf("%w: claims: %v", ErrNRASBadJWT, err)
}
// Token nonce must match request nonce — defense against replay.
if jwtClaims.Nonce != "" {
jn, derr := hex.DecodeString(strings.TrimPrefix(jwtClaims.Nonce, "0x"))
if derr != nil || len(jn) != 32 || sha256BytesEqual(jn, nonce[:]) == false {
return nil, ErrNRASNonceMismatch
}
}
if jwtClaims.Iat > 0 {
res.IssuedAt = time.Unix(jwtClaims.Iat, 0).UTC()
}
if jwtClaims.Exp > 0 {
res.ExpiresAt = time.Unix(jwtClaims.Exp, 0).UTC()
} else if rr.ExpiresAt != "" {
// Fall back to outer expires_at if claims didn't carry it.
if t, perr := time.Parse(time.RFC3339, rr.ExpiresAt); perr == nil {
res.ExpiresAt = t.UTC()
}
}
if !res.ExpiresAt.IsZero() && c.now().After(res.ExpiresAt) {
return nil, ErrNRASTokenExpired
}
var jwtHdr struct {
Alg string `json:"alg"`
Kid string `json:"kid"`
}
if err := json.Unmarshal(hdr, &jwtHdr); err != nil {
return nil, fmt.Errorf("%w: header: %v", ErrNRASBadJWT, err)
}
if len(c.TrustRoots) == 0 {
return nil, fmt.Errorf("%w: no trust roots", ErrNRASBadSignature)
}
root, ok := findTrustRoot(c.TrustRoots, jwtHdr.Kid)
if !ok {
return nil, fmt.Errorf("%w: kid=%q not in trust roots", ErrNRASBadSignature, jwtHdr.Kid)
}
if err := verifyJWS(root.Public, jwtHdr.Alg, signedInput, sigBytes); err != nil {
return nil, fmt.Errorf("%w: %v", ErrNRASBadSignature, err)
}
res.SignerKeyID = root.KeyID
return res, nil
}
// FoldIntoEvidence projects a successful VerifyResult plus a parsed
// GPUReport plus a matched RIM into the GPUEvidence record consumed
// by CompositeAttestation.
func FoldIntoEvidence(r *VerifyResult, report *GPUReport, rim *RIM) GPUEvidence {
out := GPUEvidence{
UUID: report.UUID,
DriverVersion: report.DriverVersion,
VBIOSVersion: report.VBIOSVersion,
Architecture: report.Architecture,
Verified: true,
NRASTokenHash: r.TokenHash,
Nonce: r.Nonce,
VerifiedAt: r.IssuedAt,
NVSwitchVerified: report.NVSwitchPresent,
}
if rim != nil {
out.MeasurementRoot = rim.MeasurementRoot()
out.RIMMatched = true
}
return out
}
// =============================================================================
// internal helpers
// =============================================================================
func (c *NRASClient) now() time.Time {
if c.Now != nil {
return c.Now()
}
return time.Now()
}
// splitJWS returns header, payload, signature, signed-input ([]byte
// form of "<header>.<payload>") for a JWS compact serialization.
func splitJWS(token string) (hdr, payload, sig, signedInput []byte, err error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, nil, nil, nil, fmt.Errorf("expected 3 parts, got %d", len(parts))
}
hdr, err = base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("header: %w", err)
}
payload, err = base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("payload: %w", err)
}
sig, err = base64.RawURLEncoding.DecodeString(parts[2])
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("signature: %w", err)
}
signedInput = []byte(parts[0] + "." + parts[1])
return hdr, payload, sig, signedInput, nil
}
// verifyJWS verifies signedInput vs sig under pub. The dispatch is
// driven by `alg` from the JWS header AND simultaneously enforces that
// the public-key type matches `alg` — this is the algorithm-substitution
// defense (Red audit F3 / CVE-2015-9235 family). A token forged with
// header alg=EdDSA against a trust-root holding an *rsa.PublicKey for
// the same kid is rejected here, before any verification primitive runs.
//
// For ECDSA the curve is also asserted against the alg (ES256→P-256,
// ES384→P-384). alg=none (any case) is refused unconditionally;
// unknown algs are refused.
//
// The companion verifySignature(pub, msg, sig) in rim.go is intentionally
// retained for the RIM path: detached RIM signatures carry no alg envelope,
// so dispatch by key-type is correct there. This split (JWS = alg-driven,
// RIM = key-type-driven) is the only correct factoring; merging them
// re-introduces the substitution attack.
func verifyJWS(pub crypto.PublicKey, alg string, signedInput, sig []byte) error {
switch alg {
case "EdDSA":
pubED, ok := pub.(ed25519.PublicKey)
if !ok {
return fmt.Errorf("%w: alg=EdDSA pub=%T", ErrAlgKeyTypeMismatch, pub)
}
if !ed25519.Verify(pubED, signedInput, sig) {
return errors.New("ed25519 verify failed")
}
return nil
case "ES256":
pubEC, ok := pub.(*ecdsa.PublicKey)
if !ok {
return fmt.Errorf("%w: alg=ES256 pub=%T", ErrAlgKeyTypeMismatch, pub)
}
if pubEC.Curve != elliptic.P256() {
return fmt.Errorf("%w: alg=ES256 curve=%s (want P-256)", ErrAlgKeyTypeMismatch, pubEC.Curve.Params().Name)
}
digest := sha256.Sum256(signedInput)
return ecdsaVerifyJOSE(pubEC, digest[:], sig)
case "ES384":
pubEC, ok := pub.(*ecdsa.PublicKey)
if !ok {
return fmt.Errorf("%w: alg=ES384 pub=%T", ErrAlgKeyTypeMismatch, pub)
}
if pubEC.Curve != elliptic.P384() {
return fmt.Errorf("%w: alg=ES384 curve=%s (want P-384)", ErrAlgKeyTypeMismatch, pubEC.Curve.Params().Name)
}
digest := sha512.Sum384(signedInput)
return ecdsaVerifyJOSE(pubEC, digest[:], sig)
case "PS256":
pubRSA, ok := pub.(*rsa.PublicKey)
if !ok {
return fmt.Errorf("%w: alg=PS256 pub=%T", ErrAlgKeyTypeMismatch, pub)
}
digest := sha256.Sum256(signedInput)
if err := rsa.VerifyPSS(pubRSA, crypto.SHA256, digest[:], sig, &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
Hash: crypto.SHA256,
}); err != nil {
return fmt.Errorf("rsa-pss verify: %w", err)
}
return nil
case "PS384":
pubRSA, ok := pub.(*rsa.PublicKey)
if !ok {
return fmt.Errorf("%w: alg=PS384 pub=%T", ErrAlgKeyTypeMismatch, pub)
}
digest := sha512.Sum384(signedInput)
if err := rsa.VerifyPSS(pubRSA, crypto.SHA384, digest[:], sig, &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
Hash: crypto.SHA384,
}); err != nil {
return fmt.Errorf("rsa-pss verify: %w", err)
}
return nil
case "none", "None", "NONE", "":
return ErrAlgNoneRefused
default:
return fmt.Errorf("%w: %q", ErrUnknownAlg, alg)
}
}
// ecdsaVerifyJOSE verifies a JOSE-flavored ECDSA signature (fixed-width
// r||s, big-endian, equal length — RFC 7515 §3.4) over the supplied
// pre-hashed digest. The hash algorithm is the caller's responsibility
// and must match the JWS alg.
func ecdsaVerifyJOSE(pub *ecdsa.PublicKey, digest, sig []byte) error {
if len(sig) == 0 || len(sig)%2 != 0 {
return fmt.Errorf("ecdsa jose: bad signature length %d", len(sig))
}
half := len(sig) / 2
// JOSE width must equal the curve's coordinate size.
if want := (pub.Curve.Params().BitSize + 7) / 8; half != want {
return fmt.Errorf("ecdsa jose: signature half=%d want=%d for %s", half, want, pub.Curve.Params().Name)
}
r := new(big.Int).SetBytes(sig[:half])
s := new(big.Int).SetBytes(sig[half:])
if !ecdsa.Verify(pub, digest, r, s) {
return errors.New("ecdsa verify failed")
}
return nil
}
func sha256BytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
var diff byte
for i := range a {
diff |= a[i] ^ b[i]
}
return diff == 0
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}
-349
View File
@@ -1,349 +0,0 @@
package nvidia
import (
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// mockServer returns a stub NRAS that signs an EdDSA token.
type mockServer struct {
priv ed25519.PrivateKey
pub ed25519.PublicKey
keyID string
now func() time.Time
wantNonce *[32]byte
failHTTP int
failBody bool
}
func newMockServer(t *testing.T) *mockServer {
t.Helper()
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("genkey: %v", err)
}
return &mockServer{
priv: priv,
pub: pub,
keyID: "nras-test-1",
now: func() time.Time { return time.Unix(1735689600, 0).UTC() },
}
}
func (m *mockServer) handler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/attest/gpu" || r.Method != http.MethodPost {
http.Error(w, "not found", http.StatusNotFound)
return
}
body, _ := io.ReadAll(r.Body)
var req struct {
Evidence string `json:"evidence"`
Nonce string `json:"nonce"`
}
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
if m.wantNonce != nil {
if req.Nonce != hex.EncodeToString((*m.wantNonce)[:]) {
http.Error(w, "nonce mismatch", http.StatusBadRequest)
return
}
}
if m.failHTTP != 0 {
http.Error(w, "configured failure", m.failHTTP)
return
}
// Build a tiny JWT.
hdr := map[string]string{"alg": "EdDSA", "kid": m.keyID, "typ": "JWT"}
hdrJSON, _ := json.Marshal(hdr)
hdrB64 := base64.RawURLEncoding.EncodeToString(hdrJSON)
now := m.now()
claims := map[string]interface{}{
"iat": now.Unix(),
"exp": now.Add(time.Hour).Unix(),
"nonce": req.Nonce,
}
clmJSON, _ := json.Marshal(claims)
clmB64 := base64.RawURLEncoding.EncodeToString(clmJSON)
signedInput := hdrB64 + "." + clmB64
sig := ed25519.Sign(m.priv, []byte(signedInput))
token := signedInput + "." + base64.RawURLEncoding.EncodeToString(sig)
resp := map[string]string{
"token": token,
"expires_at": now.Add(time.Hour).Format(time.RFC3339),
}
if m.failBody {
w.Write([]byte("{not json"))
return
}
out, _ := json.Marshal(resp)
w.Header().Set("Content-Type", "application/json")
w.Write(out)
}
}
// helper — build a valid GPU report bound to nonce.
func reportBytes(nonce [32]byte) []byte {
return []byte(fmt.Sprintf(`{
"evidence_version": "2.1",
"gpu_uuid": "GPU-AAAA-BBBB",
"architecture": "Hopper",
"driver_version": "535.104.05",
"vbios_version": "96.00.74.00.01",
"nonce": "%s",
"measurements": [{"name":"FW","value":"00"}]
}`, hex.EncodeToString(nonce[:])))
}
func TestNRAS_HappyPath(t *testing.T) {
m := newMockServer(t)
srv := httptest.NewServer(m.handler())
defer srv.Close()
roots := []TrustRoot{{KeyID: m.keyID, Public: m.pub}}
c := NewNRASClient(srv.URL, roots)
c.Now = m.now
var nonce [32]byte
rand.Read(nonce[:])
m.wantNonce = &nonce
res, err := c.Verify(context.Background(), reportBytes(nonce), nonce, VerifyOptions{})
if err != nil {
t.Fatalf("verify: %v", err)
}
if res.Token == "" || res.SignerKeyID != m.keyID {
t.Fatalf("bad result: %+v", res)
}
if res.Nonce != nonce {
t.Fatalf("nonce drift: got %x want %x", res.Nonce, nonce)
}
if res.IssuedAt.IsZero() || res.ExpiresAt.IsZero() {
t.Fatalf("times unset: %+v", res)
}
wantHash := sha256.Sum256([]byte(res.Token))
if res.TokenHash != wantHash {
t.Fatal("token hash drift")
}
}
func TestNRAS_RejectsNonceMismatch(t *testing.T) {
m := newMockServer(t)
srv := httptest.NewServer(m.handler())
defer srv.Close()
roots := []TrustRoot{{KeyID: m.keyID, Public: m.pub}}
c := NewNRASClient(srv.URL, roots)
c.Now = m.now
var nonceA, nonceB [32]byte
rand.Read(nonceA[:])
rand.Read(nonceB[:])
// Report carries nonceA, request says nonceB → reject locally.
_, err := c.Verify(context.Background(), reportBytes(nonceA), nonceB, VerifyOptions{})
if !errors.Is(err, ErrNRASNonceMismatch) {
t.Fatalf("got %v", err)
}
}
func TestNRAS_RejectsBadStatus(t *testing.T) {
m := newMockServer(t)
m.failHTTP = http.StatusUnauthorized
srv := httptest.NewServer(m.handler())
defer srv.Close()
c := NewNRASClient(srv.URL, []TrustRoot{{KeyID: m.keyID, Public: m.pub}})
c.Now = m.now
var nonce [32]byte
rand.Read(nonce[:])
_, err := c.Verify(context.Background(), reportBytes(nonce), nonce, VerifyOptions{})
if !errors.Is(err, ErrNRASBadStatus) {
t.Fatalf("got %v", err)
}
}
func TestNRAS_RejectsMalformedBody(t *testing.T) {
m := newMockServer(t)
m.failBody = true
srv := httptest.NewServer(m.handler())
defer srv.Close()
c := NewNRASClient(srv.URL, []TrustRoot{{KeyID: m.keyID, Public: m.pub}})
c.Now = m.now
var nonce [32]byte
rand.Read(nonce[:])
_, err := c.Verify(context.Background(), reportBytes(nonce), nonce, VerifyOptions{})
if !errors.Is(err, ErrNRASBadResponse) {
t.Fatalf("got %v", err)
}
}
func TestNRAS_RejectsExpiredToken(t *testing.T) {
m := newMockServer(t)
srv := httptest.NewServer(m.handler())
defer srv.Close()
roots := []TrustRoot{{KeyID: m.keyID, Public: m.pub}}
c := NewNRASClient(srv.URL, roots)
// client clock 25h after server's "now"
c.Now = func() time.Time { return m.now().Add(25 * time.Hour) }
var nonce [32]byte
rand.Read(nonce[:])
_, err := c.Verify(context.Background(), reportBytes(nonce), nonce, VerifyOptions{})
if !errors.Is(err, ErrNRASTokenExpired) {
t.Fatalf("got %v", err)
}
}
func TestNRAS_RejectsNoTrustRoots(t *testing.T) {
m := newMockServer(t)
srv := httptest.NewServer(m.handler())
defer srv.Close()
c := NewNRASClient(srv.URL, nil)
c.Now = m.now
var nonce [32]byte
rand.Read(nonce[:])
_, err := c.Verify(context.Background(), reportBytes(nonce), nonce, VerifyOptions{})
if !errors.Is(err, ErrNRASBadSignature) {
t.Fatalf("got %v", err)
}
}
func TestNRAS_RejectsNoEndpoint(t *testing.T) {
c := NewNRASClient("", nil)
var n [32]byte
if _, err := c.Verify(context.Background(), []byte("x"), n, VerifyOptions{}); !errors.Is(err, ErrNRASNoEndpoint) {
t.Fatalf("got %v", err)
}
}
func TestNRAS_RejectsEmptyEvidence(t *testing.T) {
c := NewNRASClient("https://x", nil)
var n [32]byte
if _, err := c.Verify(context.Background(), nil, n, VerifyOptions{}); !errors.Is(err, ErrNRASNoEvidence) {
t.Fatalf("got %v", err)
}
}
// JOSE ECDSA path — token signed with ES256 must parse + verify after
// the JOSE-to-ASN.1 conversion in verifyJWS.
func TestNRAS_ECDSAToken(t *testing.T) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("genkey: %v", err)
}
keyID := "ecdsa-server"
now := time.Unix(1735689600, 0).UTC()
mux := http.NewServeMux()
mux.HandleFunc("/v1/attest/gpu", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var req struct{ Nonce string }
_ = json.Unmarshal(body, &req)
hdr := map[string]string{"alg": "ES256", "kid": keyID, "typ": "JWT"}
hdrJSON, _ := json.Marshal(hdr)
hdrB64 := base64.RawURLEncoding.EncodeToString(hdrJSON)
claims := map[string]interface{}{
"iat": now.Unix(),
"exp": now.Add(time.Hour).Unix(),
"nonce": req.Nonce,
}
clmJSON, _ := json.Marshal(claims)
clmB64 := base64.RawURLEncoding.EncodeToString(clmJSON)
signedInput := hdrB64 + "." + clmB64
digest := sha256.Sum256([]byte(signedInput))
// JOSE ES256: r||s, fixed 32 bytes each.
r2, s2, err := ecdsa.Sign(rand.Reader, priv, digest[:])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jose := joseEncode(r2, s2, 32)
token := signedInput + "." + base64.RawURLEncoding.EncodeToString(jose)
out, _ := json.Marshal(map[string]string{
"token": token,
"expires_at": now.Add(time.Hour).Format(time.RFC3339),
})
w.Header().Set("Content-Type", "application/json")
w.Write(out)
})
srv := httptest.NewServer(mux)
defer srv.Close()
c := NewNRASClient(srv.URL, []TrustRoot{{KeyID: keyID, Public: &priv.PublicKey}})
c.Now = func() time.Time { return now }
var nonce [32]byte
rand.Read(nonce[:])
if _, err := c.Verify(context.Background(), reportBytes(nonce), nonce, VerifyOptions{}); err != nil {
t.Fatalf("verify ecdsa: %v", err)
}
}
func joseEncode(r, s *big.Int, sz int) []byte {
out := make([]byte, sz*2)
rB := r.Bytes()
sB := s.Bytes()
copy(out[sz-len(rB):sz], rB)
copy(out[sz*2-len(sB):], sB)
return out
}
func TestFoldIntoEvidence(t *testing.T) {
report, err := ParseGPUReport(goodReportJSON())
if err != nil {
t.Fatalf("parse: %v", err)
}
rim := &RIM{
Architecture: "Hopper",
DriverVersion: "535.104.05",
VBIOSVersion: "96.00.74.00.01",
Entries: []RIMEntry{
{Name: "FW_RUNTIME", ValueHex: "deadbeef00112233"},
{Name: "VBIOS_RT", ValueHex: "cafebabe44556677"},
},
}
res := &VerifyResult{
Token: "abc.def.ghi", TokenHash: sha256.Sum256([]byte("abc.def.ghi")),
Nonce: report.Nonce, IssuedAt: time.Now(),
}
ev := FoldIntoEvidence(res, report, rim)
if !ev.Verified || !ev.RIMMatched {
t.Fatalf("not verified: %+v", ev)
}
if ev.MeasurementRoot != rim.MeasurementRoot() {
t.Fatal("measurement root drift")
}
if !strings.HasPrefix(ev.UUID, "GPU-") {
t.Fatalf("uuid: %s", ev.UUID)
}
}
-178
View File
@@ -1,178 +0,0 @@
// Algorithm-substitution defense tests for verifyJWS (Red audit F3 /
// CVE-2015-9235 family). The eight cases below pin down the alg ↔
// key-type matrix at the dispatch layer:
//
// 1. EdDSA + ed25519.PublicKey → PASS
// 2. ES256 + *ecdsa.PublicKey on P-256 → PASS
// 3. PS256 + *rsa.PublicKey → PASS
// 4. EdDSA paired with RSA pub → ErrAlgKeyTypeMismatch
// 5. ES256 paired with ed25519 pub → ErrAlgKeyTypeMismatch
// 6. ES256 paired with ecdsa P-384 → ErrAlgKeyTypeMismatch
// 7. alg=none → ErrAlgNoneRefused
// 8. alg=Unknown → ErrUnknownAlg
//
// Cases 4-6 are the substitution attack: a forged JWS header that
// names a different alg than the trust-root's key actually supports.
// Pre-fix, dispatch ran by alg but verification ran by key type, so
// an attacker who could choose the alg got to pick which primitive
// ran against which key. The test for case 4 is the canonical
// reproducer in the audit narrative.
package nvidia
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"errors"
"math/big"
"testing"
)
func TestVerifyJWS_EdDSA_Valid(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("genkey: %v", err)
}
signedInput := []byte("eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJ0ZXN0In0")
sig := ed25519.Sign(priv, signedInput)
if err := verifyJWS(pub, "EdDSA", signedInput, sig); err != nil {
t.Fatalf("expected PASS, got %v", err)
}
}
func TestVerifyJWS_ES256_Valid(t *testing.T) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("genkey: %v", err)
}
signedInput := []byte("eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0")
digest := sha256.Sum256(signedInput)
r, s, err := ecdsa.Sign(rand.Reader, priv, digest[:])
if err != nil {
t.Fatalf("sign: %v", err)
}
sig := joseConcat(r, s, 32) // P-256 coord size = 32 bytes
if err := verifyJWS(&priv.PublicKey, "ES256", signedInput, sig); err != nil {
t.Fatalf("expected PASS, got %v", err)
}
}
func TestVerifyJWS_PS256_Valid(t *testing.T) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("genkey: %v", err)
}
signedInput := []byte("eyJhbGciOiJQUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0")
digest := sha256.Sum256(signedInput)
sig, err := rsa.SignPSS(rand.Reader, priv, crypto.SHA256, digest[:], &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
Hash: crypto.SHA256,
})
if err != nil {
t.Fatalf("sign: %v", err)
}
if err := verifyJWS(&priv.PublicKey, "PS256", signedInput, sig); err != nil {
t.Fatalf("expected PASS, got %v", err)
}
}
// === Algorithm-substitution attacks ===
// alg=EdDSA paired with an RSA trust-root for the same kid. The forged
// header tells the verifier "this is an Ed25519 signature"; the trust
// root holds an *rsa.PublicKey. Pre-fix, verifySignature dispatched by
// key type and ran RSA-PSS on the attacker-chosen signedInput. Post-fix,
// the alg=EdDSA branch type-asserts ed25519.PublicKey and refuses.
func TestVerifyJWS_EdDSA_AgainstRSAPub_Rejected(t *testing.T) {
rsaPriv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("genkey: %v", err)
}
signedInput := []byte("attacker-chosen")
// Attacker can produce *anything* for sig — verifier must reject
// before looking at sig at all.
sig := []byte{0xAA, 0xBB, 0xCC}
err = verifyJWS(&rsaPriv.PublicKey, "EdDSA", signedInput, sig)
if !errors.Is(err, ErrAlgKeyTypeMismatch) {
t.Fatalf("expected ErrAlgKeyTypeMismatch, got %v", err)
}
}
// alg=ES256 paired with ed25519 pub.
func TestVerifyJWS_ES256_AgainstEd25519Pub_Rejected(t *testing.T) {
pub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("genkey: %v", err)
}
err = verifyJWS(pub, "ES256", []byte("input"), []byte{0x00})
if !errors.Is(err, ErrAlgKeyTypeMismatch) {
t.Fatalf("expected ErrAlgKeyTypeMismatch, got %v", err)
}
}
// alg=ES256 paired with ecdsa P-384 pub. Curve mismatch — even though
// both are *ecdsa.PublicKey, ES256 is bound to P-256 only.
func TestVerifyJWS_ES256_AgainstP384Pub_Rejected(t *testing.T) {
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
t.Fatalf("genkey: %v", err)
}
// Construct a real ES384-shaped signature so the failure is
// proven to come from the curve check, not from a malformed sig.
signedInput := []byte("input")
digest := sha512.Sum384(signedInput)
r, s, err := ecdsa.Sign(rand.Reader, priv, digest[:])
if err != nil {
t.Fatalf("sign: %v", err)
}
sig := joseConcat(r, s, 48) // P-384 coord size = 48 bytes
err = verifyJWS(&priv.PublicKey, "ES256", signedInput, sig)
if !errors.Is(err, ErrAlgKeyTypeMismatch) {
t.Fatalf("expected ErrAlgKeyTypeMismatch (curve), got %v", err)
}
}
// alg=none must be refused unconditionally regardless of trust-root key.
func TestVerifyJWS_AlgNone_Refused(t *testing.T) {
pub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("genkey: %v", err)
}
for _, alg := range []string{"none", "None", "NONE", ""} {
err := verifyJWS(pub, alg, []byte("input"), []byte("sig"))
if !errors.Is(err, ErrAlgNoneRefused) {
t.Fatalf("alg=%q: expected ErrAlgNoneRefused, got %v", alg, err)
}
}
}
// Unknown alg must be refused.
func TestVerifyJWS_UnknownAlg_Refused(t *testing.T) {
pub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("genkey: %v", err)
}
err = verifyJWS(pub, "HS256", []byte("input"), []byte("sig"))
if !errors.Is(err, ErrUnknownAlg) {
t.Fatalf("expected ErrUnknownAlg, got %v", err)
}
}
// === helpers ===
// joseConcat encodes (r, s) as fixed-width big-endian r||s (RFC 7515 §3.4).
func joseConcat(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
}
-279
View File
@@ -1,279 +0,0 @@
// Reference Integrity Manifest (RIM) validation. The RIM is the
// signed list of measurement values NVIDIA publishes for each
// driver/VBIOS combination. Match every measurement in a parsed GPU
// report against its RIM entry; reject any non-match.
//
// The signature on the RIM blob is verified against the supplied
// trust roots. We do not bake in NVIDIA's signing keys — operators
// supply them via deployment config. This keeps the package
// license-clean (Apache-2 nvTrust schema only, no NVIDIA cert PEMs
// vendored) and lets enterprises pin their own RIM trust path.
package nvidia
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
)
// Errors returned by RIM validation.
var (
ErrRIMEmpty = errors.New("nvidia/rim: empty manifest")
ErrRIMBadJSON = errors.New("nvidia/rim: bad JSON manifest")
ErrRIMBadSignature = errors.New("nvidia/rim: signature verification failed")
ErrRIMNoTrustRoots = errors.New("nvidia/rim: no trust roots configured")
ErrRIMMissingEntry = errors.New("nvidia/rim: measurement not in manifest")
ErrRIMValueMismatch = errors.New("nvidia/rim: measurement value mismatch")
ErrRIMArchMismatch = errors.New("nvidia/rim: architecture mismatch")
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.
type RIMEntry struct {
Name string `json:"name"`
ValueHex string `json:"value"`
}
// rawRIM is the on-the-wire JSON. The signature block is detachable:
// signers compute it over the canonical body bytes (sorted keys, no
// whitespace) and append the signature alongside.
type rawRIM struct {
Architecture string `json:"architecture"`
DriverVersion string `json:"driver_version"`
VBIOSVersion string `json:"vbios_version"`
Entries []RIMEntry `json:"entries"`
SignerKeyID string `json:"signer_key_id,omitempty"`
SignatureB64 string `json:"signature,omitempty"`
}
// RIM is a parsed, signature-verified Reference Integrity Manifest.
type RIM struct {
Architecture string
DriverVersion string
VBIOSVersion string
Entries []RIMEntry
// SignerKeyID identifies which trust root verified the manifest.
// Empty if the manifest was loaded with VerifySignature disabled
// (development only).
SignerKeyID string
}
// TrustRoot is a public key registered to validate RIM signatures.
// Algorithm is inferred from the key type at verify time — we accept
// Ed25519, ECDSA P-256/P-384, and RSA-PSS (SHA-256). Operators wire
// their NVIDIA RIM signing keys here at startup.
type TrustRoot struct {
KeyID string
Public crypto.PublicKey
}
// ParseAndVerifyRIM parses the manifest bytes and verifies the
// detached signature against trustRoots. If trustRoots is empty,
// returns ErrRIMNoTrustRoots — there is no insecure mode.
//
// On success, returns a *RIM with SignerKeyID set to the trust root
// that produced a valid signature.
func ParseAndVerifyRIM(data []byte, trustRoots []TrustRoot) (*RIM, error) {
if len(data) == 0 {
return nil, ErrRIMEmpty
}
if len(trustRoots) == 0 {
return nil, ErrRIMNoTrustRoots
}
var raw rawRIM
if err := json.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("%w: %v", ErrRIMBadJSON, err)
}
if raw.SignatureB64 == "" {
return nil, fmt.Errorf("%w: missing signature", ErrRIMBadSignature)
}
if raw.SignerKeyID == "" {
return nil, fmt.Errorf("%w: missing signer_key_id", ErrRIMBadSignature)
}
// Re-encode the body (sans signature fields) deterministically so
// the signature is over a stable canonical form.
body := rawRIM{
Architecture: raw.Architecture,
DriverVersion: raw.DriverVersion,
VBIOSVersion: raw.VBIOSVersion,
Entries: raw.Entries,
}
canon, err := canonicalJSON(body)
if err != nil {
return nil, fmt.Errorf("nvidia/rim: canonicalize: %w", err)
}
sig, err := base64.StdEncoding.DecodeString(raw.SignatureB64)
if err != nil {
return nil, fmt.Errorf("%w: bad base64: %v", ErrRIMBadSignature, err)
}
root, ok := findTrustRoot(trustRoots, raw.SignerKeyID)
if !ok {
return nil, fmt.Errorf("%w: signer %q not in trust roots", ErrRIMBadSignature, raw.SignerKeyID)
}
if err := verifySignature(root.Public, canon, sig); err != nil {
return nil, fmt.Errorf("%w: %v", ErrRIMBadSignature, err)
}
return &RIM{
Architecture: raw.Architecture,
DriverVersion: raw.DriverVersion,
VBIOSVersion: raw.VBIOSVersion,
Entries: raw.Entries,
SignerKeyID: raw.SignerKeyID,
}, nil
}
// Match validates that every measurement in report has an entry in
// rim with the same value. Architecture + driver + VBIOS must match
// the manifest's preconditions.
//
// Returns nil iff every measurement matches AND the GPU's
// driver/VBIOS line up with the manifest. Any divergence returns the
// most-specific error so audit logs can categorize failures.
func (rim *RIM) Match(report *GPUReport) error {
if rim == nil || report == nil {
return errors.New("nvidia/rim: nil rim or report")
}
if rim.Architecture != report.Architecture {
return fmt.Errorf("%w: rim=%s report=%s", ErrRIMArchMismatch, rim.Architecture, report.Architecture)
}
if rim.DriverVersion != report.DriverVersion || rim.VBIOSVersion != report.VBIOSVersion {
return fmt.Errorf("%w: rim=%s/%s report=%s/%s", ErrRIMVersionMismatch,
rim.DriverVersion, rim.VBIOSVersion, report.DriverVersion, report.VBIOSVersion)
}
want := make(map[string][]byte, len(rim.Entries))
for _, e := range rim.Entries {
v, err := hex.DecodeString(strings.TrimPrefix(e.ValueHex, "0x"))
if err != nil {
return fmt.Errorf("nvidia/rim: bad entry hex %q: %w", e.Name, err)
}
want[e.Name] = v
}
for _, m := range report.Measurements {
exp, ok := want[m.Name]
if !ok {
return fmt.Errorf("%w: name=%s", ErrRIMMissingEntry, m.Name)
}
if !bytesEqual(exp, m.Value) {
return fmt.Errorf("%w: name=%s", ErrRIMValueMismatch, m.Name)
}
}
return nil
}
// MeasurementRoot returns sha256(name||value) folded into a single
// root over a deterministic ordering. Used as the canonical
// MeasurementRoot in GPUEvidence.
func (rim *RIM) MeasurementRoot() [32]byte {
// Sort entries by Name so the root is deterministic regardless of
// list order in the manifest.
names := make([]string, 0, len(rim.Entries))
values := make(map[string][]byte, len(rim.Entries))
for _, e := range rim.Entries {
names = append(names, e.Name)
v, _ := hex.DecodeString(strings.TrimPrefix(e.ValueHex, "0x"))
values[e.Name] = v
}
sortStrings(names)
h := sha256.New()
for _, n := range names {
_, _ = h.Write([]byte(n))
_, _ = h.Write([]byte{0})
_, _ = h.Write(values[n])
_, _ = h.Write([]byte{0})
}
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
// =============================================================================
// internal helpers
// =============================================================================
// canonicalJSON re-encodes v with sorted keys and no whitespace. The
// stdlib's encoding/json already sorts struct fields by source order
// and map keys alphabetically; for our struct shape that is sufficient.
// We reject any nested map[string]interface{} that would require deeper
// canonicalization than stdlib provides.
func canonicalJSON(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
func findTrustRoot(roots []TrustRoot, keyID string) (TrustRoot, bool) {
for _, r := range roots {
if r.KeyID == keyID {
return r, true
}
}
return TrustRoot{}, false
}
func verifySignature(pub crypto.PublicKey, msg, sig []byte) error {
switch k := pub.(type) {
case ed25519.PublicKey:
if !ed25519.Verify(k, msg, sig) {
return errors.New("ed25519 verify failed")
}
return nil
case *ecdsa.PublicKey:
// ASN.1 DER signature over sha256(msg).
digest := sha256.Sum256(msg)
if !ecdsa.VerifyASN1(k, digest[:], sig) {
return errors.New("ecdsa verify failed")
}
return nil
case *rsa.PublicKey:
digest := sha256.Sum256(msg)
// PSS with SHA-256, salt length = hash length.
err := rsa.VerifyPSS(k, crypto.SHA256, digest[:], sig, &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
Hash: crypto.SHA256,
})
if err != nil {
return fmt.Errorf("rsa verify: %w", err)
}
return nil
default:
return fmt.Errorf("unsupported trust root key type %T", pub)
}
}
func bytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
var diff byte
for i := range a {
diff |= a[i] ^ b[i]
}
return diff == 0
}
func sortStrings(s []string) {
// insertion sort — n is tiny (measurement count per RIM), avoids
// pulling in sort just for this.
for i := 1; i < len(s); i++ {
v := s[i]
j := i - 1
for j >= 0 && s[j] > v {
s[j+1] = s[j]
j--
}
s[j+1] = v
}
}
-229
View File
@@ -1,229 +0,0 @@
package nvidia
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"testing"
)
// makeManifest produces a signed RIM manifest for the supplied entries
// using the provided ed25519 key.
func makeManifestEd25519(t *testing.T, priv ed25519.PrivateKey, kid string, entries []RIMEntry) []byte {
t.Helper()
body := rawRIM{
Architecture: "Hopper",
DriverVersion: "535.104.05",
VBIOSVersion: "96.00.74.00.01",
Entries: entries,
}
canon, err := json.Marshal(body)
if err != nil {
t.Fatalf("canonicalize: %v", err)
}
sig := ed25519.Sign(priv, canon)
body.SignerKeyID = kid
body.SignatureB64 = base64.StdEncoding.EncodeToString(sig)
out, err := json.Marshal(body)
if err != nil {
t.Fatalf("re-marshal: %v", err)
}
return out
}
func TestParseAndVerifyRIM_Ed25519(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("genkey: %v", err)
}
roots := []TrustRoot{{KeyID: "nvidia-rim-1", Public: pub}}
entries := []RIMEntry{
{Name: "FW_RUNTIME", ValueHex: "deadbeef"},
{Name: "VBIOS_RT", ValueHex: "cafebabe"},
}
blob := makeManifestEd25519(t, priv, "nvidia-rim-1", entries)
r, err := ParseAndVerifyRIM(blob, roots)
if err != nil {
t.Fatalf("verify: %v", err)
}
if r.SignerKeyID != "nvidia-rim-1" {
t.Fatalf("signer: %s", r.SignerKeyID)
}
}
func TestParseAndVerifyRIM_RejectsTamperedSignature(t *testing.T) {
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
roots := []TrustRoot{{KeyID: "k", Public: pub}}
entries := []RIMEntry{{Name: "X", ValueHex: "00"}}
blob := makeManifestEd25519(t, priv, "k", entries)
// Flip a byte in the manifest entries — signature must reject.
tampered := append([]byte{}, blob...)
for i := range tampered {
if tampered[i] == 'X' {
tampered[i] = 'Y'
break
}
}
if _, err := ParseAndVerifyRIM(tampered, roots); !errors.Is(err, ErrRIMBadSignature) {
t.Fatalf("got %v", err)
}
}
func TestParseAndVerifyRIM_RejectsUnknownSigner(t *testing.T) {
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
roots := []TrustRoot{{KeyID: "OTHER", Public: pub}}
blob := makeManifestEd25519(t, priv, "k", []RIMEntry{{Name: "X", ValueHex: "00"}})
if _, err := ParseAndVerifyRIM(blob, roots); !errors.Is(err, ErrRIMBadSignature) {
t.Fatalf("got %v", err)
}
}
func TestParseAndVerifyRIM_RejectsEmpty(t *testing.T) {
if _, err := ParseAndVerifyRIM(nil, []TrustRoot{{KeyID: "k"}}); !errors.Is(err, ErrRIMEmpty) {
t.Fatalf("got %v", err)
}
}
func TestParseAndVerifyRIM_RejectsNoTrustRoots(t *testing.T) {
if _, err := ParseAndVerifyRIM([]byte("{}"), nil); !errors.Is(err, ErrRIMNoTrustRoots) {
t.Fatalf("got %v", err)
}
}
func TestParseAndVerifyRIM_ECDSA(t *testing.T) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("genkey: %v", err)
}
roots := []TrustRoot{{KeyID: "ecdsa-1", Public: &priv.PublicKey}}
body := rawRIM{
Architecture: "Hopper",
DriverVersion: "535.104.05",
VBIOSVersion: "96.00.74.00.01",
Entries: []RIMEntry{{Name: "FW", ValueHex: "01"}},
}
canon, _ := json.Marshal(body)
digest := sha256.Sum256(canon)
sig, err := ecdsa.SignASN1(rand.Reader, priv, digest[:])
if err != nil {
t.Fatalf("sign: %v", err)
}
body.SignerKeyID = "ecdsa-1"
body.SignatureB64 = base64.StdEncoding.EncodeToString(sig)
blob, _ := json.Marshal(body)
if _, err := ParseAndVerifyRIM(blob, roots); err != nil {
t.Fatalf("ecdsa verify: %v", err)
}
}
func TestParseAndVerifyRIM_RSAPSS(t *testing.T) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("genkey: %v", err)
}
roots := []TrustRoot{{KeyID: "rsa-1", Public: &priv.PublicKey}}
body := rawRIM{
Architecture: "Hopper", DriverVersion: "535.104.05",
VBIOSVersion: "96", Entries: []RIMEntry{{Name: "FW", ValueHex: "01"}},
}
canon, _ := json.Marshal(body)
digest := sha256.Sum256(canon)
sig, err := rsa.SignPSS(rand.Reader, priv, crypto.SHA256, digest[:], &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash, Hash: crypto.SHA256,
})
if err != nil {
t.Fatalf("sign: %v", err)
}
body.SignerKeyID = "rsa-1"
body.SignatureB64 = base64.StdEncoding.EncodeToString(sig)
blob, _ := json.Marshal(body)
if _, err := ParseAndVerifyRIM(blob, roots); err != nil {
t.Fatalf("rsa verify: %v", err)
}
}
func TestRIM_Match_HappyPath(t *testing.T) {
rim := &RIM{
Architecture: "Hopper",
DriverVersion: "535.104.05",
VBIOSVersion: "96.00.74.00.01",
Entries: []RIMEntry{
{Name: "FW_RUNTIME", ValueHex: "deadbeef00112233"},
{Name: "VBIOS_RT", ValueHex: "cafebabe44556677"},
},
}
report, err := ParseGPUReport(goodReportJSON())
if err != nil {
t.Fatalf("report: %v", err)
}
if err := rim.Match(report); err != nil {
t.Fatalf("match: %v", err)
}
}
func TestRIM_Match_RejectsArchMismatch(t *testing.T) {
rim := &RIM{Architecture: "Blackwell", DriverVersion: "535.104.05", VBIOSVersion: "96.00.74.00.01"}
report, _ := ParseGPUReport(goodReportJSON())
if err := rim.Match(report); !errors.Is(err, ErrRIMArchMismatch) {
t.Fatalf("got %v", err)
}
}
func TestRIM_Match_RejectsVersionMismatch(t *testing.T) {
rim := &RIM{Architecture: "Hopper", DriverVersion: "OTHER", VBIOSVersion: "OTHER"}
report, _ := ParseGPUReport(goodReportJSON())
if err := rim.Match(report); !errors.Is(err, ErrRIMVersionMismatch) {
t.Fatalf("got %v", err)
}
}
func TestRIM_Match_RejectsMissingMeasurement(t *testing.T) {
rim := &RIM{
Architecture: "Hopper",
DriverVersion: "535.104.05",
VBIOSVersion: "96.00.74.00.01",
Entries: []RIMEntry{
{Name: "OTHER", ValueHex: "00"},
},
}
report, _ := ParseGPUReport(goodReportJSON())
if err := rim.Match(report); !errors.Is(err, ErrRIMMissingEntry) {
t.Fatalf("got %v", err)
}
}
func TestRIM_Match_RejectsValueMismatch(t *testing.T) {
rim := &RIM{
Architecture: "Hopper",
DriverVersion: "535.104.05",
VBIOSVersion: "96.00.74.00.01",
Entries: []RIMEntry{
{Name: "FW_RUNTIME", ValueHex: "00"},
{Name: "VBIOS_RT", ValueHex: "cafebabe44556677"},
},
}
report, _ := ParseGPUReport(goodReportJSON())
if err := rim.Match(report); !errors.Is(err, ErrRIMValueMismatch) {
t.Fatalf("got %v", err)
}
}
func TestRIM_MeasurementRoot_Deterministic(t *testing.T) {
a := &RIM{Entries: []RIMEntry{{Name: "B", ValueHex: "02"}, {Name: "A", ValueHex: "01"}}}
b := &RIM{Entries: []RIMEntry{{Name: "A", ValueHex: "01"}, {Name: "B", ValueHex: "02"}}}
if a.MeasurementRoot() != b.MeasurementRoot() {
t.Fatal("root differs across orderings")
}
}
+1 -1
View File
@@ -51,7 +51,7 @@ import (
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"github.com/luxfi/mpc/cc/attest"
"github.com/luxfi/cc/attest"
)
// CompositeAttestation is the contract surface the release gate consumes
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"golang.org/x/crypto/curve25519"
"github.com/luxfi/mpc/cc/attest"
"github.com/luxfi/cc/attest"
)
// x25519Basepoint multiplies priv by the curve25519 basepoint.
+1 -1
View File
@@ -83,7 +83,7 @@ func (l IOLevel) String() string {
// Worker is the scheduler's view of a candidate node for a session.
// All fields are populated by the registry from attested measurements
// (see pkg/attestation/nvidia for the GPU side).
// (see github.com/luxfi/cc/attest/nvidia for the GPU side).
type Worker struct {
ID string
TrustMode TrustMode