Files
zeekay da19c97e0e feat(attest): KindTDX is production-real — full Intel DCAP v4 quote verification
TDX was a fail-loud stub (Verify returned ErrNotImplemented, parsing nothing).
It now performs the complete Intel TDX DCAP attestation flow via the canonical
Google verifier github.com/google/go-tdx-guest (v0.3.1, sibling of the
go-sev-guest used by KindSEVSNP), matching the SEV path's rigor:

  - Parse the DCAP v4 quote (QuoteV4 only) via go-tdx-guest/abi.
  - Verify the PCK certificate chain embedded in the quote's signed
    certification data up to the Intel SGX Root CA pinned inside go-tdx-guest
    (verify/trusted_root.pem) — the trust anchor is never taken from the quote.
  - Verify the Quoting Enclave (QE) report signature and the attestation-key
    binding (QE report data == SHA-256(att key || QE auth data)).
  - Verify the TD report (TDQuoteBody) ECDSA-P256 signature.
  - Fetch Intel PCS collateral (TCB Info + QE Identity + PCK/Root CRLs) and
    enforce TCB status (UpToDate) and revocation. GetCollateral and
    CheckRevocations are ON; DefaultOptions leaves them off, so we set them.
  - Fold MRTD + the four RTMRs into a 48-byte SHA-384 TD measurement root
    (Measurement); raw registers + MRSEAM/TD_ATTRIBUTES/XFAM/etc. in Extra.
  - Enforce caller policy (expected measurement / report-data nonce binding)
    in constant time, and bind CompositeHash to the canonical TD fields.

(nil, err) always means refuse — no partial report, no best-effort fallback.
go-tdx-guest error strings are classified onto the package refusal taxonomy
(ErrInvalidEvidence / ErrSignatureInvalid / ErrChainInvalid / ErrPolicy).

Tests (attest/tdx_test.go) run offline against a REAL Intel Sapphire Rapids
TDX quote + Intel collateral committed verbatim to testdata/ (from the
go-tdx-guest corpus; the go-tdx-guest `testing` getter is linux-gated, so a
small replay getter is used here):
  - real SPR quote passes the full DCAP cryptographic chain;
  - full production path refuses the fixture on TCB status (upstream
    deliberately bumped the sample TCB SVN above the quote) -> ErrPolicy;
  - tamper rejection: flipped MRTD breaks the TD report signature, flipped
    ECDSA sig rejected, bad version / truncation / garbage -> ErrInvalidEvidence;
  - measurement fold + composite hash determinism + policy binding;
  - no-fallback when Intel PCS is unreachable; stale-clock refusal; Dispatch
    routing; context cancellation.

verifier_test.go: retired TestStubsReturnNotImplemented /
TestDispatch_StubKindsReturnNotImplemented (TDX was the last stub kind they
asserted) now that TDX is real — mirrors the earlier NVTrust promotion which
already edited this file. go.mod/go.sum: add go-tdx-guest v0.3.1 (latest, < v1).

GOWORK=off go build/vet/test/-race ./... all green.
2026-06-27 13:49:26 -07:00

391 lines
15 KiB
Go

// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package attest
import (
"bytes"
"context"
_ "embed"
"errors"
"strings"
"testing"
"time"
"github.com/google/go-tdx-guest/abi"
tdxpb "github.com/google/go-tdx-guest/proto/tdx"
"github.com/google/go-tdx-guest/verify"
)
// tdxProdQuote is a real Intel TDX DCAP v4 quote captured from a Sapphire
// Rapids (SPR, E4 stepping) TD. Sourced verbatim from the upstream
// go-tdx-guest test corpus (testing/testdata/tdx_prod_quote_SPR_E4.dat) and
// committed so this package's tests need no live TDX hardware or network.
//
//go:embed testdata/tdx_prod_quote_spr_e4.bin
var tdxProdQuote []byte
// The Intel PCS collateral that pairs with the quote above, committed
// verbatim from the same upstream corpus. NOTE: upstream deliberately bumped
// the TCB SVNs in tdx_tcb_info.json above the quote's TEE_TCB_SVN, so the
// full collateral path is EXPECTED to refuse on TCB status — that is what
// TestTDX_Verify_FullProductionPath_RefusesStaleTCB asserts. The quote itself
// is cryptographically genuine (TestTDX_QuoteIsCryptographicallyAuthentic).
//
//go:embed testdata/tdx_tcb_info.json
var tdxTcbInfoBody []byte
//go:embed testdata/tdx_qe_identity.json
var tdxQeIdentityBody []byte
//go:embed testdata/tdx_pck_crl.body
var tdxPckCrlBody []byte
//go:embed testdata/tdx_root_ca_crl.der
var tdxRootCaCrlBody []byte
// Issuer chains returned as Intel PCS response headers. The TCB Info and QE
// Identity share the Intel SGX TCB-Signing issuer chain; the PCK CRL uses the
// PCK Platform CA chain.
//
//go:embed testdata/tdx_collateral_issuer_chain.txt
var tdxCollateralIssuerChain string
//go:embed testdata/tdx_pck_crl_issuer_chain.txt
var tdxPckCrlIssuerChain string
// tdxFixedNow pins the verification clock inside the validity window of the
// committed quote's PCK chain and collateral (the quote is from ~2023). Tests
// are reproducible regardless of when CI runs, exactly like the SEV path's
// fixedNow.
func tdxFixedNow() time.Time {
return time.Date(2023, time.July, 1, 1, 0, 0, 0, time.UTC)
}
// Absolute byte offsets into the raw quote, per the Intel DCAP v4 layout
// (header 0x30, then the TD quote body; see go-tdx-guest/abi):
// - MRTD begins at header(0x30) + body-relative MRTD offset(0x88).
// - The ECDSA-P256 signature begins at header(0x30) + body(0x248) +
// signed-data-size(4).
const (
tdxAbsMRTDOffset = 0x30 + 0x88
tdxAbsSigOffset = 0x30 + 0x248 + 4
)
// tdxReplayGetter replays the committed Intel PCS collateral so the full
// production verification path runs offline. The URL keys are exactly the
// ones go-tdx-guest requests (mirrored from the upstream TestGetter); a
// substring fallback keeps the test robust to incidental URL formatting.
type tdxReplayGetter struct{}
func (tdxReplayGetter) Get(url string) (map[string][]string, []byte, error) {
const (
hdrTCB = "Tcb-Info-Issuer-Chain"
hdrQE = "Sgx-Enclave-Identity-Issuer-Chain"
hdrPCK = "Sgx-Pck-Crl-Issuer-Chain"
)
switch {
case strings.Contains(url, "qe/identity"):
return map[string][]string{hdrQE: {tdxCollateralIssuerChain}}, tdxQeIdentityBody, nil
case strings.Contains(url, "/tcb"):
return map[string][]string{hdrTCB: {tdxCollateralIssuerChain}}, tdxTcbInfoBody, nil
case strings.Contains(url, "pckcrl"):
return map[string][]string{hdrPCK: {tdxPckCrlIssuerChain}}, tdxPckCrlBody, nil
case strings.Contains(url, "RootCA"):
return nil, tdxRootCaCrlBody, nil
default:
return nil, nil, errors.New("tdx replay getter: unexpected url " + url)
}
}
// tdxErrGetter simulates an unreachable Intel PCS.
type tdxErrGetter struct{}
func (tdxErrGetter) Get(string) (map[string][]string, []byte, error) {
return nil, nil, errors.New("intel pcs unreachable")
}
func mustParseQuote(t *testing.T, raw []byte) *tdxpb.QuoteV4 {
t.Helper()
any, err := abi.QuoteToProto(raw)
if err != nil {
t.Fatalf("QuoteToProto: %v", err)
}
q, ok := any.(*tdxpb.QuoteV4)
if !ok {
t.Fatalf("quote is %T, want *tdxpb.QuoteV4", any)
}
return q
}
// -----------------------------------------------------------------------------
// Cryptographic authenticity of the real quote
// -----------------------------------------------------------------------------
// TestTDX_QuoteIsCryptographicallyAuthentic proves the committed SPR quote
// passes the full DCAP cryptographic chain — PCK chain to the Intel SGX Root
// CA pinned in go-tdx-guest, QE report signature, attestation-key binding, and
// the TD report ECDSA-P256 signature — independent of the (deliberately stale)
// TCB collateral. This documents that the only reason the full production path
// refuses this fixture is the TCB SVN bump, not a broken quote.
func TestTDX_QuoteIsCryptographicallyAuthentic(t *testing.T) {
quote := mustParseQuote(t, tdxProdQuote)
// GetCollateral:false isolates the chain+signature checks from the TCB
// collateral comparison.
err := verify.TdxQuote(quote, &verify.Options{
GetCollateral: false,
CheckRevocations: false,
Now: tdxFixedNow(),
})
if err != nil {
t.Fatalf("real Intel SPR quote failed cryptographic verification: %v", err)
}
}
// -----------------------------------------------------------------------------
// Full production path: TCB-status enforcement against real Intel collateral
// -----------------------------------------------------------------------------
// TestTDX_Verify_FullProductionPath_RefusesStaleTCB drives the public
// TDX.Verify with the live-equivalent replay getter, exercising the entire
// production pipeline: parse -> PCK chain -> Intel SGX Root CA -> QE sig ->
// attestation-key binding -> TD report sig -> Intel PCS collateral fetch ->
// TCB-status comparison -> revocation. The committed collateral's TCB SVN is
// above the quote's, so the TCB gate fires and the platform is REFUSED.
func TestTDX_Verify_FullProductionPath_RefusesStaleTCB(t *testing.T) {
rep, err := TDX{}.Verify(context.Background(), tdxProdQuote,
WithKDSGetter(tdxReplayGetter{}), WithNow(tdxFixedNow()))
if err == nil {
t.Fatalf("expected refusal on stale TCB, got report %+v", rep)
}
if rep != nil {
t.Errorf("refusal must return a nil report, got %+v", rep)
}
if !errors.Is(err, ErrPolicy) {
t.Errorf("stale TCB: want ErrPolicy, got %v", err)
}
if !strings.Contains(err.Error(), "TCB") {
t.Errorf("stale TCB: want a TCB-status message, got %v", err)
}
}
// TestTDX_Reject_NoFallbackOnCollateralFailure proves there is no best-effort
// fallback: if Intel PCS is unreachable, verification refuses.
func TestTDX_Reject_NoFallbackOnCollateralFailure(t *testing.T) {
rep, err := TDX{}.Verify(context.Background(), tdxProdQuote,
WithKDSGetter(tdxErrGetter{}), WithNow(tdxFixedNow()))
if err == nil || rep != nil {
t.Fatalf("expected refusal when collateral is unreachable, got (%+v, %v)", rep, err)
}
if !errors.Is(err, ErrChainInvalid) {
t.Errorf("collateral unreachable: want ErrChainInvalid, got %v", err)
}
}
// TestTDX_Reject_StaleClock proves the verifier honors the verification clock
// and refuses material that is expired at that time.
func TestTDX_Reject_StaleClock(t *testing.T) {
farFuture := time.Date(2053, time.July, 1, 1, 0, 0, 0, time.UTC)
_, err := TDX{}.Verify(context.Background(), tdxProdQuote,
WithKDSGetter(tdxReplayGetter{}), WithNow(farFuture))
if err == nil {
t.Fatal("expected refusal with an expired chain/collateral at far-future clock")
}
if !errors.Is(err, ErrChainInvalid) {
t.Errorf("stale clock: want ErrChainInvalid, got %v", err)
}
}
// -----------------------------------------------------------------------------
// Measurement extraction, fold, composite hash, and policy
// -----------------------------------------------------------------------------
func TestTDX_ExtractMeasurements_RealQuote(t *testing.T) {
quote := mustParseQuote(t, tdxProdQuote)
body := quote.GetTdQuoteBody()
rep, err := buildTDXReport(quote, applyOptions())
if err != nil {
t.Fatalf("buildTDXReport: %v", err)
}
if rep.Kind != KindTDX {
t.Errorf("Kind = %q, want %q", rep.Kind, KindTDX)
}
if rep.Vendor != tdxVendor {
t.Errorf("Vendor = %q, want %q", rep.Vendor, tdxVendor)
}
// Measurement is the 48-byte fold over MRTD + the four RTMRs.
if len(rep.Measurement) != tdxMrTdSize {
t.Fatalf("Measurement len = %d, want %d", len(rep.Measurement), tdxMrTdSize)
}
wantFold := foldTDXMeasurement(body.GetMrTd(), body.GetRtmrs())
if !bytes.Equal(rep.Measurement, wantFold) {
t.Errorf("Measurement is not the MRTD+RTMR fold")
}
// The fold must not equal MRTD alone (proves the RTMRs are bound in).
if bytes.Equal(rep.Measurement, body.GetMrTd()) {
t.Errorf("Measurement equals raw MRTD; RTMRs are not folded in")
}
if bytes.Equal(rep.Measurement, make([]byte, tdxMrTdSize)) {
t.Errorf("Measurement is all-zero")
}
// ReportData round-trips the verified 64-byte field.
if len(rep.ReportData) != tdxReportDataLen {
t.Errorf("ReportData len = %d, want %d", len(rep.ReportData), tdxReportDataLen)
}
if !bytes.Equal(rep.ReportData, body.GetReportData()) {
t.Errorf("ReportData does not match the quote body")
}
// CompositeHash is non-zero and deterministic.
var zero [32]byte
if rep.CompositeHash == zero {
t.Errorf("CompositeHash is zero")
}
rep2, _ := buildTDXReport(quote, applyOptions())
if rep.CompositeHash != rep2.CompositeHash {
t.Errorf("CompositeHash is not deterministic")
}
// Extra carries the raw registers for finer-grained policy/audit.
for _, k := range []string{"tdx.mrtd", "tdx.rtmr0", "tdx.rtmr1", "tdx.rtmr2", "tdx.rtmr3", "tdx.tee_tcb_svn"} {
if rep.Extra[k] == "" {
t.Errorf("Extra missing key %q", k)
}
}
t.Logf("TD measurement root (fold) = %x", rep.Measurement)
t.Logf("CompositeHash = %x", rep.CompositeHash)
}
func TestTDX_Policy_MeasurementAndReportDataBinding(t *testing.T) {
quote := mustParseQuote(t, tdxProdQuote)
body := quote.GetTdQuoteBody()
goodFold := foldTDXMeasurement(body.GetMrTd(), body.GetRtmrs())
goodRD := body.GetReportData()
// Correct expected measurement -> accepted.
if _, err := buildTDXReport(quote, applyOptions(WithExpectedMeasurement(goodFold))); err != nil {
t.Errorf("correct expected measurement rejected: %v", err)
}
// Wrong expected measurement -> ErrPolicy.
wrong := append([]byte(nil), goodFold...)
wrong[0] ^= 0xFF
if _, err := buildTDXReport(quote, applyOptions(WithExpectedMeasurement(wrong))); !errors.Is(err, ErrPolicy) {
t.Errorf("wrong measurement: want ErrPolicy, got %v", err)
}
// Correct expected report data (nonce binding) -> accepted.
if _, err := buildTDXReport(quote, applyOptions(WithExpectedReportData(goodRD))); err != nil {
t.Errorf("correct expected report data rejected: %v", err)
}
// Wrong expected report data -> ErrPolicy.
wrongRD := append([]byte(nil), goodRD...)
wrongRD[0] ^= 0xFF
if _, err := buildTDXReport(quote, applyOptions(WithExpectedReportData(wrongRD))); !errors.Is(err, ErrPolicy) {
t.Errorf("wrong report data: want ErrPolicy, got %v", err)
}
}
// -----------------------------------------------------------------------------
// Tamper rejection
// -----------------------------------------------------------------------------
// TestTDX_Tamper_MRTD_BreaksSignature flips a byte of MRTD and proves the TD
// report signature no longer verifies — i.e. the measurement is genuinely
// bound by the hardware signature, so a forged measurement is rejected.
func TestTDX_Tamper_MRTD_BreaksSignature(t *testing.T) {
tampered := append([]byte(nil), tdxProdQuote...)
tampered[tdxAbsMRTDOffset] ^= 0xFF
any, err := abi.QuoteToProto(tampered)
if err != nil {
return // parser rejecting it is also a valid refusal
}
quote := any.(*tdxpb.QuoteV4)
verr := verify.TdxQuote(quote, &verify.Options{GetCollateral: false, Now: tdxFixedNow()})
if verr == nil {
t.Fatal("tampered MRTD still verified — measurement not bound by signature")
}
if got := classifyTDXVerifyError(verr); !errors.Is(got, ErrSignatureInvalid) {
t.Errorf("tampered MRTD: want ErrSignatureInvalid, got %v (raw: %v)", got, verr)
}
}
// TestTDX_Tamper_Signature_Rejected flips a byte of the ECDSA signature and
// proves verification refuses.
func TestTDX_Tamper_Signature_Rejected(t *testing.T) {
tampered := append([]byte(nil), tdxProdQuote...)
tampered[tdxAbsSigOffset] ^= 0xFF
any, err := abi.QuoteToProto(tampered)
if err != nil {
return // parser rejecting it is also a valid refusal
}
quote := any.(*tdxpb.QuoteV4)
verr := verify.TdxQuote(quote, &verify.Options{GetCollateral: false, Now: tdxFixedNow()})
if verr == nil {
t.Fatal("tampered signature still verified")
}
if got := classifyTDXVerifyError(verr); !errors.Is(got, ErrSignatureInvalid) {
t.Errorf("tampered signature: want ErrSignatureInvalid, got %v (raw: %v)", got, verr)
}
}
// TestTDX_Reject_BadVersion proves an unsupported quote version is refused at
// the parse boundary as ErrInvalidEvidence (no getter needed).
func TestTDX_Reject_BadVersion(t *testing.T) {
bad := append([]byte(nil), tdxProdQuote...)
bad[0] = 0x03 // version is a little-endian uint16 at offset 0; 4 -> 3
_, err := TDX{}.Verify(context.Background(), bad, WithNow(tdxFixedNow()))
if !errors.Is(err, ErrInvalidEvidence) {
t.Errorf("bad version: want ErrInvalidEvidence, got %v", err)
}
}
// TestTDX_Reject_Truncated proves a too-short blob is refused before any trust
// decision.
func TestTDX_Reject_Truncated(t *testing.T) {
_, err := TDX{}.Verify(context.Background(), tdxProdQuote[:abi.QuoteMinSize-1], WithNow(tdxFixedNow()))
if !errors.Is(err, ErrInvalidEvidence) {
t.Errorf("truncated: want ErrInvalidEvidence, got %v", err)
}
}
// TestTDX_Reject_Garbage proves random bytes of sufficient length are refused.
func TestTDX_Reject_Garbage(t *testing.T) {
garbage := make([]byte, abi.QuoteMinSize+16)
for i := range garbage {
garbage[i] = byte(i * 7)
}
_, err := TDX{}.Verify(context.Background(), garbage, WithNow(tdxFixedNow()))
if !errors.Is(err, ErrInvalidEvidence) {
t.Errorf("garbage: want ErrInvalidEvidence, got %v", err)
}
}
// -----------------------------------------------------------------------------
// Dispatch wiring + interface conformance
// -----------------------------------------------------------------------------
// TestTDX_DispatchRoutesToTDX proves Dispatch(KindTDX, ...) reaches this
// verifier (and, with a stale-TCB fixture, refuses just like a direct call).
func TestTDX_DispatchRoutesToTDX(t *testing.T) {
_, err := Dispatch(context.Background(), KindTDX, tdxProdQuote,
WithKDSGetter(tdxReplayGetter{}), WithNow(tdxFixedNow()))
if !errors.Is(err, ErrPolicy) {
t.Errorf("Dispatch(KindTDX): want ErrPolicy (stale TCB), got %v", err)
}
}
func TestTDX_ContextCanceled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := TDX{}.Verify(ctx, tdxProdQuote, WithKDSGetter(tdxReplayGetter{}), WithNow(tdxFixedNow()))
if !errors.Is(err, context.Canceled) {
t.Errorf("canceled context: want context.Canceled, got %v", err)
}
}