Files
mpc/cc/attest/verifier_test.go
zeekay 41c32bd4a8 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

271 lines
8.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package attest
import (
"bytes"
"context"
_ "embed"
"errors"
"testing"
"time"
"github.com/google/go-sev-guest/verify/trust"
test "github.com/google/go-sev-guest/testing"
)
// 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)
}
}
// -----------------------------------------------------------------------------
// Stubs — honest "not yet implemented" markers; flip these to real
// tests when #222 stages 2 + 3 land.
// -----------------------------------------------------------------------------
func TestTDX_Verify_NotYetImplemented(t *testing.T) {
t.Skip("TDX verifier not yet implemented; tracked at #222 stage 2")
}
func TestNRAS_Verify_NotYetImplemented(t *testing.T) {
t.Skip("NRAS verifier not yet implemented; tracked at #222 stage 3")
}
// TestStubsPanic confirms the stubs FAIL LOUD rather than silently
// returning nil. This guards against an accidental future change that
// silently accepts evidence the verifier hasn't actually checked.
func TestStubsPanic(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 did not panic; stubs MUST fail loud", c.name)
}
}()
_, _ = c.v.Verify(context.Background(), []byte{0x00})
})
}
}