fix(cc/attest): TDX/NRAS stubs error-return instead of panic

TDX.Verify and NRAS.Verify panicked when reached during stage 1 of
issue #222, taking the whole MPC process down whenever Dispatch was
called from a request-handling goroutine. The fail-loud guarantee is
preserved by returning ErrNotImplemented (a hard refusal — callers
must not paper over it) so the release gate still refuses, but the
process survives. Updates verifier_test.go's stub coverage from
TestStubsPanic to TestStubsReturnNotImplemented and adds Dispatch-path
coverage for the same behaviour.
This commit is contained in:
2026-04-29 00:51:02 -07:00
parent a168349853
commit abacfaadb6
3 changed files with 54 additions and 21 deletions
+4 -4
View File
@@ -34,15 +34,15 @@ import (
// evidence, opts...) entry point with an internal JWKS cache and the
// canonical VerifiedReport output shape.
//
// Verify panics so any caller routing NRAS evidence to it during stage 1
// fails LOUD. See TDX.Verify for the rationale.
// Verify returns ErrNotImplemented so the call site refuses the release
// without crashing the process. See TDX.Verify for the rationale.
type NRAS struct{}
// Verify implements Verifier. NOT YET IMPLEMENTED — do not enable NRAS
// dispatch in production until #222 stage 3 lands.
func (NRAS) Verify(ctx context.Context, evidence []byte, opts ...Option) (*VerifiedReport, error) {
panic(fmt.Sprintf("%v: NRAS verifier; tracked at #222 stage 3 (use NRAS JWT + JWKS cache)",
ErrNotImplemented))
return nil, fmt.Errorf("%w: NRAS attestation envelope verification not configured for this build (tracked at #222 stage 3: NRAS JWT + JWKS cache)",
ErrNotImplemented)
}
var _ Verifier = NRAS{}
+10 -8
View File
@@ -26,19 +26,21 @@ import (
// - Compose CompositeHash via the same domain-separated SHA-256 as
// the SEV path (see verifier.go::computeCompositeHash).
//
// Verify panics so any caller routing TDX evidence to it during stage 1
// fails LOUD. A silent no-op or error-return would let the release gate
// accidentally accept evidence the verifier has not actually checked;
// panic + recover at the Dispatch site is cheaper to diagnose and
// impossible to ignore in CI. Callers that want to soft-skip TDX should
// gate on a feature flag at their layer rather than here.
// 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.
type TDX struct{}
// Verify implements Verifier. NOT YET IMPLEMENTED — do not enable TDX
// dispatch in production until #222 stage 2 lands.
func (TDX) Verify(ctx context.Context, evidence []byte, opts ...Option) (*VerifiedReport, error) {
panic(fmt.Sprintf("%v: TDX verifier; tracked at #222 stage 2 (use go-tdx-guest + Intel PCS)",
ErrNotImplemented))
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)
}
var _ Verifier = TDX{}
+40 -9
View File
@@ -233,12 +233,15 @@ func TestDispatch_RejectsUnknownKind(t *testing.T) {
}
}
// TestStubsPanic confirms the TDX/NRAS stubs FAIL LOUD rather than silently
// returning nil. This is the only test the stubs need: it guards against an
// accidental future change that silently accepts evidence the verifier
// hasn't actually checked. When stages 2 + 3 of #222 land, replace this
// test with real verify cases.
func TestStubsPanic(t *testing.T) {
// TestStubsReturnNotImplemented confirms the TDX/NRAS stubs FAIL 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. When stages 2 + 3 of #222 land, replace this test with real
// verify cases.
func TestStubsReturnNotImplemented(t *testing.T) {
cases := []struct {
name string
v Verifier
@@ -249,11 +252,39 @@ func TestStubsPanic(t *testing.T) {
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)
if r := recover(); r != nil {
t.Fatalf("%s.Verify panicked (must error-return instead): %v", c.name, r)
}
}()
_, _ = c.v.Verify(context.Background(), []byte{0x00})
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, KindNRAS} {
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)
}
})
}
}