mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
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:
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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{}
|
||||
@@ -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
|
||||
@@ -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.
BIN
Binary file not shown.
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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] + "..."
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user