refactor: use bluebottle/pkg/tlsconfig for the TLS curve allowlist (#910)

This commit is contained in:
Julian Sommer
2026-08-27 14:31:29 +10:00
committed by GitHub
parent fc1e950518
commit 97acfbbd80
16 changed files with 15 additions and 196 deletions
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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 (
-16
View File
@@ -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,
}
-165
View File
@@ -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
}
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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"
)
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
)
+1 -1
View File
@@ -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"
)
+1 -1
View File
@@ -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"