Skip to content

Commit 57fa35a

Browse files
mswilkisonclaude
andcommitted
fix(frost): enforce no-coarse-fallback at the single coarse-invocation point
Codex review (PR #4101 P2): the interactive-only check sat in attemptRoastRetryOrchestrationFromRequest, AFTER the drive - so the earlier static-fallback returns (readiness gate off, no coordinator registered, unsupported signer material) returned (nil, nil, nil) before the check ran, and the adapter then proceeded to the coarse primitive. KEEP_CORE_FROST_INTERACTIVE_SIGNING_ONLY was bypassed on exactly the misconfigured/cutover paths it was meant to protect. Move the enforcement to native_ffi_executor_adapter, the SINGLE point where the coarse primitive is invoked: when the orchestration yields no interactive signature for ANY reason (audit gate off, no engine, or any static fallback) and interactive-only mode is on, the adapter fails CLOSED before nefea.primitive.Sign instead of falling through. Revert the partial executor-entry check. Test moves to the adapter level: TestNativeExecutionFFIExecutorAdapter_Execute_ InteractiveOnlyRefusesCoarse runs in the DEFAULT build, where the orchestration helper is a no-op (nil,nil,nil) - the ultimate static fallback - and asserts the adapter returns a refusal AND the coarse primitive's signCalls == 0 (never invoked). Plus the flag-parsing test. Builds across all tag combos; existing adapter + executor-entry tests unchanged; gofmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a9aed42 commit 57fa35a

4 files changed

Lines changed: 83 additions & 61 deletions

pkg/frost/signing/native_ffi_executor_adapter.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,20 @@ func (nefea *nativeExecutionFFIExecutorAdapter) Execute(
144144
}, nil
145145
}
146146

147+
if InteractiveSigningOnlyEnabled() {
148+
// Interactive-only mode (coarse-path retirement): the orchestration produced
149+
// no interactive signature - the audit gate is off, no engine is registered,
150+
// OR any static fallback fired (readiness off, no coordinator, unsupported
151+
// material). This is the SINGLE point where the coarse primitive is invoked,
152+
// so refusing here fails CLOSED on every fall-through path rather than
153+
// silently signing over the retired coarse path.
154+
return nil, fmt.Errorf(
155+
"interactive-only signing mode (%s) is set but interactive signing did not run "+
156+
"(%s off, no engine, or static fallback); refusing the coarse fallback",
157+
InteractiveSigningOnlyEnvVar, InteractiveSigningOptInEnvVar,
158+
)
159+
}
160+
147161
signature, err := nefea.primitive.Sign(ctx, logger, ffiRequest)
148162
if err != nil {
149163
return nil, err

pkg/frost/signing/native_ffi_executor_adapter_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,75 @@ func TestNativeExecutionFFIExecutorAdapter_Execute_RejectsNilSignature(
257257
}
258258
}
259259

260+
func TestNativeExecutionFFIExecutorAdapter_Execute_InteractiveOnlyRefusesCoarse(
261+
t *testing.T,
262+
) {
263+
// Coarse-path retirement: with interactive-only mode ON, the adapter must NOT fall
264+
// through to the coarse primitive on ANY no-interactive-signature path. In the
265+
// default build attemptRoastRetryOrchestrationFromRequest is a no-op (nil,nil,nil)
266+
// - the ultimate static fallback - so this exercises exactly the gap Codex flagged:
267+
// a fall-through that an executor-level check (which runs only after orchestration
268+
// activates) would have bypassed. The adapter is the single coarse-invocation
269+
// point, so the refusal here covers every path.
270+
t.Setenv(InteractiveSigningOnlyEnvVar, "true")
271+
272+
primitive := &mockNativeExecutionFFISigningPrimitive{
273+
signature: &frost.Signature{
274+
R: [frost.SignatureComponentSize]byte{0x01},
275+
S: [frost.SignatureComponentSize]byte{0x02},
276+
},
277+
}
278+
279+
executor, err := NewNativeExecutionFFIExecutorAdapter(primitive)
280+
if err != nil {
281+
t.Fatalf("unexpected adapter setup error: [%v]", err)
282+
}
283+
284+
_, err = executor.Execute(context.Background(), nil, &Request{
285+
Message: big.NewInt(123),
286+
SessionID: "session-interactive-only",
287+
MemberIndex: 2,
288+
GroupSize: 5,
289+
DishonestThreshold: 1,
290+
SignerMaterial: &NativeSignerMaterial{
291+
Format: NativeSignerMaterialFormatFrostUniFFIV1,
292+
Payload: []byte{0xaa},
293+
},
294+
Attempt: &Attempt{
295+
Number: 3,
296+
CoordinatorMemberIndex: 1,
297+
IncludedMembersIndexes: []group.MemberIndex{1, 2, 3},
298+
},
299+
})
300+
if err == nil {
301+
t.Fatal("interactive-only mode must fail closed instead of using the coarse primitive")
302+
}
303+
if !strings.Contains(err.Error(), InteractiveSigningOnlyEnvVar) {
304+
t.Fatalf("unexpected error (want a refusal naming %s): %v", InteractiveSigningOnlyEnvVar, err)
305+
}
306+
if primitive.signCalls != 0 {
307+
t.Fatalf(
308+
"coarse primitive must NOT be called in interactive-only mode, got %d call(s)",
309+
primitive.signCalls,
310+
)
311+
}
312+
}
313+
314+
func TestInteractiveSigningOnlyEnabled_ParsesFlag(t *testing.T) {
315+
t.Setenv(InteractiveSigningOnlyEnvVar, "")
316+
if InteractiveSigningOnlyEnabled() {
317+
t.Fatal("unset must be off")
318+
}
319+
t.Setenv(InteractiveSigningOnlyEnvVar, " TrUe ")
320+
if !InteractiveSigningOnlyEnabled() {
321+
t.Fatal("case-insensitive, trimmed true must be on")
322+
}
323+
t.Setenv(InteractiveSigningOnlyEnvVar, "false")
324+
if InteractiveSigningOnlyEnabled() {
325+
t.Fatal("false must be off")
326+
}
327+
}
328+
260329
func TestNativeExecutionFFIExecutorAdapter_RegisterUnmarshallers_Delegates(
261330
t *testing.T,
262331
) {

pkg/frost/signing/roast_retry_executor_entry_frost_native.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,5 @@ func attemptRoastRetryOrchestrationFromRequest(
131131
if err != nil {
132132
return nil, cleanup, err
133133
}
134-
if signature == nil && InteractiveSigningOnlyEnabled() {
135-
// Interactive-only mode (coarse-path retirement): interactive signing did
136-
// not produce a signature (its audit gate is off, or no engine is
137-
// registered), and the coarse fallback is disabled - fail CLOSED rather than
138-
// silently signing via the retired coarse path.
139-
return nil, cleanup, fmt.Errorf(
140-
"interactive-only signing mode (%s) is set but interactive signing did not run "+
141-
"(%s off or no engine registered); refusing the coarse fallback",
142-
InteractiveSigningOnlyEnvVar, InteractiveSigningOptInEnvVar,
143-
)
144-
}
145134
return signature, cleanup, nil
146135
}

pkg/frost/signing/roast_retry_executor_entry_frost_roast_retry_test.go

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"encoding/json"
88
"errors"
99
"math/big"
10-
"strings"
1110
"testing"
1211

1312
"github.com/ipfs/go-log/v2"
@@ -37,55 +36,6 @@ func newEntryRetryTestRequest(t *testing.T) *NativeExecutionFFISigningRequest {
3736
}
3837
}
3938

40-
func TestEntry_InteractiveOnly_RefusesCoarseFallback(t *testing.T) {
41-
// Coarse-path retirement: with interactive-only mode ON but interactive signing
42-
// not running (its audit gate off), the executor must REFUSE the coarse fallback
43-
// and fail closed, rather than returning a nil signature for the caller to sign
44-
// over the retired coarse path.
45-
t.Setenv(RoastRetryReadinessOptInEnvVar, "true")
46-
t.Setenv(InteractiveSigningOptInEnvVar, "") // audit gate OFF -> the drive returns nil
47-
t.Setenv(InteractiveSigningOnlyEnvVar, "true")
48-
ResetRoastRetryRegistrationForTest()
49-
ResetSessionHandleRegistryForTest()
50-
t.Cleanup(ResetRoastRetryRegistrationForTest)
51-
t.Cleanup(ResetSessionHandleRegistryForTest)
52-
53-
RegisterRoastRetryCoordinator(RoastRetryDeps{
54-
Coordinator: roast.NewInMemoryCoordinator(),
55-
Signer: roast.NoOpSigner(),
56-
Verifier: roast.NoOpSignatureVerifier(),
57-
SelfMember: 1,
58-
})
59-
60-
signature, _, err := attemptRoastRetryOrchestrationFromRequest(
61-
context.Background(), newEntryRetryTestRequest(t), log.Logger("entry-interactive-only"),
62-
)
63-
if signature != nil {
64-
t.Fatal("interactive-only refusal must not return a signature")
65-
}
66-
if err == nil {
67-
t.Fatal("interactive-only mode must refuse the coarse fallback when interactive signing did not run")
68-
}
69-
if !strings.Contains(err.Error(), InteractiveSigningOnlyEnvVar) {
70-
t.Fatalf("unexpected error (want a refusal naming %s): %v", InteractiveSigningOnlyEnvVar, err)
71-
}
72-
}
73-
74-
func TestEntry_InteractiveSigningOnlyEnabled_ParsesFlag(t *testing.T) {
75-
t.Setenv(InteractiveSigningOnlyEnvVar, "")
76-
if InteractiveSigningOnlyEnabled() {
77-
t.Fatal("unset must be off")
78-
}
79-
t.Setenv(InteractiveSigningOnlyEnvVar, " TrUe ")
80-
if !InteractiveSigningOnlyEnabled() {
81-
t.Fatal("case-insensitive, trimmed true must be on")
82-
}
83-
t.Setenv(InteractiveSigningOnlyEnvVar, "false")
84-
if InteractiveSigningOnlyEnabled() {
85-
t.Fatal("false must be off")
86-
}
87-
}
88-
8939
func TestEntry_StaticFallback_ReadinessOptInUnset(t *testing.T) {
9040
// Explicitly unset the env var.
9141
t.Setenv(RoastRetryReadinessOptInEnvVar, "")

0 commit comments

Comments
 (0)