Files
cc/attest/verifier_test.go
zeekay 0d0faff0f8 merge: ARM CCA realm verifier (feat/kind-armcca) — 6th vendor, converged
ARM CCA Realm attestation: a CBOR/COSE CCA token collection (platform token
signed by the operator-pinned CPAK/IAK + realm token signed by the RAK, with
the H(RAK) platform-realm binding enforced). Vendor arm.cca, fail-closed zero
value, real COSE_Sign1 verification.

armcca branched before the registry landed and carried its own parallel copy
(extraVerifiers map + a duplicate registerVerifier + a second VerifierFor
lookup). Converged to the one registry: deleted that block so armcca.go's
init() registers through the canonical registerVerifier in verifier.go, and
repointed its self-registration test to RegisteredVerifier. There is one
registry, one lookup, one dispatch path — no second way.

go.mod: unioned the require blocks (cbor/v2, go-sev-guest, go-tdx-guest,
veraison/ccatoken, go-cose); go mod tidy clean; ccatoken is v1.4.0 and its
transitive jwx/v2, secp256k1/v4 carry their major in the import path — no dep
bumped to a new major. doc.go + verifier.go: ARM CCA is now a real 6th Kind,
not pending. verifier_test.go: TestRegistry_RoutesEveryKind covers all six.

go build/vet/test + go test -race ./attest/... green; gofmt clean.
2026-06-27 14:23:58 -07:00

293 lines
9.6 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"
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)
}
}
// All evidence Kinds are now real verifiers, each fail-closed on bad input and
// covered in its own test file: SEV-SNP (this file), TDX (tdx_test.go), SGX
// (sgx_test.go), NVTrust (nvtrust_test.go), and Nitro (nitro_test.go). No stub
// Kinds remain, so the former TestStubsReturnNotImplemented /
// TestDispatch_StubKindsReturnNotImplemented (which asserted TDX returned
// ErrNotImplemented) have been retired now that TDX is production-real.
// ErrNotImplemented stays a valid refusal sentinel: a future stub Kind should
// ship its own fail-loud test alongside the verifier, mirroring the per-Kind
// test layout.
// TestRegistry_RoutesEveryKind proves the converged dispatch path: every
// shipped Kind self-registered its verifier via init() (one registry, no
// switch), RegisteredVerifier returns the right concrete verifier for each,
// and Dispatch routes through that same registry. All six TEE/GPU vendors are
// covered. An unregistered Kind is fail-closed: RegisteredVerifier reports
// absent and Dispatch refuses with ErrUnsupportedKind, never a silent pass.
func TestRegistry_RoutesEveryKind(t *testing.T) {
registered := []struct {
kind Kind
is func(Verifier) bool
}{
{KindSEVSNP, func(v Verifier) bool { _, ok := v.(SEVSNP); return ok }},
{KindTDX, func(v Verifier) bool { _, ok := v.(TDX); return ok }},
{KindSGX, func(v Verifier) bool { _, ok := v.(SGX); return ok }},
{KindNVTrust, func(v Verifier) bool { _, ok := v.(NVTrust); return ok }},
{KindNitro, func(v Verifier) bool { _, ok := v.(Nitro); return ok }},
{KindARMCCA, func(v Verifier) bool { _, ok := v.(ARMCCA); return ok }},
}
for _, tc := range registered {
t.Run(string(tc.kind), func(t *testing.T) {
v, ok := RegisteredVerifier(tc.kind)
if !ok {
t.Fatalf("Kind %q did not self-register via init()", tc.kind)
}
if !tc.is(v) {
t.Fatalf("Kind %q registered %T, want its own verifier type", tc.kind, v)
}
})
}
// A Kind no verifier installed is fail-closed at both the lookup and the
// dispatch boundary.
for _, kind := range []Kind{Kind("madeup"), Kind("tpm")} {
t.Run("unregistered/"+string(kind), func(t *testing.T) {
if v, ok := RegisteredVerifier(kind); ok {
t.Fatalf("unexpected verifier %T registered for unregistered Kind %q", v, kind)
}
rep, err := Dispatch(context.Background(), kind, []byte{0x00})
if rep != nil {
t.Fatalf("Dispatch(%q) returned non-nil report on unregistered Kind", kind)
}
if !errors.Is(err, ErrUnsupportedKind) {
t.Fatalf("Dispatch(%q) err = %v, want ErrUnsupportedKind", kind, err)
}
})
}
}