mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
Adds the canonical TEE attestation verifier package at cc/attest. This
is the home for chain validation + trust anchor management; the
luxcpp/crypto/attestation parser primitives feed bytes here BEFORE
their measurement output is trusted.
Stage 1 — SEV-SNP — PRODUCTION:
- cc/attest/sev.go: SEVSNP{} verifier built on
github.com/google/go-sev-guest v0.14.1. Live VCEK fetch from AMD
KDS, ARK→ASK→VCEK chain validation, signature verify against the
leaf, MEASUREMENT/REPORT_DATA/HOST_DATA/CHIP_ID extraction,
caller-supplied policy gates (WithExpectedReportData,
WithExpectedMeasurement). 197 LOC.
- testdata/: real-vendor Milan attestation report + VCEK leaf
committed verbatim from the upstream go-sev-guest test corpus.
Tests replay the AMD KDS responses via SimpleGetter so CI never
hits the network.
- 9 SEV-SNP tests passing (happy path, dispatch, length reject,
tampered signature, wrong report_data, wrong measurement,
deterministic composite hash, unknown kind, stub-panic guard).
Stage 2 — TDX — STUB (#222 stage 2):
- cc/attest/tdx.go: TDX{}.Verify panics with ErrNotImplemented so
any caller routing TDX evidence during stage 1 fails LOUD. Plan
documented inline: go-tdx-guest + Intel PCS + QE/TDX module
identity pinning.
Stage 3 — NRAS — STUB (#222 stage 3):
- cc/attest/nras.go: NRAS{}.Verify panics with ErrNotImplemented.
Plan: lift the existing pkg/attestation/nvidia NRAS client into
the cc/attest contract with a JWKS cache.
Wire-in:
- pkg/attestation/composite.go gains VerifyEvidence(ctx, opts...)
which dispatches each Evidence blob to cc/attest by Issuer.
Callers (kms release gate, scheduler) MUST invoke this before
accepting parser output.
Doc:
- cc/attest/doc.go states the production/stub matrix and the
layering contract between the C-ABI parser and this package.
222 lines
8.1 KiB
Go
222 lines
8.1 KiB
Go
// 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
|
|
}
|