mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
refactor: use bluebottle/pkg/tlsconfig for the TLS curve allowlist (#910)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user