Files
mpc/cc/attest/verifier.go
zeekay 48744d2e82 cc/attest: canonical TEE attestation verifier (#222 stage 1)
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.
2026-04-28 00:47:37 -07:00

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
}