mirror of
https://github.com/luxfi/cc.git
synced 2026-09-24 00:24:54 +00:00
Merge remote-tracking branch 'origin/feat/kind-tdx-real'
This commit is contained in:
+297
-27
@@ -5,42 +5,312 @@ package attest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-tdx-guest/abi"
|
||||
tdxpb "github.com/google/go-tdx-guest/proto/tdx"
|
||||
"github.com/google/go-tdx-guest/verify"
|
||||
"github.com/google/go-tdx-guest/verify/trust"
|
||||
)
|
||||
|
||||
// TDX is the Intel TDX verifier.
|
||||
// TDX is the production Intel TDX verifier. It accepts a raw Intel
|
||||
// DCAP v4 TDX quote (the bytes the guest reads from the TDX quote-
|
||||
// generation service) and performs the full DCAP attestation flow via
|
||||
// github.com/google/go-tdx-guest — the canonical Google verifier, sibling
|
||||
// of go-sev-guest used by the SEV-SNP path:
|
||||
//
|
||||
// Status: STUB — tracked at issue #222 stage 2.
|
||||
// 1. Parse the quote via go-tdx-guest/abi.QuoteToProto (QuoteV4 only).
|
||||
// 2. Verify the PCK certificate chain that is EMBEDDED in the quote's
|
||||
// signed certification data (PCK Leaf -> Intel SGX Platform CA ->
|
||||
// Intel SGX Root CA) up to Intel's Root CA, which ships pinned inside
|
||||
// go-tdx-guest (verify/trusted_root.pem). Unlike SEV — where the VCEK
|
||||
// leaf is fetched from the AMD KDS — the TDX leaf travels inside the
|
||||
// quote, so the chain itself needs no network.
|
||||
// 3. Verify the Quoting Enclave (QE) report signature with the verified
|
||||
// PCK leaf key, and verify the attestation-key binding: the QE report
|
||||
// data must equal SHA-256(ECDSA attestation key || QE auth data), so
|
||||
// the key that signed the TD report is the one Intel's QE vouched for.
|
||||
// 4. Verify the TD report (TDQuoteBody) ECDSA-P256 signature made by that
|
||||
// attestation key over the quote header + body.
|
||||
// 5. Fetch Intel PCS collateral (TCB Info + QE Identity, plus PCK and Root
|
||||
// CA CRLs) and enforce TCB status: the platform's TEE_TCB_SVN must map
|
||||
// to an "UpToDate" TCB level, the QE identity must match, and no cert
|
||||
// in the chain may be revoked. This is the freshness/patch-level gate;
|
||||
// a downlevel or revoked platform is REFUSED.
|
||||
// 6. Extract MRTD + the four RTMRs and fold them into a single 48-byte
|
||||
// measurement root, extract REPORTDATA, and enforce caller policy
|
||||
// (expected measurement / expected report data == nonce binding).
|
||||
//
|
||||
// Production plan (intentionally not yet implemented):
|
||||
// Trust anchor: the Intel SGX Root CA pinned inside go-tdx-guest. It is
|
||||
// never taken from the quote. Production requires network reachability to
|
||||
// the Intel PCS (api.trustedservices.intel.com) for collateral, exactly as
|
||||
// the SEV-SNP path requires the AMD KDS; operators that cannot reach Intel
|
||||
// directly front it with a PCCS mirror and point the getter at it. In tests
|
||||
// the getter is overridden (see WithKDSGetter) to replay committed Intel
|
||||
// collateral from testdata/ so verification runs offline.
|
||||
//
|
||||
// - Parse the TDREPORT / quote via github.com/google/go-tdx-guest/abi.
|
||||
// - Fetch the PCK (Provisioning Certification Key) chain from the
|
||||
// Intel PCS at api.trustedservices.intel.com using the report's
|
||||
// FMSPC + PCEID.
|
||||
// - Validate the chain to Intel's pinned root certificate and check
|
||||
// the report signature against the PCK.
|
||||
// - Cross-check the QE Identity and TDX Module Identity against the
|
||||
// pinned values published by Intel.
|
||||
// - Extract MRTD, RTMRs, REPORTDATA, and the platform identity.
|
||||
// - Compose CompositeHash via the same domain-separated SHA-256 as
|
||||
// the SEV path (see verifier.go::computeCompositeHash).
|
||||
//
|
||||
// Verify returns ErrNotImplemented so the call site refuses the release
|
||||
// without crashing the process. Callers MUST treat this exactly like
|
||||
// any other refusal: do not release, do not fall back. A silent no-op
|
||||
// or success would let the release gate accidentally accept evidence
|
||||
// the verifier has not actually checked; the explicit error-return path
|
||||
// preserves the same hard-refusal guarantee while letting Dispatch be
|
||||
// invoked from request-handling goroutines without taking the whole
|
||||
// process down.
|
||||
// As with every Kind: (nil, err) means refuse. There is no partial report
|
||||
// and no best-effort fallback.
|
||||
type TDX struct{}
|
||||
|
||||
// Verify implements Verifier. NOT YET IMPLEMENTED — do not enable TDX
|
||||
// dispatch in production until #222 stage 2 lands.
|
||||
const (
|
||||
tdxVendor = "intel.tdx"
|
||||
|
||||
// Field sizes from the Intel TDX DCAP quote spec (see go-tdx-guest/abi):
|
||||
// MRTD and each RTMR are SHA-384 (48 bytes); REPORTDATA is 64 bytes.
|
||||
tdxMrTdSize = 48
|
||||
tdxRtmrSize = 48
|
||||
tdxRtmrCount = 4
|
||||
tdxReportDataLen = 64
|
||||
)
|
||||
|
||||
// tdxMeasureFoldDomain domain-separates the TD measurement-root fold so it
|
||||
// can never collide with a raw register value or another kind's hash input.
|
||||
var tdxMeasureFoldDomain = []byte("LUX-TDX-MEAS-FOLD-v1")
|
||||
|
||||
// Verify implements Verifier for Intel TDX DCAP v4 quotes.
|
||||
func (TDX) Verify(ctx context.Context, evidence []byte, opts ...Option) (*VerifiedReport, error) {
|
||||
return nil, fmt.Errorf("%w: TDX attestation envelope verification not configured for this build (tracked at #222 stage 2: go-tdx-guest + Intel PCS)",
|
||||
ErrNotImplemented)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(evidence) < abi.QuoteMinSize {
|
||||
return nil, fmt.Errorf("%w: tdx quote expects at least %d bytes, got %d",
|
||||
ErrInvalidEvidence, abi.QuoteMinSize, len(evidence))
|
||||
}
|
||||
|
||||
cfg := applyOptions(opts...)
|
||||
|
||||
// Parse first so we fail fast on malformed framing, and so the extracted
|
||||
// fields below come from a structurally validated quote (CheckQuoteV4 runs
|
||||
// inside QuoteToProto).
|
||||
anyQuote, err := abi.QuoteToProto(evidence)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
|
||||
}
|
||||
quote, ok := anyQuote.(*tdxpb.QuoteV4)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: unsupported tdx quote type %T (only DCAP QuoteV4)",
|
||||
ErrInvalidEvidence, anyQuote)
|
||||
}
|
||||
|
||||
// Build the full-rigor verification options. GetCollateral and
|
||||
// CheckRevocations are ON: without collateral the TEE_TCB_SVN is never
|
||||
// compared to Intel's signed TCB levels, so a downlevel/vulnerable but
|
||||
// otherwise well-formed platform would pass. DefaultOptions() leaves both
|
||||
// off, so we set them explicitly.
|
||||
vopts := &verify.Options{
|
||||
GetCollateral: true,
|
||||
CheckRevocations: true,
|
||||
Now: cfg.nowOrWall(),
|
||||
}
|
||||
// Getter resolution mirrors the SEV path. The cc/attest option carries an
|
||||
// HTTPS getter as `any` (WithKDSGetter); for KindTDX it supplies the Intel
|
||||
// PCS / PCCS getter and tests inject a replay map so the suite runs
|
||||
// offline. Production leaves it nil and the live Intel PCS getter is used.
|
||||
if cfg.kdsGetter != nil {
|
||||
g, ok := cfg.kdsGetter.(trust.HTTPSGetter)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: injected getter does not implement go-tdx-guest trust.HTTPSGetter",
|
||||
ErrInvalidEvidence)
|
||||
}
|
||||
vopts.Getter = g
|
||||
} else {
|
||||
vopts.Getter = trust.DefaultHTTPSGetter()
|
||||
}
|
||||
|
||||
// Full DCAP verification: PCK chain -> Intel SGX Root CA, QE report sig,
|
||||
// attestation-key binding, TD report ECDSA-P256 sig, TCB status, and
|
||||
// revocation. Reuses the parse above (no re-decode).
|
||||
if err := verify.TdxQuote(quote, vopts); err != nil {
|
||||
return nil, classifyTDXVerifyError(err)
|
||||
}
|
||||
|
||||
// Chain + signatures + TCB are valid; extract measurements and apply
|
||||
// caller policy.
|
||||
return buildTDXReport(quote, cfg)
|
||||
}
|
||||
|
||||
// classifyTDXVerifyError maps go-tdx-guest's flat error strings onto the
|
||||
// package refusal taxonomy so callers can switch on errors.Is. go-tdx-guest
|
||||
// returns a single error value; we classify by content. Every branch is a
|
||||
// refusal — when in doubt we treat the failure as a vendor-chain failure
|
||||
// (ErrChainInvalid), never as success. Ordering is significant: the most
|
||||
// specific causes are matched first.
|
||||
func classifyTDXVerifyError(err error) error {
|
||||
msg := err.Error()
|
||||
low := strings.ToLower(msg)
|
||||
switch {
|
||||
// Structural / framing problems the parser surfaces late.
|
||||
case strings.Contains(low, "could not convert raw bytes"),
|
||||
strings.Contains(low, "quote format not supported"),
|
||||
strings.Contains(low, "unsupported quote type"),
|
||||
strings.Contains(low, "is nil"):
|
||||
return fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
|
||||
|
||||
// TCB status: the chain is cryptographically valid but the platform's
|
||||
// patch level does not meet Intel's "UpToDate" bar (or no level matches).
|
||||
// This is a policy refusal on otherwise-valid evidence.
|
||||
case strings.Contains(low, "tcb status"),
|
||||
strings.Contains(low, "matching tcb level"),
|
||||
strings.Contains(low, "tcb level"):
|
||||
return fmt.Errorf("%w: %v", ErrPolicy, err)
|
||||
|
||||
// Revocation: a certificate in the chain was pulled by Intel.
|
||||
case strings.Contains(low, "revoked"),
|
||||
strings.Contains(low, "revocation"):
|
||||
return fmt.Errorf("%w: %v", ErrChainInvalid, err)
|
||||
|
||||
// Signature / attestation-key binding failures. ErrHashVerificationFail
|
||||
// ("unable to verify message digest using quote's signature and ecdsa
|
||||
// attestation key") is the TD report signature; ErrSHA56VerificationFail
|
||||
// ("QE Report Data does not match ... ECDSA Attestation Key ...") is the
|
||||
// attestation-key binding. Both mean the signing key is not authentic.
|
||||
case strings.Contains(low, "signature"),
|
||||
strings.Contains(low, "ecdsa attestation key"),
|
||||
strings.Contains(low, "message digest"),
|
||||
strings.Contains(low, "report data does not match"):
|
||||
return fmt.Errorf("%w: %v", ErrSignatureInvalid, err)
|
||||
|
||||
// Everything else — cert/chain/CRL/issuer/public-key/collateral/expiry —
|
||||
// is a vendor-chain validation failure. Conservative default: refuse.
|
||||
default:
|
||||
return fmt.Errorf("%w: %v", ErrChainInvalid, err)
|
||||
}
|
||||
}
|
||||
|
||||
// buildTDXReport extracts the verified measurements from a TD quote whose
|
||||
// chain, signatures, and TCB status have already passed, applies caller
|
||||
// policy, and returns the canonical VerifiedReport.
|
||||
func buildTDXReport(quote *tdxpb.QuoteV4, cfg config) (*VerifiedReport, error) {
|
||||
body := quote.GetTdQuoteBody()
|
||||
if body == nil {
|
||||
return nil, fmt.Errorf("%w: tdx quote has no TD report body", ErrInvalidEvidence)
|
||||
}
|
||||
|
||||
mrtd := body.GetMrTd()
|
||||
rtmrs := body.GetRtmrs()
|
||||
reportData := body.GetReportData()
|
||||
|
||||
// Defense in depth: the parser already enforced these, but we are about to
|
||||
// make trust decisions on the output of C-ABI / proto code, so we recheck
|
||||
// the lengths that feed the measurement fold and the policy comparisons.
|
||||
if len(mrtd) != tdxMrTdSize {
|
||||
return nil, fmt.Errorf("%w: MRTD is %d bytes, expected %d", ErrInvalidEvidence, len(mrtd), tdxMrTdSize)
|
||||
}
|
||||
if len(rtmrs) != tdxRtmrCount {
|
||||
return nil, fmt.Errorf("%w: got %d RTMRs, expected %d", ErrInvalidEvidence, len(rtmrs), tdxRtmrCount)
|
||||
}
|
||||
for i, r := range rtmrs {
|
||||
if len(r) != tdxRtmrSize {
|
||||
return nil, fmt.Errorf("%w: RTMR%d is %d bytes, expected %d", ErrInvalidEvidence, i, len(r), tdxRtmrSize)
|
||||
}
|
||||
}
|
||||
if len(reportData) != tdxReportDataLen {
|
||||
return nil, fmt.Errorf("%w: REPORTDATA is %d bytes, expected %d", ErrInvalidEvidence, len(reportData), tdxReportDataLen)
|
||||
}
|
||||
|
||||
// Fold MRTD + the four RTMRs into a single 48-byte TD measurement root.
|
||||
// MRTD is the build-time launch measurement; the RTMRs are the runtime-
|
||||
// extended registers (firmware config, kernel, OS, runtime). Pinning the
|
||||
// fold pins the entire launch+runtime state in one comparison value.
|
||||
measurement := foldTDXMeasurement(mrtd, rtmrs)
|
||||
|
||||
// Caller policy on the verified fields. Constant-time compares.
|
||||
if cfg.expectedReportData != nil {
|
||||
if subtle.ConstantTimeCompare(cfg.expectedReportData, reportData) != 1 {
|
||||
return nil, fmt.Errorf("%w: report_data mismatch (got %s)",
|
||||
ErrPolicy, hex.EncodeToString(reportData))
|
||||
}
|
||||
}
|
||||
if cfg.expectedMeasurement != nil {
|
||||
if subtle.ConstantTimeCompare(cfg.expectedMeasurement, measurement) != 1 {
|
||||
return nil, fmt.Errorf("%w: measurement mismatch (got %s)",
|
||||
ErrPolicy, hex.EncodeToString(measurement))
|
||||
}
|
||||
}
|
||||
|
||||
out := &VerifiedReport{
|
||||
Kind: KindTDX,
|
||||
Vendor: tdxVendor,
|
||||
Measurement: measurement,
|
||||
ReportData: cloneBytes(reportData),
|
||||
// HostData/ChipID: TDX exposes no host-data or per-chip identifier in
|
||||
// the TD report (PCK identity is per-platform FMSPC, surfaced in Extra
|
||||
// where available). Left nil rather than populated with a stand-in.
|
||||
IssuedAt: cfg.nowOrWall().UTC(),
|
||||
Extra: buildTDXExtra(body),
|
||||
}
|
||||
out.CompositeHash = computeCompositeHash(KindTDX, canonicalTDXBytes(body))
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// foldTDXMeasurement returns SHA-384(domain || 0x00 || MRTD || RTMR0..3).
|
||||
// SHA-384 keeps the root at the 48-byte width of the TDX measurement
|
||||
// registers themselves, symmetric with the SEV-SNP 48-byte launch digest.
|
||||
func foldTDXMeasurement(mrtd []byte, rtmrs [][]byte) []byte {
|
||||
h := sha512.New384()
|
||||
h.Write(tdxMeasureFoldDomain)
|
||||
h.Write([]byte{0x00}) // domain separator
|
||||
h.Write(mrtd)
|
||||
for _, r := range rtmrs {
|
||||
h.Write(r)
|
||||
}
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// buildTDXExtra collects TDX-specific fields that don't fit the common shape.
|
||||
// Keys are stable wire identifiers prefixed "tdx."; values are hex. The raw
|
||||
// MRTD and each RTMR are exposed individually so callers can pin finer-grained
|
||||
// policy than the folded measurement root.
|
||||
func buildTDXExtra(body *tdxpb.TDQuoteBody) map[string]string {
|
||||
extra := make(map[string]string, 12)
|
||||
extra["tdx.mrtd"] = hex.EncodeToString(body.GetMrTd())
|
||||
for i, r := range body.GetRtmrs() {
|
||||
extra[fmt.Sprintf("tdx.rtmr%d", i)] = hex.EncodeToString(r)
|
||||
}
|
||||
extra["tdx.mrseam"] = hex.EncodeToString(body.GetMrSeam())
|
||||
extra["tdx.mrsignerseam"] = hex.EncodeToString(body.GetMrSignerSeam())
|
||||
extra["tdx.seam_attributes"] = hex.EncodeToString(body.GetSeamAttributes())
|
||||
extra["tdx.td_attributes"] = hex.EncodeToString(body.GetTdAttributes())
|
||||
extra["tdx.xfam"] = hex.EncodeToString(body.GetXfam())
|
||||
extra["tdx.mrconfigid"] = hex.EncodeToString(body.GetMrConfigId())
|
||||
extra["tdx.mrowner"] = hex.EncodeToString(body.GetMrOwner())
|
||||
extra["tdx.mrownerconfig"] = hex.EncodeToString(body.GetMrOwnerConfig())
|
||||
extra["tdx.tee_tcb_svn"] = hex.EncodeToString(body.GetTeeTcbSvn())
|
||||
return extra
|
||||
}
|
||||
|
||||
// canonicalTDXBytes returns the fixed-shape bytes that participate in
|
||||
// CompositeHash. We hash the verifier-extracted TD report fields (never the
|
||||
// raw wire) in a fixed order and width so a consumer re-deriving the hash
|
||||
// reproduces it iff the same quote was verified. Field widths follow the
|
||||
// Intel TDX quote spec; fixedSize zero-pads/truncates to keep the blob shape
|
||||
// constant.
|
||||
func canonicalTDXBytes(body *tdxpb.TDQuoteBody) []byte {
|
||||
// 16(teeTcbSvn) + 48(mrSeam) + 48(mrSignerSeam) + 8(seamAttrs)
|
||||
// + 8(tdAttrs) + 8(xfam) + 48(mrTd) + 48(mrConfigId) + 48(mrOwner)
|
||||
// + 48(mrOwnerConfig) + 4*48(rtmrs) + 64(reportData) = 632 bytes.
|
||||
buf := make([]byte, 0, 632)
|
||||
buf = append(buf, fixedSize(body.GetTeeTcbSvn(), 16)...)
|
||||
buf = append(buf, fixedSize(body.GetMrSeam(), 48)...)
|
||||
buf = append(buf, fixedSize(body.GetMrSignerSeam(), 48)...)
|
||||
buf = append(buf, fixedSize(body.GetSeamAttributes(), 8)...)
|
||||
buf = append(buf, fixedSize(body.GetTdAttributes(), 8)...)
|
||||
buf = append(buf, fixedSize(body.GetXfam(), 8)...)
|
||||
buf = append(buf, fixedSize(body.GetMrTd(), 48)...)
|
||||
buf = append(buf, fixedSize(body.GetMrConfigId(), 48)...)
|
||||
buf = append(buf, fixedSize(body.GetMrOwner(), 48)...)
|
||||
buf = append(buf, fixedSize(body.GetMrOwnerConfig(), 48)...)
|
||||
for _, r := range body.GetRtmrs() {
|
||||
buf = append(buf, fixedSize(r, 48)...)
|
||||
}
|
||||
buf = append(buf, fixedSize(body.GetReportData(), 64)...)
|
||||
return buf
|
||||
}
|
||||
|
||||
// Compile-time guard: TDX satisfies Verifier.
|
||||
var _ Verifier = TDX{}
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
-----BEGIN%20CERTIFICATE-----%0AMIICizCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw%0AaDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv%0AcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ%0ABgNVBAYTAlVTMB4XDTE4MDUyMTEwNTAxMFoXDTI1MDUyMTEwNTAxMFowbDEeMBwG%0AA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw%0Ab3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD%0AVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv%0AP%2BmAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh%2FzN3C4xvpoouGlirMba%2BW2lju%0AypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f%0ABEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz%0ALmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK%0AQEmORYQD6RSRvfRVMA4GA1UdDwEB%2FwQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG%0ASM49BAMCA0cAMEQCIB9C8wOAN%2FImxDtGACV246KcqjagZOR0kyctyBrsGGJVAiAj%0AftbrNGsGU8YH211dRiYNoPPu19Zp%2Fze8JmhujB0oBw%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw%0AaDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv%0AcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ%0ABgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG%0AA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0%0AaW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT%0AAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj%2FiPWsCzaEKi7%0A1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB%0AuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ%0AMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50%0AZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV%0AUr9QGzknBqwwDgYDVR0PAQH%2FBAQDAgEGMBIGA1UdEwEB%2FwQIMAYBAf8CAQEwCgYI%0AKoZIzj0EAwIDSQAwRgIhAOW%2F5QkR%2BS9CiSDcNoowLuPRLsWGf%2FYi7GSX94BgwTwg%0AAiEA4J0lrHoMs%2BXo5o%2FsX6O9QWxHRAvZUGOdRQ7cvqRXaqI%3D%0A-----END%20CERTIFICATE-----%0A
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
-----BEGIN%20CERTIFICATE-----%0AMIICljCCAj2gAwIBAgIVAJVvXc29G%2BHpQEnJ1PQzzgFXC95UMAoGCCqGSM49BAMC%0AMGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBD%0Ab3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQsw%0ACQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHAxIjAg%0ABgNVBAMMGUludGVsIFNHWCBQQ0sgUGxhdGZvcm0gQ0ExGjAYBgNVBAoMEUludGVs%0AIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0Ex%0ACzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENSB%2F7t21lXSO%0A2Cuzpxw74eJB72EyDGgW5rXCtx2tVTLq6hKk6z%2BUiRZCnqR7psOvgqFeSxlmTlJl%0AeTmi2WYz3qOBuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBS%0ABgNVHR8ESzBJMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2Vy%0AdmljZXMuaW50ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUlW9d%0Azb0b4elAScnU9DPOAVcL3lQwDgYDVR0PAQH%2FBAQDAgEGMBIGA1UdEwEB%2FwQIMAYB%0AAf8CAQAwCgYIKoZIzj0EAwIDRwAwRAIgXsVki0w%2Bi6VYGW3UF%2F22uaXe0YJDj1Ue%0AnA%2BTjD1ai5cCICYb1SAmD5xkfTVpvo4UoyiSYxrDWLmUR4CI9NKyfPN%2B%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw%0AaDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv%0AcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ%0ABgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG%0AA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0%0AaW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT%0AAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj%2FiPWsCzaEKi7%0A1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB%0AuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ%0AMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50%0AZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV%0AUr9QGzknBqwwDgYDVR0PAQH%2FBAQDAgEGMBIGA1UdEwEB%2FwQIMAYBAf8CAQEwCgYI%0AKoZIzj0EAwIDSQAwRgIhAOW%2F5QkR%2BS9CiSDcNoowLuPRLsWGf%2FYi7GSX94BgwTwg%0AAiEA4J0lrHoMs%2BXo5o%2FsX6O9QWxHRAvZUGOdRQ7cvqRXaqI%3D%0A-----END%20CERTIFICATE-----%0A
|
||||
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"enclaveIdentity":{"id":"TD_QE","version":2,"issueDate":"2023-06-08T07:24:59Z","nextUpdate":"2023-07-08T07:24:59Z","tcbEvaluationDataNumber":15,"miscselect":"00000000","miscselectMask":"FFFFFFFF","attributes":"11000000000000000000000000000000","attributesMask":"FBFFFFFFFFFFFFFF0000000000000000","mrsigner":"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5","isvprodid":2,"tcbLevels":[{"tcb":{"isvsvn":4},"tcbDate":"2023-02-15T00:00:00Z","tcbStatus":"UpToDate"}]},"signature":"b6a601f05de27f2ca5105eec24bdd4bf7dd1b8bbfffc76dffe4f4d16b8a395843e4b92d430fd6744b0648bf44302c528412fcb9cbf3cc9ce6922a3057932b6a6"}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"tcbInfo":{"id":"TDX","version":3,"issueDate":"2023-06-18T08:42:58Z","nextUpdate":"2023-07-18T08:42:58Z","fmspc":"50806f000000","pceId":"0000","tcbType":0,"tcbEvaluationDataNumber":15,"tdxModule":{"mrsigner":"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","attributes":"0000000000000000","attributesMask":"FFFFFFFFFFFFFFFF"},"tcbLevels":[{"tcb":{"sgxtcbcomponents":[{"svn":5,"category":"BIOS","type":"Early Microcode Update"},{"svn":5,"category":"OS/VMM","type":"SGX Late Microcode Update"},{"svn":2,"category":"OS/VMM","type":"TXT SINIT"},{"svn":2,"category":"BIOS"},{"svn":3,"category":"BIOS"},{"svn":1,"category":"BIOS"},{"svn":0},{"svn":3,"category":"OS/VMM","type":"SEAMLDR ACM"},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":11,"tdxtcbcomponents":[{"svn":3,"category":"OS/VMM","type":"TDX Module"},{"svn":0,"category":"OS/VMM","type":"TDX Module"},{"svn":5,"category":"OS/VMM","type":"TDX Late Microcode Update"},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}]},"tcbDate":"2023-02-15T00:00:00Z","tcbStatus":"UpToDate"},{"tcb":{"sgxtcbcomponents":[{"svn":5,"category":"BIOS","type":"Early Microcode Update"},{"svn":5,"category":"OS/VMM","type":"SGX Late Microcode Update"},{"svn":2,"category":"OS/VMM","type":"TXT SINIT"},{"svn":2,"category":"BIOS"},{"svn":3,"category":"BIOS"},{"svn":1,"category":"BIOS"},{"svn":0},{"svn":3,"category":"OS/VMM","type":"SEAMLDR ACM"},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":5,"tdxtcbcomponents":[{"svn":3,"category":"OS/VMM","type":"TDX Module"},{"svn":0,"category":"OS/VMM","type":"TDX Module"},{"svn":5,"category":"OS/VMM","type":"TDX Late Microcode Update"},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}]},"tcbDate":"2018-01-04T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00106","INTEL-SA-00115","INTEL-SA-00135","INTEL-SA-00203","INTEL-SA-00220","INTEL-SA-00233","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477"]}]},"signature":"f6502d6fad1e3b7281df2b7eddc773d5b5281187346c12c5647b4f243cea49212be96a7a1a6b5d83e36323fe3fa9dacd61ebfbc38e631ff0fe29ef14ae0db0b4"}
|
||||
+8
-54
@@ -233,57 +233,11 @@ func TestDispatch_RejectsUnknownKind(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestStubsReturnNotImplemented confirms the TDX stub FAILS LOUD —
|
||||
// returning ErrNotImplemented (a hard refusal callers must not paper
|
||||
// over) rather than silently returning a nil error. This guards against
|
||||
// an accidental future change that silently accepts evidence the
|
||||
// verifier hasn't actually checked, while keeping the failure
|
||||
// recoverable so request-handling goroutines do not crash the whole
|
||||
// process. The NVIDIA path is no longer a stub — see nvtrust_test.go.
|
||||
// When stage 2 of #222 lands, replace this test with a real TDX case.
|
||||
func TestStubsReturnNotImplemented(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
v Verifier
|
||||
}{
|
||||
{"tdx", TDX{}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("%s.Verify panicked (must error-return instead): %v", c.name, r)
|
||||
}
|
||||
}()
|
||||
rep, err := c.v.Verify(context.Background(), []byte{0x00})
|
||||
if rep != nil {
|
||||
t.Fatalf("%s.Verify returned non-nil report: %+v", c.name, rep)
|
||||
}
|
||||
if !errors.Is(err, ErrNotImplemented) {
|
||||
t.Fatalf("%s.Verify returned %v, want ErrNotImplemented", c.name, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDispatch_StubKindsReturnNotImplemented covers the Dispatch path
|
||||
// for the stub kinds: the entry point must surface ErrNotImplemented
|
||||
// instead of crashing.
|
||||
func TestDispatch_StubKindsReturnNotImplemented(t *testing.T) {
|
||||
for _, kind := range []Kind{KindTDX} {
|
||||
t.Run(string(kind), func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("Dispatch(%s) panicked: %v", kind, r)
|
||||
}
|
||||
}()
|
||||
rep, err := Dispatch(context.Background(), kind, []byte{0x00})
|
||||
if rep != nil {
|
||||
t.Fatalf("Dispatch(%s) returned non-nil report: %+v", kind, rep)
|
||||
}
|
||||
if !errors.Is(err, ErrNotImplemented) {
|
||||
t.Fatalf("Dispatch(%s) returned %v, want ErrNotImplemented", kind, 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),
|
||||
// and NVTrust (nvtrust_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:
|
||||
// any future stub Kind should ship its own fail-loud test alongside the
|
||||
// verifier, mirroring the SEV/TDX/NVTrust per-kind test layout.
|
||||
|
||||
@@ -2,7 +2,10 @@ module github.com/luxfi/cc
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require github.com/google/go-sev-guest v0.14.1
|
||||
require (
|
||||
github.com/google/go-sev-guest v0.14.1
|
||||
github.com/google/go-tdx-guest v0.3.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/logger v1.1.1 // indirect
|
||||
|
||||
@@ -6,6 +6,8 @@ github.com/google/go-configfs-tsm v0.2.2 h1:YnJ9rXIOj5BYD7/0DNnzs8AOp7UcvjfTvt21
|
||||
github.com/google/go-configfs-tsm v0.2.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo=
|
||||
github.com/google/go-sev-guest v0.14.1 h1:j/DXy9jk1qSW/dEV9vDiQnhAVFD1zqnWNVu6p1J0Jgo=
|
||||
github.com/google/go-sev-guest v0.14.1/go.mod h1:SK9vW+uyfuzYdVN0m8BShL3OQCtXZe/JPF7ZkpD3760=
|
||||
github.com/google/go-tdx-guest v0.3.1 h1:gl0KvjdsD4RrJzyLefDOvFOUH3NAJri/3qvaL5m83Iw=
|
||||
github.com/google/go-tdx-guest v0.3.1/go.mod h1:/rc3d7rnPykOPuY8U9saMyEps0PZDThLk/RygXm04nE=
|
||||
github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ=
|
||||
github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
|
||||
Reference in New Issue
Block a user