From 6b7f506c1ba653c3023b173582ce85827f18ba5b Mon Sep 17 00:00:00 2001 From: Jakub Nowak Date: Wed, 21 Oct 2026 14:19:20 +0200 Subject: [PATCH 1/3] [DEVSVCS-6732] seth: verify TLS certificates of RPC endpoints by default NewLoggingTransport always set InsecureSkipVerify, so every RPC connection created via WithRpcUrl skipped certificate verification. Certificates are now verified by default; SETH_INSECURE_SKIP_VERIFY=true restores the old behaviour for testing environments with self-signed certificates. Co-Authored-By: Claude Opus 5.5 --- book/src/libs/seth.md | 4 +++ seth/.changeset/v1.51.8.md | 1 + seth/http_logging_transport.go | 30 +++++++++++++--------- seth/http_logging_transport_test.go | 39 +++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 seth/.changeset/v1.51.8.md create mode 100644 seth/http_logging_transport_test.go diff --git a/book/src/libs/seth.md b/book/src/libs/seth.md index 73c2e4bc2..7bcb4f01c 100644 --- a/book/src/libs/seth.md +++ b/book/src/libs/seth.md @@ -411,6 +411,7 @@ export SETH_LOG_LEVEL=info # global logger level export SETH_CONFIG_PATH=seth.toml # path to the toml config export SETH_NETWORK=Geth # selected network export SETH_ROOT_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 # root private key +export SETH_INSECURE_SKIP_VERIFY=false # set to true to skip TLS certificate verification for RPC connections (testing only!) alias seth="SETH_CONFIG_PATH=seth.toml go run cmd/seth/seth.go" # useful alias for CLI ``` @@ -974,6 +975,9 @@ You need to pass a file with a list of transaction hashes to trace. The file sho ### RPC Traffic logging With `SETH_LOG_LEVEL=trace` we will also log to console all traffic between Seth and RPC node. This can be useful for debugging as you can see all the requests and responses. +### TLS certificate verification +Seth verifies TLS certificates of HTTPS RPC endpoints. If you need to connect to an endpoint with a self-signed certificate in a testing environment, set `SETH_INSECURE_SKIP_VERIFY=true`. Never use it with production RPC endpoints, as it allows anyone intercepting the traffic to return fake chain state. + ### Read-only mode It's possible to use Seth in read-only mode only for transaction confirmation and tracing. Following operations will fail: diff --git a/seth/.changeset/v1.51.8.md b/seth/.changeset/v1.51.8.md new file mode 100644 index 000000000..c5427ade4 --- /dev/null +++ b/seth/.changeset/v1.51.8.md @@ -0,0 +1 @@ +- Verify TLS certificates of RPC endpoints by default (previously `InsecureSkipVerify` was always enabled). Set `SETH_INSECURE_SKIP_VERIFY=true` to restore the old behaviour in testing environments with self-signed certificates. diff --git a/seth/http_logging_transport.go b/seth/http_logging_transport.go index d0cbe6d84..99a1b3b23 100644 --- a/seth/http_logging_transport.go +++ b/seth/http_logging_transport.go @@ -10,6 +10,10 @@ import ( "time" ) +// InsecureSkipVerifyEnvVar disables TLS certificate verification for RPC connections when set to "true". +// Use only in testing environments with self-signed certificates. +const InsecureSkipVerifyEnvVar = "SETH_INSECURE_SKIP_VERIFY" + // LoggingTransport is a custom transport to log requests and responses type LoggingTransport struct { Transport http.RoundTripper @@ -47,21 +51,23 @@ func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) return resp, nil } -// NewLoggingTransport creates a new logging transport for GAP or default transport -// controlled by SETH_LOG_LEVEL +// NewLoggingTransport creates a new transport for RPC HTTP connections. TLS certificates are verified +// unless InsecureSkipVerifyEnvVar is set to "true". When SETH_LOG_LEVEL is "trace" requests and responses are logged. func NewLoggingTransport() http.RoundTripper { - if strings.EqualFold(os.Getenv(LogLevelEnvVar), "trace") { - return &LoggingTransport{ - // TODO: GAP, add proper certificates - Transport: &http.Transport{ - //nolint - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, + transport := http.DefaultTransport.(*http.Transport).Clone() + if strings.EqualFold(os.Getenv(InsecureSkipVerifyEnvVar), "true") { + L.Warn(). + Str("EnvVar", InsecureSkipVerifyEnvVar). + Msg("TLS certificate verification is disabled for RPC connections. Do not use this setting outside of testing environments") + transport.TLSClientConfig = &tls.Config{ + //nolint:gosec // explicit opt-in for testing environments with self-signed certificates + InsecureSkipVerify: true, } } - return &http.Transport{ - //nolint - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + if strings.EqualFold(os.Getenv(LogLevelEnvVar), "trace") { + return &LoggingTransport{Transport: transport} } + + return transport } diff --git a/seth/http_logging_transport_test.go b/seth/http_logging_transport_test.go new file mode 100644 index 000000000..787d66632 --- /dev/null +++ b/seth/http_logging_transport_test.go @@ -0,0 +1,39 @@ +package seth + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewLoggingTransport_TLS(t *testing.T) { + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(server.Close) + + for _, logLevel := range []string{"info", "trace"} { + t.Run("untrusted certificate is rejected with log level "+logLevel, func(t *testing.T) { + t.Setenv(LogLevelEnvVar, logLevel) + client := &http.Client{Transport: NewLoggingTransport()} + resp, err := client.Get(server.URL) + if resp != nil { + _ = resp.Body.Close() + } + require.Error(t, err, "expected TLS verification to fail for untrusted certificate") + require.Contains(t, err.Error(), "certificate") + }) + + t.Run("untrusted certificate is accepted when verification is disabled with log level "+logLevel, func(t *testing.T) { + t.Setenv(LogLevelEnvVar, logLevel) + t.Setenv(InsecureSkipVerifyEnvVar, "true") + client := &http.Client{Transport: NewLoggingTransport()} + resp, err := client.Get(server.URL) + require.NoError(t, err) + _ = resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + } +} From a9e26095000f57332c97236c751c37443cd77d9a Mon Sep 17 00:00:00 2001 From: skudasov Date: Wed, 21 Oct 2026 17:54:43 +0200 Subject: [PATCH 2/3] bump minor --- seth/.changeset/{v1.51.8.md => v1.52.0.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename seth/.changeset/{v1.51.8.md => v1.52.0.md} (100%) diff --git a/seth/.changeset/v1.51.8.md b/seth/.changeset/v1.52.0.md similarity index 100% rename from seth/.changeset/v1.51.8.md rename to seth/.changeset/v1.52.0.md From d694f2ee26f5d742a99440d66fbd99ff18f1ffc5 Mon Sep 17 00:00:00 2001 From: skudasov Date: Wed, 21 Oct 2026 18:14:18 +0200 Subject: [PATCH 3/3] TOML config + func opt --- book/src/libs/seth.md | 9 ++++- seth/.changeset/v1.52.0.md | 4 ++- seth/client.go | 2 +- seth/client_builder.go | 18 ++++++++++ seth/config.go | 2 ++ seth/http_logging_transport.go | 18 ++++++++-- seth/http_logging_transport_test.go | 56 +++++++++++++++++++++++++++++ 7 files changed, 103 insertions(+), 6 deletions(-) diff --git a/book/src/libs/seth.md b/book/src/libs/seth.md index 7bcb4f01c..4e689cfb6 100644 --- a/book/src/libs/seth.md +++ b/book/src/libs/seth.md @@ -976,7 +976,14 @@ You need to pass a file with a list of transaction hashes to trace. The file sho With `SETH_LOG_LEVEL=trace` we will also log to console all traffic between Seth and RPC node. This can be useful for debugging as you can see all the requests and responses. ### TLS certificate verification -Seth verifies TLS certificates of HTTPS RPC endpoints. If you need to connect to an endpoint with a self-signed certificate in a testing environment, set `SETH_INSECURE_SKIP_VERIFY=true`. Never use it with production RPC endpoints, as it allows anyone intercepting the traffic to return fake chain state. +Seth verifies TLS certificates of HTTPS RPC endpoints. If you need to connect to an endpoint with a self-signed certificate in a testing environment, you can disable verification for a single network in the TOML config: +```toml +[[networks]] +name = "Anvil" +urls_secret = ["https://localhost:8545"] +insecure_skip_verify = true +``` +or with `WithInsecureSkipVerify(true)` when building the client with `ClientBuilder`. Setting `SETH_INSECURE_SKIP_VERIFY=true` disables verification for all networks. Never use any of these with production RPC endpoints, as it allows anyone intercepting the traffic to return fake chain state. ### Read-only mode diff --git a/seth/.changeset/v1.52.0.md b/seth/.changeset/v1.52.0.md index c5427ade4..1cbdc280b 100644 --- a/seth/.changeset/v1.52.0.md +++ b/seth/.changeset/v1.52.0.md @@ -1 +1,3 @@ -- Verify TLS certificates of RPC endpoints by default (previously `InsecureSkipVerify` was always enabled). Set `SETH_INSECURE_SKIP_VERIFY=true` to restore the old behaviour in testing environments with self-signed certificates. +- **Breaking:** verify TLS certificates of RPC endpoints by default (previously `InsecureSkipVerify` was always enabled). Set `insecure_skip_verify = true` in the network TOML config, use `ClientBuilder.WithInsecureSkipVerify(true)` or set `SETH_INSECURE_SKIP_VERIFY=true` (all networks) to restore the old behaviour in testing environments with self-signed certificates. +- Add `WithInsecureSkipVerify` to `ClientBuilder` for programmatic usage +- RPC HTTP transport now honours `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` environment variables. diff --git a/seth/client.go b/seth/client.go index 32ce6f058..642215fa0 100644 --- a/seth/client.go +++ b/seth/client.go @@ -223,7 +223,7 @@ func NewClientRaw( cfg.MustFirstNetworkURL(), rpc.WithHeaders(cfg.RPCHeaders), rpc.WithHTTPClient(&http.Client{ - Transport: NewLoggingTransport(), + Transport: newRPCTransport(cfg.Network.InsecureSkipVerify), }), ) if err != nil { diff --git a/seth/client_builder.go b/seth/client_builder.go index 689776adb..ea3a52986 100644 --- a/seth/client_builder.go +++ b/seth/client_builder.go @@ -375,6 +375,24 @@ func (c *ClientBuilder) WithHooks(hooks Hooks) *ClientBuilder { return c } +// WithInsecureSkipVerify disables TLS certificate verification for RPC connections. +// Use only in testing environments with self-signed certificates, never with production RPC endpoints. +// It sets `insecure_skip_verify` of the selected network, SETH_INSECURE_SKIP_VERIFY=true disables verification for all networks. +// Default value is false. +func (c *ClientBuilder) WithInsecureSkipVerify(insecureSkipVerify bool) *ClientBuilder { + if !c.checkIfNetworkIsSet() { + return c + } + c.config.Network.InsecureSkipVerify = insecureSkipVerify + // defensive programming + if len(c.config.Networks) == 0 { + c.config.Networks = append(c.config.Networks, c.config.Network) + } else if net := c.config.findNetworkByName(c.config.Network.Name); net != nil { + net.InsecureSkipVerify = insecureSkipVerify + } + return c +} + // WithReadOnlyMode sets the client to read-only mode. It removes all private keys from all Networks and disables nonce protection and ephemeral addresses. func (c *ClientBuilder) WithReadOnlyMode() *ClientBuilder { c.readonly = true diff --git a/seth/config.go b/seth/config.go index e661d3c75..3bfb6d0ba 100644 --- a/seth/config.go +++ b/seth/config.go @@ -114,6 +114,8 @@ type Network struct { GasPriceEstimationBlocks uint64 `toml:"gas_price_estimation_blocks"` GasPriceEstimationTxPriority string `toml:"gas_price_estimation_tx_priority"` GasPriceEstimationAttemptCount uint `toml:"gas_price_estimation_attempt_count"` + // InsecureSkipVerify disables TLS certificate verification for RPC connections. Use only in testing environments. + InsecureSkipVerify bool `toml:"insecure_skip_verify"` } // DefaultClient returns a Client with reasonable default config with the specified RPC URL and private keys. You should pass at least 1 private key. diff --git a/seth/http_logging_transport.go b/seth/http_logging_transport.go index 99a1b3b23..44c628ab4 100644 --- a/seth/http_logging_transport.go +++ b/seth/http_logging_transport.go @@ -54,10 +54,22 @@ func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) // NewLoggingTransport creates a new transport for RPC HTTP connections. TLS certificates are verified // unless InsecureSkipVerifyEnvVar is set to "true". When SETH_LOG_LEVEL is "trace" requests and responses are logged. func NewLoggingTransport() http.RoundTripper { - transport := http.DefaultTransport.(*http.Transport).Clone() - if strings.EqualFold(os.Getenv(InsecureSkipVerifyEnvVar), "true") { + return newRPCTransport(false) +} + +// newRPCTransport creates a transport for RPC HTTP connections. TLS certificate verification is disabled +// when insecureSkipVerify is true or InsecureSkipVerifyEnvVar is set to "true". +func newRPCTransport(insecureSkipVerify bool) http.RoundTripper { + var transport *http.Transport + if defaultTransport, ok := http.DefaultTransport.(*http.Transport); ok { + transport = defaultTransport.Clone() + } else { + // http.DefaultTransport was replaced with a custom RoundTripper, fall back to a transport with sane defaults + transport = &http.Transport{Proxy: http.ProxyFromEnvironment} + } + + if insecureSkipVerify || strings.EqualFold(os.Getenv(InsecureSkipVerifyEnvVar), "true") { L.Warn(). - Str("EnvVar", InsecureSkipVerifyEnvVar). Msg("TLS certificate verification is disabled for RPC connections. Do not use this setting outside of testing environments") transport.TLSClientConfig = &tls.Config{ //nolint:gosec // explicit opt-in for testing environments with self-signed certificates diff --git a/seth/http_logging_transport_test.go b/seth/http_logging_transport_test.go index 787d66632..e164887bf 100644 --- a/seth/http_logging_transport_test.go +++ b/seth/http_logging_transport_test.go @@ -5,6 +5,7 @@ import ( "net/http/httptest" "testing" + "github.com/pelletier/go-toml/v2" "github.com/stretchr/testify/require" ) @@ -35,5 +36,60 @@ func TestNewLoggingTransport_TLS(t *testing.T) { _ = resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode) }) + + t.Run("untrusted certificate is accepted when verification is disabled via config with log level "+logLevel, func(t *testing.T) { + t.Setenv(LogLevelEnvVar, logLevel) + client := &http.Client{Transport: newRPCTransport(true)} + resp, err := client.Get(server.URL) + require.NoError(t, err) + _ = resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + }) } + + t.Run("custom http.DefaultTransport does not panic and still verifies certificates", func(t *testing.T) { + original := http.DefaultTransport + http.DefaultTransport = &LoggingTransport{Transport: original} + t.Cleanup(func() { http.DefaultTransport = original }) + + client := &http.Client{Transport: NewLoggingTransport()} + resp, err := client.Get(server.URL) + if resp != nil { + _ = resp.Body.Close() + } + require.Error(t, err, "expected TLS verification to fail for untrusted certificate") + require.Contains(t, err.Error(), "certificate") + }) +} + +func TestNetworkInsecureSkipVerify(t *testing.T) { + t.Run("is read from TOML network config", func(t *testing.T) { + var cfg Config + err := toml.Unmarshal([]byte(` +[[networks]] +name = "Secure" +urls_secret = ["https://rpc.example.com"] + +[[networks]] +name = "SelfSigned" +urls_secret = ["https://localhost:8545"] +insecure_skip_verify = true +`), &cfg) + require.NoError(t, err) + require.Len(t, cfg.Networks, 2) + require.False(t, cfg.Networks[0].InsecureSkipVerify, "expected TLS verification to be enabled by default") + require.True(t, cfg.Networks[1].InsecureSkipVerify, "expected TLS verification to be disabled for SelfSigned network") + }) + + t.Run("is set on selected network by builder", func(t *testing.T) { + cfg, err := NewClientBuilder(). + WithRpcUrl("https://localhost:8545"). + WithInsecureSkipVerify(true). + WithReadOnlyMode(). + BuildConfig() + require.NoError(t, err) + require.True(t, cfg.Network.InsecureSkipVerify) + require.Len(t, cfg.Networks, 1) + require.True(t, cfg.Networks[0].InsecureSkipVerify) + }) }