From 34e1733cde98cfb34032e8d4cd69e036b13289cb Mon Sep 17 00:00:00 2001 From: Badgerbees Date: Thu, 12 Mar 2026 11:02:28 +0700 Subject: [PATCH] fix: load CA certs from Termux paths on Android (#1375) On Android/Termux, Go's x509.SystemCertPool() returns an empty pool because it does not probe Termux-specific paths. This causes TLS handshakes to fail with 'certificate signed by unknown authority' for all LLM providers, including volcengine. Changes: - Add buildCertPool() helper that supplements the system cert pool with CA bundles from Termux-specific paths (/data/data/com.termux/files/usr/etc/tls/cert.pem, etc.) - Update NewProvider() to always use an explicit TLS-configured transport that clones http.DefaultTransport and sets RootCAs - Preserve proxy support and all http.DefaultTransport defaults - Add TestNewProvider_TLSTransportNeverInsecure() to ensure InsecureSkipVerify is never set The fix is secure: certificate verification remains enforced, no weak ciphers or version downgrades, InsecureSkipVerify is never set. Fixes #1375 --- pkg/providers/openai_compat/provider.go | 41 +++++++++++++++++--- pkg/providers/openai_compat/provider_test.go | 28 +++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index f97bf3acd..bef24bd18 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -4,12 +4,15 @@ import ( "bufio" "bytes" "context" + "crypto/tls" + "crypto/x509" "encoding/json" "fmt" "io" "log" "net/http" "net/url" + "os" "strings" "time" @@ -54,22 +57,48 @@ func WithRequestTimeout(timeout time.Duration) Option { } } -func NewProvider(apiKey, apiBase, proxy string, opts ...Option) *Provider { - client := &http.Client{ - Timeout: defaultRequestTimeout, +// buildCertPool returns the system cert pool supplemented with CA bundles from +// well-known Termux paths. On Android/Termux, Go's x509.SystemCertPool returns +// an empty pool because it does not probe Termux-specific locations, causing +// TLS handshakes to fail with "certificate signed by unknown authority". +// InsecureSkipVerify is never set. +func buildCertPool() *x509.CertPool { + pool, err := x509.SystemCertPool() + if err != nil || pool == nil { + pool = x509.NewCertPool() } + for _, p := range []string{ + "/data/data/com.termux/files/usr/etc/tls/cert.pem", + "/data/data/com.termux/files/usr/etc/ssl/certs/ca-bundle.crt", + "/data/data/com.termux/files/usr/etc/ssl/certs/ca-certificates.crt", + } { + if pem, e := os.ReadFile(p); e == nil { + pool.AppendCertsFromPEM(pem) + } + } + return pool +} + +func NewProvider(apiKey, apiBase, proxy string, opts ...Option) *Provider { + // Clone preserves all http.DefaultTransport defaults (connection pooling, + // dial/TLS handshake timeouts, HTTP/2, env proxy). We only patch RootCAs. + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{RootCAs: buildCertPool()} if proxy != "" { parsed, err := url.Parse(proxy) if err == nil { - client.Transport = &http.Transport{ - Proxy: http.ProxyURL(parsed), - } + transport.Proxy = http.ProxyURL(parsed) } else { log.Printf("openai_compat: invalid proxy URL %q: %v", proxy, err) } } + client := &http.Client{ + Timeout: defaultRequestTimeout, + Transport: transport, + } + p := &Provider{ apiKey: apiKey, apiBase: strings.TrimRight(apiBase, "/"), diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go index 41f278a1b..f4c1d50ee 100644 --- a/pkg/providers/openai_compat/provider_test.go +++ b/pkg/providers/openai_compat/provider_test.go @@ -841,3 +841,31 @@ func TestSerializeMessages_StripsSystemParts(t *testing.T) { t.Fatal("system_parts should not appear in serialized output") } } + +// TestNewProvider_TLSTransportNeverInsecure ensures every provider — whether +// configured with a proxy or not — has an explicit TLS transport that never +// sets InsecureSkipVerify. This is the security guard for issue #1375. +func TestNewProvider_TLSTransportNeverInsecure(t *testing.T) { + tests := []struct { + name string + proxy string + }{ + {name: "no proxy", proxy: ""}, + {name: "with proxy", proxy: "http://127.0.0.1:8080"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := NewProvider("key", "https://example.com", tt.proxy) + tr, ok := p.httpClient.Transport.(*http.Transport) + if !ok || tr == nil { + t.Fatalf("Transport = %T, want *http.Transport", p.httpClient.Transport) + } + if tr.TLSClientConfig == nil { + t.Fatal("TLSClientConfig is nil") + } + if tr.TLSClientConfig.InsecureSkipVerify { + t.Fatal("InsecureSkipVerify must never be true") + } + }) + } +}