Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,12 @@ func initCovenantSignerFlags(cmd *cobra.Command, cfg *config.Config) {
false,
"Expose self_v1 covenant signer HTTP routes. Keep disabled for a qc_v1-first launch unless self_v1 is explicitly approved.",
)
cmd.Flags().BoolVar(
&cfg.CovenantSigner.RequireApprovalTrustRoots,
"covenantSigner.requireApprovalTrustRoots",
false,
"Fail startup when enabled covenant routes are missing route-level approval trust roots. Request-time validation still enforces exact reserve/network trust-root matches.",
)
}

// Initialize flags for Maintainer configuration.
Expand Down
7 changes: 7 additions & 0 deletions cmd/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ var cmdFlagsTests = map[string]struct {
expectedValueFromFlag: true,
defaultValue: false,
},
"covenantSigner.requireApprovalTrustRoots": {
readValueFunc: func(c *config.Config) interface{} { return c.CovenantSigner.RequireApprovalTrustRoots },
flagName: "--covenantSigner.requireApprovalTrustRoots",
flagValue: "",
expectedValueFromFlag: true,
defaultValue: false,
},
"tbtc.preParamsPoolSize": {
readValueFunc: func(c *config.Config) interface{} { return c.Tbtc.PreParamsPoolSize },
flagName: "--tbtc.preParamsPoolSize",
Expand Down
4 changes: 4 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ func TestReadConfigFromFile(t *testing.T) {
readValueFunc: func(c *Config) interface{} { return c.CovenantSigner.Port },
expectedValue: 9702,
},
"CovenantSigner.RequireApprovalTrustRoots": {
readValueFunc: func(c *Config) interface{} { return c.CovenantSigner.RequireApprovalTrustRoots },
expectedValue: true,
},
"Maintainer.BitcoinDifficulty.Enabled": {
readValueFunc: func(c *Config) interface{} { return c.Maintainer.BitcoinDifficulty.Enabled },
expectedValue: true,
Expand Down
5 changes: 5 additions & 0 deletions pkg/covenantsigner/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ type Config struct {
// EnableSelfV1 exposes the self_v1 signer HTTP routes. Keep this disabled
// for a qc_v1-first launch unless self_v1 has cleared its own go-live gate.
EnableSelfV1 bool
// RequireApprovalTrustRoots turns missing route-level approval trust roots
// from startup warnings into startup errors. This does not prove every
// reserve/network launch scope is provisioned; request-time validation still
// enforces exact route/reserve/network matches for configured entries.
RequireApprovalTrustRoots bool `mapstructure:"requireApprovalTrustRoots"`
// MigrationPlanQuoteTrustRoots configures the destination-service plan-quote
// trust roots used to verify migration plan quotes when the quote authority
// path is enabled.
Expand Down
124 changes: 124 additions & 0 deletions pkg/covenantsigner/covenantsigner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3337,6 +3337,130 @@ func TestInitializeRejectsInvalidOrUnavailablePort(t *testing.T) {
}
}

func availableLoopbackPort(t *testing.T) int {
t.Helper()

listener, err := net.Listen("tcp", net.JoinHostPort(DefaultListenAddress, "0"))
if err != nil {
t.Fatal(err)
}
defer listener.Close()

return listener.Addr().(*net.TCPAddr).Port
}

func TestInitializeRequiresQcV1DepositorTrustRootsWhenConfigured(t *testing.T) {
handle := newMemoryHandle()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

_, enabled, err := Initialize(
ctx,
Config{
Port: availableLoopbackPort(t),
RequireApprovalTrustRoots: true,
},
handle,
&scriptedEngine{},
)
if err == nil || enabled {
t.Fatalf("expected missing qc_v1 depositor trust roots to fail, got enabled=%v err=%v", enabled, err)
}
if !strings.Contains(
err.Error(),
"covenant signer qc_v1 routes require depositorTrustRoots when covenantSigner.requireApprovalTrustRoots=true",
) {
t.Fatalf("unexpected error: %v", err)
}
}

func TestInitializeRequiresQcV1CustodianTrustRootsWhenConfigured(t *testing.T) {
handle := newMemoryHandle()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

_, enabled, err := Initialize(
ctx,
Config{
Port: availableLoopbackPort(t),
RequireApprovalTrustRoots: true,
DepositorTrustRoots: []DepositorTrustRoot{
testDepositorTrustRoot(TemplateQcV1),
},
},
handle,
&scriptedEngine{},
)
if err == nil || enabled {
t.Fatalf("expected missing qc_v1 custodian trust roots to fail, got enabled=%v err=%v", enabled, err)
}
if !strings.Contains(
err.Error(),
"covenant signer qc_v1 routes require custodianTrustRoots when covenantSigner.requireApprovalTrustRoots=true",
) {
t.Fatalf("unexpected error: %v", err)
}
}

func TestInitializeRequiresSelfV1DepositorTrustRootsWhenConfigured(t *testing.T) {
handle := newMemoryHandle()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

_, enabled, err := Initialize(
ctx,
Config{
Port: availableLoopbackPort(t),
EnableSelfV1: true,
RequireApprovalTrustRoots: true,
DepositorTrustRoots: []DepositorTrustRoot{
testDepositorTrustRoot(TemplateQcV1),
},
CustodianTrustRoots: []CustodianTrustRoot{
testCustodianTrustRoot(TemplateQcV1),
},
},
handle,
&scriptedEngine{},
)
if err == nil || enabled {
t.Fatalf("expected missing self_v1 depositor trust roots to fail, got enabled=%v err=%v", enabled, err)
}
if !strings.Contains(
err.Error(),
"covenant signer self_v1 routes require depositorTrustRoots when covenantSigner.requireApprovalTrustRoots=true",
) {
t.Fatalf("unexpected error: %v", err)
}
}

func TestInitializeAcceptsRequiredApprovalTrustRootsWhenConfigured(t *testing.T) {
handle := newMemoryHandle()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

server, enabled, err := Initialize(
ctx,
Config{
Port: availableLoopbackPort(t),
EnableSelfV1: true,
RequireApprovalTrustRoots: true,
DepositorTrustRoots: []DepositorTrustRoot{
testDepositorTrustRoot(TemplateQcV1),
testDepositorTrustRoot(TemplateSelfV1),
},
CustodianTrustRoots: []CustodianTrustRoot{
testCustodianTrustRoot(TemplateQcV1),
},
},
handle,
&scriptedEngine{},
)
if err != nil || !enabled || server == nil {
t.Fatalf("expected startup to succeed with required trust roots, got enabled=%v server=%v err=%v", enabled, server != nil, err)
}
}

func TestIsLoopbackListenAddressAcceptsBracketedIPv6Loopback(t *testing.T) {
if !isLoopbackListenAddress("[::1]") {
t.Fatal("expected bracketed IPv6 loopback address to be recognized")
Expand Down
42 changes: 42 additions & 0 deletions pkg/covenantsigner/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ func Initialize(
if err != nil {
return nil, false, err
}
if err := validateRequiredApprovalTrustRoots(config, service); err != nil {
return nil, false, err
}
if service.signerApprovalVerifier == nil {
logger.Warn(
"covenant signer started without a signer approval verifier; " +
Expand Down Expand Up @@ -135,6 +138,45 @@ func Initialize(
return server, true, nil
}

func validateRequiredApprovalTrustRoots(
config Config,
service *Service,
) error {
if !config.RequireApprovalTrustRoots {
return nil
}

if config.EnableSelfV1 &&
!hasDepositorTrustRootForRoute(
service.depositorTrustRoots,
TemplateSelfV1,
) {
return fmt.Errorf(
"covenant signer self_v1 routes require depositorTrustRoots when covenantSigner.requireApprovalTrustRoots=true",
)
}

if !hasDepositorTrustRootForRoute(
service.depositorTrustRoots,
TemplateQcV1,
) {
return fmt.Errorf(
"covenant signer qc_v1 routes require depositorTrustRoots when covenantSigner.requireApprovalTrustRoots=true",
)
}

if !hasCustodianTrustRootForRoute(
service.custodianTrustRoots,
TemplateQcV1,
) {
return fmt.Errorf(
"covenant signer qc_v1 routes require custodianTrustRoots when covenantSigner.requireApprovalTrustRoots=true",
)
}

return nil
}

func hasDepositorTrustRootForRoute(
trustRoots []DepositorTrustRoot,
route TemplateID,
Expand Down
3 changes: 2 additions & 1 deletion test/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"EthereumMetricsTick": "1m27s"
},
"CovenantSigner": {
"Port": 9702
"Port": 9702,
"RequireApprovalTrustRoots": true
},
"Maintainer": {
"BitcoinDifficulty": {
Expand Down
1 change: 1 addition & 0 deletions test/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ EthereumMetricsTick = "1m27s"

[covenantsigner]
Port = 9702
RequireApprovalTrustRoots = true

[maintainer.BitcoinDifficulty]
Enabled = true
Expand Down
1 change: 1 addition & 0 deletions test/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ ClientInfo:
EthereumMetricsTick: "1m27s"
CovenantSigner:
Port: 9702
RequireApprovalTrustRoots: true
Maintainer:
BitcoinDifficulty:
Enabled: true
Expand Down
Loading