Skip to content

Commit a9aed42

Browse files
mswilkisonclaude
andcommitted
feat(frost): no-coarse-fallback mode for coarse-path retirement (default off)
The reversible, un-gated half of coarse-path retirement (RFC-21 Phase 7.3). Adds a default-OFF KEEP_CORE_FROST_INTERACTIVE_SIGNING_ONLY gate; when set, the executor REFUSES to fall through to the coarse signing primitive: if interactive signing did not run (its audit gate off, or no engine), the attempt fails CLOSED rather than silently signing over the retired coarse path. The hard-fail on a committed interactive failure is unchanged; this only converts the (nil signature, nil error) "interactive not enabled -> coarse" fall-through into a refusal. Default off, so production is unchanged: coarse stays the path until an operator flips this on. Flipping it on IS the tECDSA->FROST cutover for that node (the coarse fallback is gone), so it stays off until the frost-secp256k1-tr external audit clears and the recovery-leaf decision lands - the actual code deletion of the transitional coarse primitive is the irreversible follow-up, deliberately deferred. Tests: TestEntry_InteractiveOnly_RefusesCoarseFallback (orchestration active + interactive audit gate off + this flag on -> the executor returns a refusal naming the env var, no signature) and TestEntry_InteractiveSigningOnlyEnabled_ParsesFlag. Existing static-fallback executor tests unchanged (the flag defaults off). Builds clean across the tag combos; cgo vet + gofmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f4953b8 commit a9aed42

3 files changed

Lines changed: 81 additions & 0 deletions

File tree

pkg/frost/signing/roast_interactive_signing_gate.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,23 @@ func InteractiveSigningOptInEnabled() bool {
3131
value := strings.TrimSpace(os.Getenv(InteractiveSigningOptInEnvVar))
3232
return strings.EqualFold(value, "true")
3333
}
34+
35+
// InteractiveSigningOnlyEnvVar is the no-coarse-fallback half of coarse-path
36+
// retirement (RFC-21 Phase 7.3). When set to "true", the executor REFUSES to fall
37+
// through to the coarse signing primitive: interactive signing is mandatory, and a
38+
// session where it does not run fails CLOSED rather than silently signing via the
39+
// retired coarse path. It is meant to be set ONLY together with the audit gate above
40+
// (and a registered engine) - setting it on its own makes signing fail closed.
41+
//
42+
// It stays OFF by default and is intended to remain off in production until the
43+
// frost-secp256k1-tr engine external audit clears and the tECDSA->FROST cutover is
44+
// made: flipping it on IS that cutover for this node (the coarse path is no longer
45+
// available as a fallback). Read per call, not cached, matching the audit gate.
46+
const InteractiveSigningOnlyEnvVar = "KEEP_CORE_FROST_INTERACTIVE_SIGNING_ONLY"
47+
48+
// InteractiveSigningOnlyEnabled reports whether interactive-only (no coarse
49+
// fallback) mode is currently set to "true" (case-insensitive, whitespace-trimmed).
50+
func InteractiveSigningOnlyEnabled() bool {
51+
value := strings.TrimSpace(os.Getenv(InteractiveSigningOnlyEnvVar))
52+
return strings.EqualFold(value, "true")
53+
}

pkg/frost/signing/roast_retry_executor_entry_frost_native.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,16 @@ 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+
}
134145
return signature, cleanup, nil
135146
}

pkg/frost/signing/roast_retry_executor_entry_frost_roast_retry_test.go

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

1213
"github.com/ipfs/go-log/v2"
@@ -36,6 +37,55 @@ func newEntryRetryTestRequest(t *testing.T) *NativeExecutionFFISigningRequest {
3637
}
3738
}
3839

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+
3989
func TestEntry_StaticFallback_ReadinessOptInUnset(t *testing.T) {
4090
// Explicitly unset the env var.
4191
t.Setenv(RoastRetryReadinessOptInEnvVar, "")

0 commit comments

Comments
 (0)