From 97acfbbd80c400857f8bf235a0e398e49d054caf Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:31:29 +1000 Subject: [PATCH] refactor: use bluebottle/pkg/tlsconfig for the TLS curve allowlist (#910) --- cmd/eks-token-webhook/cert.go | 2 +- go.mod | 2 +- go.sum | 4 +- internal/ecrauth/ecrauth.go | 2 +- internal/ecsgw/client.go | 2 +- internal/eksgw/client.go | 2 +- internal/rdsgw/client.go | 2 +- internal/tlsconfig/curves.go | 16 -- internal/tlsconfig/curves_test.go | 165 --------------------- spinifex/daemon/daemon.go | 2 +- spinifex/formation/formation.go | 2 +- spinifex/lbagent/agent.go | 2 +- spinifex/services/awsgw/awsgw.go | 2 +- spinifex/services/spinifexui/proxy.go | 2 +- spinifex/services/spinifexui/spinifexui.go | 2 +- spinifex/utils/nats.go | 2 +- 16 files changed, 15 insertions(+), 196 deletions(-) delete mode 100644 internal/tlsconfig/curves.go delete mode 100644 internal/tlsconfig/curves_test.go diff --git a/cmd/eks-token-webhook/cert.go b/cmd/eks-token-webhook/cert.go index fcaf21b5a..07c3057df 100644 --- a/cmd/eks-token-webhook/cert.go +++ b/cmd/eks-token-webhook/cert.go @@ -15,7 +15,7 @@ import ( "os" "time" - "github.com/mulgadc/spinifex/internal/tlsconfig" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" ) // serverTLSConfig is the webhook's loopback serving TLS config. diff --git a/go.mod b/go.mod index c4050b4d2..89695b398 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/jackc/pgx/v5 v5.10.0 github.com/klauspost/cpuid/v2 v2.4.0 github.com/miekg/dns v1.1.73 - github.com/mulgadc/bluebottle v1.18.1-0.20260827002656-8dfaf43f84e6 + github.com/mulgadc/bluebottle v1.18.1-0.20260827042935-b2f552cd66c4 github.com/mulgadc/northstar v1.18.0 github.com/mulgadc/predastore v1.18.1-0.20260826045109-26ce1b091112 github.com/mulgadc/viperblock v1.18.0 diff --git a/go.sum b/go.sum index b4c6feeec..c0d663d3b 100644 --- a/go.sum +++ b/go.sum @@ -1327,8 +1327,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/mulgadc/bluebottle v1.18.1-0.20260827002656-8dfaf43f84e6 h1:MEk7iPcOKgxWtRtJge8jiHjypiXFXEW8AfACOo3oN5U= -github.com/mulgadc/bluebottle v1.18.1-0.20260827002656-8dfaf43f84e6/go.mod h1:mvvV4d2w+kGLXxO1ymptNlxqUlNJKB2oAhk6PkpVLj8= +github.com/mulgadc/bluebottle v1.18.1-0.20260827042935-b2f552cd66c4 h1:C/LBupoohsrQJPX34I/AQ47QP2FTABWblH71oDfA9Bs= +github.com/mulgadc/bluebottle v1.18.1-0.20260827042935-b2f552cd66c4/go.mod h1:mvvV4d2w+kGLXxO1ymptNlxqUlNJKB2oAhk6PkpVLj8= github.com/mulgadc/northstar v1.18.0 h1:ksRjIKE6qtrw/ZoYaJh8JRBh+xdnuDw8FsmFLFCiXbg= github.com/mulgadc/northstar v1.18.0/go.mod h1:z6iol77R1vsIQyIBc1JQboUPhSJ6eG4bipzfbZZbLEM= github.com/mulgadc/predastore v1.18.1-0.20260826045109-26ce1b091112 h1:eCzKJDL1lmw6DNwjbDCQBoT3ZHIsYE1lMj5qitjySrY= diff --git a/internal/ecrauth/ecrauth.go b/internal/ecrauth/ecrauth.go index d45135ca5..bfd58fc79 100644 --- a/internal/ecrauth/ecrauth.go +++ b/internal/ecrauth/ecrauth.go @@ -19,7 +19,7 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ecr" - "github.com/mulgadc/spinifex/internal/tlsconfig" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" ) // defaultRegion is used when the caller supplies no region; SigV4 still needs a diff --git a/internal/ecsgw/client.go b/internal/ecsgw/client.go index f6c393b85..f12d96ae3 100644 --- a/internal/ecsgw/client.go +++ b/internal/ecsgw/client.go @@ -20,7 +20,7 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" v4 "github.com/aws/aws-sdk-go/aws/signer/v4" - "github.com/mulgadc/spinifex/internal/tlsconfig" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" ) // targetPrefix is the X-Amz-Target service prefix the gateway's ECS dispatch diff --git a/internal/eksgw/client.go b/internal/eksgw/client.go index 021dfc0c7..e8eceb8b4 100644 --- a/internal/eksgw/client.go +++ b/internal/eksgw/client.go @@ -17,8 +17,8 @@ import ( "strings" "time" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" "github.com/mulgadc/spinifex/internal/gwsign" - "github.com/mulgadc/spinifex/internal/tlsconfig" ) // Client posts SigV4-signed requests to the AWS gateway for the "eks" service. diff --git a/internal/rdsgw/client.go b/internal/rdsgw/client.go index 2b6e1ef00..db41674b4 100644 --- a/internal/rdsgw/client.go +++ b/internal/rdsgw/client.go @@ -22,8 +22,8 @@ import ( "strings" "time" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" "github.com/mulgadc/spinifex/internal/gwsign" - "github.com/mulgadc/spinifex/internal/tlsconfig" ) const ( diff --git a/internal/tlsconfig/curves.go b/internal/tlsconfig/curves.go deleted file mode 100644 index 7bc30ad60..000000000 --- a/internal/tlsconfig/curves.go +++ /dev/null @@ -1,16 +0,0 @@ -// Package tlsconfig pins the cluster-wide TLS 1.3 curve preference list. -// Hardcoded to prevent silent downgrade via misconfiguration; the FIPS build -// is the per-deployment choice. -package tlsconfig - -import "crypto/tls" - -// Curves is the fixed TLS 1.3 curve allowlist: two ML-KEM hybrids (PQ-capable -// peers always negotiate these) followed by two classical fallbacks for SDKs -// without ML-KEM. Weak Go defaults (SecP256r1MLKEM768, P-256, P-521) excluded. -var Curves = []tls.CurveID{ - tls.X25519MLKEM768, - tls.SecP384r1MLKEM1024, - tls.X25519, - tls.CurveP384, -} diff --git a/internal/tlsconfig/curves_test.go b/internal/tlsconfig/curves_test.go deleted file mode 100644 index 0dd7091b4..000000000 --- a/internal/tlsconfig/curves_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package tlsconfig_test - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "io" - "math/big" - "net" - "net/http" - "net/http/httptest" - "slices" - "strings" - "testing" - "time" - - "github.com/mulgadc/spinifex/internal/tlsconfig" -) - -func TestCurves_OnlyApprovedEntries(t *testing.T) { - want := []tls.CurveID{ - tls.X25519MLKEM768, - tls.SecP384r1MLKEM1024, - tls.X25519, - tls.CurveP384, - } - if !slices.Equal(tlsconfig.Curves, want) { - t.Errorf("Curves = %v, want %v", tlsconfig.Curves, want) - } -} - -func TestCurves_ExcludesWeakPrimitives(t *testing.T) { - forbidden := []tls.CurveID{ - tls.SecP256r1MLKEM768, // weakest ML-KEM hybrid (P-256 component) - tls.CurveP256, // not CNSA 2.0 - tls.CurveP521, // niche, no current peers - } - for _, c := range forbidden { - if slices.Contains(tlsconfig.Curves, c) { - t.Errorf("Curves includes forbidden curve %v", c) - } - } -} - -// TestIntegration_NegotiatesPQHybrid verifies that a Curves-configured server -// negotiates a PQ hybrid at TLS 1.3; checks for any approved hybrid to stay -// stable across Go stdlib reorderings. -func TestIntegration_NegotiatesPQHybrid(t *testing.T) { - cert := selfSignedCert(t) - srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - _, _ = io.WriteString(w, "ok") - })) - srv.TLS = &tls.Config{ - Certificates: []tls.Certificate{cert}, - MinVersion: tls.VersionTLS13, - CurvePreferences: tlsconfig.Curves, - } - srv.StartTLS() - defer srv.Close() - - pool := x509.NewCertPool() - pool.AddCert(srv.Certificate()) - - client := &http.Client{ - Timeout: 5 * time.Second, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: pool, - CurvePreferences: tlsconfig.Curves, - }, - }, - } - resp, err := client.Get(srv.URL) - if err != nil { - t.Fatalf("https get: %v", err) - } - defer resp.Body.Close() - - state := resp.TLS - if state == nil { - t.Fatal("response has no TLS state") - } - if state.Version != tls.VersionTLS13 { - t.Errorf("negotiated version = 0x%x, want TLS 1.3 (0x%x)", state.Version, tls.VersionTLS13) - } - pqHybrids := []tls.CurveID{tls.X25519MLKEM768, tls.SecP384r1MLKEM1024} - if !slices.Contains(pqHybrids, state.CurveID) { - t.Errorf("negotiated curve = %v, want one of %v (PQ hybrid)", state.CurveID, pqHybrids) - } -} - -// TestIntegration_TLS12ClientRejected confirms MinVersion TLS 1.3 is enforced: -// a TLS 1.2-pinned client must get a handshake error, not a silent downgrade. -func TestIntegration_TLS12ClientRejected(t *testing.T) { - cert := selfSignedCert(t) - srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - _, _ = io.WriteString(w, "ok") - })) - srv.TLS = &tls.Config{ - Certificates: []tls.Certificate{cert}, - MinVersion: tls.VersionTLS13, - CurvePreferences: tlsconfig.Curves, - } - srv.StartTLS() - defer srv.Close() - - pool := x509.NewCertPool() - pool.AddCert(srv.Certificate()) - - client := &http.Client{ - Timeout: 5 * time.Second, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: pool, - MinVersion: tls.VersionTLS12, - MaxVersion: tls.VersionTLS12, - }, - }, - } - _, err := client.Get(srv.URL) - if err == nil { - t.Fatal("TLS 1.2-pinned client succeeded; MinVersion not enforced") - } - if !strings.Contains(err.Error(), "protocol version") && - !strings.Contains(err.Error(), "tls:") { - t.Errorf("expected protocol-version handshake error, got: %v", err) - } -} - -func selfSignedCert(t *testing.T) tls.Certificate { - t.Helper() - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatalf("generate key: %v", err) - } - tmpl := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{CommonName: "tlsconfig-test"}, - NotBefore: time.Now().Add(-time.Hour), - NotAfter: time.Now().Add(time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - DNSNames: []string{"localhost"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, - } - der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) - if err != nil { - t.Fatalf("create cert: %v", err) - } - keyDER, err := x509.MarshalECPrivateKey(key) - if err != nil { - t.Fatalf("marshal key: %v", err) - } - certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) - keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) - pair, err := tls.X509KeyPair(certPEM, keyPEM) - if err != nil { - t.Fatalf("X509KeyPair: %v", err) - } - return pair -} diff --git a/spinifex/daemon/daemon.go b/spinifex/daemon/daemon.go index 61e6dfd9f..62b3311a2 100644 --- a/spinifex/daemon/daemon.go +++ b/spinifex/daemon/daemon.go @@ -29,7 +29,7 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/go-chi/chi/v5" "github.com/mulgadc/bluebottle/pkg/masterkey" - "github.com/mulgadc/spinifex/internal/tlsconfig" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" "github.com/mulgadc/spinifex/spinifex/admin" "github.com/mulgadc/spinifex/spinifex/awserrors" "github.com/mulgadc/spinifex/spinifex/config" diff --git a/spinifex/formation/formation.go b/spinifex/formation/formation.go index ae4961c05..3c74498a0 100644 --- a/spinifex/formation/formation.go +++ b/spinifex/formation/formation.go @@ -15,7 +15,7 @@ import ( "sync" "time" - "github.com/mulgadc/spinifex/internal/tlsconfig" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" ) // NodeInfo describes a node participating in cluster formation. diff --git a/spinifex/lbagent/agent.go b/spinifex/lbagent/agent.go index c16cd2109..fc0cce5fe 100644 --- a/spinifex/lbagent/agent.go +++ b/spinifex/lbagent/agent.go @@ -24,8 +24,8 @@ import ( "syscall" "time" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" "github.com/mulgadc/spinifex/internal/gwsign" - "github.com/mulgadc/spinifex/internal/tlsconfig" "github.com/mulgadc/spinifex/spinifex/awserrors" "github.com/mulgadc/spinifex/spinifex/otelsetup" ) diff --git a/spinifex/services/awsgw/awsgw.go b/spinifex/services/awsgw/awsgw.go index 3e0caa8f6..f39b0c5f4 100644 --- a/spinifex/services/awsgw/awsgw.go +++ b/spinifex/services/awsgw/awsgw.go @@ -16,7 +16,7 @@ import ( "github.com/mulgadc/bluebottle/pkg/masterkey" "github.com/mulgadc/bluebottle/pkg/ratelimit" - "github.com/mulgadc/spinifex/internal/tlsconfig" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" "github.com/mulgadc/spinifex/spinifex/admin" "github.com/mulgadc/spinifex/spinifex/config" "github.com/mulgadc/spinifex/spinifex/gateway" diff --git a/spinifex/services/spinifexui/proxy.go b/spinifex/services/spinifexui/proxy.go index 6b0783f9a..0050e72d6 100644 --- a/spinifex/services/spinifexui/proxy.go +++ b/spinifex/services/spinifexui/proxy.go @@ -13,7 +13,7 @@ import ( "strings" "time" - "github.com/mulgadc/spinifex/internal/tlsconfig" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) diff --git a/spinifex/services/spinifexui/spinifexui.go b/spinifex/services/spinifexui/spinifexui.go index 7fd57a509..cff6543d0 100644 --- a/spinifex/services/spinifexui/spinifexui.go +++ b/spinifex/services/spinifexui/spinifexui.go @@ -20,7 +20,7 @@ import ( "time" "github.com/go-chi/chi/v5/middleware" - "github.com/mulgadc/spinifex/internal/tlsconfig" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" "github.com/mulgadc/spinifex/spinifex/otelsetup" "github.com/mulgadc/spinifex/spinifex/utils" ) diff --git a/spinifex/utils/nats.go b/spinifex/utils/nats.go index bd1606191..6d6f5a151 100644 --- a/spinifex/utils/nats.go +++ b/spinifex/utils/nats.go @@ -11,7 +11,7 @@ import ( "os" "time" - "github.com/mulgadc/spinifex/internal/tlsconfig" + "github.com/mulgadc/bluebottle/pkg/tlsconfig" "github.com/mulgadc/spinifex/spinifex/awserrors" "github.com/mulgadc/spinifex/spinifex/otelsetup" "github.com/nats-io/nats.go"