From cecf08f4efade9f3e4cad22cfc0b96b49c350488 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 13 Mar 2026 22:17:33 -0500 Subject: [PATCH] Require covenant approval trust roots in production mode --- cmd/flags.go | 6 ++ cmd/flags_test.go | 7 ++ config/config_test.go | 4 + pkg/covenantsigner/config.go | 5 + pkg/covenantsigner/covenantsigner_test.go | 124 ++++++++++++++++++++++ pkg/covenantsigner/server.go | 42 ++++++++ test/config.json | 3 +- test/config.toml | 1 + test/config.yaml | 1 + 9 files changed, 192 insertions(+), 1 deletion(-) diff --git a/cmd/flags.go b/cmd/flags.go index 9899b814d1..e2e82e3d80 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -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. diff --git a/cmd/flags_test.go b/cmd/flags_test.go index 559640bac5..29ccddd53a 100644 --- a/cmd/flags_test.go +++ b/cmd/flags_test.go @@ -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", diff --git a/config/config_test.go b/config/config_test.go index 8f63b7ea99..a29da7d9fd 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -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, diff --git a/pkg/covenantsigner/config.go b/pkg/covenantsigner/config.go index e14b855eb0..d9e100261f 100644 --- a/pkg/covenantsigner/config.go +++ b/pkg/covenantsigner/config.go @@ -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. diff --git a/pkg/covenantsigner/covenantsigner_test.go b/pkg/covenantsigner/covenantsigner_test.go index b6bd61cf5e..85eb497cba 100644 --- a/pkg/covenantsigner/covenantsigner_test.go +++ b/pkg/covenantsigner/covenantsigner_test.go @@ -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") diff --git a/pkg/covenantsigner/server.go b/pkg/covenantsigner/server.go index 6667e9a76a..8283c29ee5 100644 --- a/pkg/covenantsigner/server.go +++ b/pkg/covenantsigner/server.go @@ -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; " + @@ -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, diff --git a/test/config.json b/test/config.json index 96b5771908..8e3662cd9a 100644 --- a/test/config.json +++ b/test/config.json @@ -40,7 +40,8 @@ "EthereumMetricsTick": "1m27s" }, "CovenantSigner": { - "Port": 9702 + "Port": 9702, + "RequireApprovalTrustRoots": true }, "Maintainer": { "BitcoinDifficulty": { diff --git a/test/config.toml b/test/config.toml index 220c2dd6fa..44836f6e9a 100644 --- a/test/config.toml +++ b/test/config.toml @@ -37,6 +37,7 @@ EthereumMetricsTick = "1m27s" [covenantsigner] Port = 9702 +RequireApprovalTrustRoots = true [maintainer.BitcoinDifficulty] Enabled = true diff --git a/test/config.yaml b/test/config.yaml index 29b78b814d..dce648d86c 100644 --- a/test/config.yaml +++ b/test/config.yaml @@ -32,6 +32,7 @@ ClientInfo: EthereumMetricsTick: "1m27s" CovenantSigner: Port: 9702 + RequireApprovalTrustRoots: true Maintainer: BitcoinDifficulty: Enabled: true