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.
271 lines
8.1 KiB
Go
271 lines
8.1 KiB
Go
// 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})
|
||
})
|
||
}
|
||
}
|