Files
cloudflared/crypto/curves_test.go
T
lneto f674b82e2a TUN-10413: Centralize TLS curve configuration in crypto/ and adopt X25519MLKEM768 for QUIC/H2
Introduce a new crypto/ package as the single source of truth for TLS
curve preferences used on every edge-facing connection, and adopt
X25519MLKEM768 as the primary post-quantum key exchange for both QUIC
and HTTP/2:

  PQ Prefer (default):     X25519MLKEM768, P256Kyber768Draft00, CurveP256
  PQ Strict (--post-quantum): X25519MLKEM768, P256Kyber768Draft00

The curve list is identical under FIPS and non-FIPS builds, so
crypto.GetCurvePreferences takes only a features.PostQuantumMode and
returns a fresh slice on every call.

HTTP/2 now applies these curve preferences the same way QUIC does. The
previous PostQuantumStrict rejection in serveHTTP2 and the forced
QUIC-only selection in NewProtocolSelector are removed since both
transports support the same post-quantum curves; the needPQ parameter
is dropped from NewProtocolSelector accordingly.

Also fix a shared tls.Config race: both the QUIC and HTTP/2 paths now
Clone() the per-protocol entry from TunnelConfig.EdgeTLSConfigs before
mutating CurvePreferences instead of writing through the shared map
entry.

Legacy Kyber draft curve X25519Kyber768Draft00
and the unused removeDuplicates helper are removed along with the old
supervisor/pqtunnels.go / _test.go files.

AGENTS.md is updated with guidance on the new crypto/ package, the
cfdcrypto import alias, the tls.Config cloning rule, and the lint
workflow implications of .golangci.yaml's whole-files: true setting.
2026-05-12 07:47:38 +01:00

127 lines
4.2 KiB
Go

package crypto
import (
"crypto/tls"
"net/http"
"net/http/httptest"
"runtime"
"slices"
"testing"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/features"
)
// TestCurvePreferences verifies that GetCurvePreferences returns the
// documented curve list for each supported PostQuantumMode. The expected
// values correspond to the contract described in the package documentation
// and must be identical under FIPS and non-FIPS builds (see TUN-10413).
func TestCurvePreferences(t *testing.T) {
t.Parallel()
tests := []struct {
name string
expectedCurves []tls.CurveID
pqMode features.PostQuantumMode
}{
{
name: "Prefer PQ",
pqMode: features.PostQuantumPrefer,
expectedCurves: []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00, tls.CurveP256},
},
{
name: "Strict PQ",
pqMode: features.PostQuantumStrict,
expectedCurves: []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00},
},
}
for _, tcase := range tests {
t.Run(tcase.name, func(t *testing.T) {
t.Parallel()
curves, err := getCurvePreferences(tcase.pqMode)
require.NoError(t, err)
require.Equal(t, tcase.expectedCurves, curves)
})
}
}
// TestCurvePreferenceUnknownMode asserts that passing a PostQuantumMode
// value outside of the documented constants produces an error instead of
// silently returning a nil or default curve list. This protects callers
// from accidentally negotiating with an unintended curve set.
func TestCurvePreferenceUnknownMode(t *testing.T) {
t.Parallel()
_, err := getCurvePreferences(features.PostQuantumMode(255))
require.Error(t, err)
}
// TestReturnedSliceIsIndependent ensures GetCurvePreferences returns a
// fresh slice on every call, so that callers cannot corrupt the
// package-level defaults by mutating the result.
func TestReturnedSliceIsIndependent(t *testing.T) {
t.Parallel()
first, err := getCurvePreferences(features.PostQuantumPrefer)
require.NoError(t, err)
// Mutate the returned slice.
first[0] = tls.CurveP521
second, err := getCurvePreferences(features.PostQuantumPrefer)
require.NoError(t, err)
require.Equal(t, tls.X25519MLKEM768, second[0], "package defaults must not be affected by caller mutation")
}
// runClientServerHandshake drives a TLS 1.3 handshake with the given curve
// preferences set on the client and captures the SupportedCurves list
// advertised by the client in its ClientHello. The helper is used by
// TestSupportedCurvesNegotiation to exercise the curves end-to-end against
// the standard library's TLS stack.
func runClientServerHandshake(t *testing.T, curves []tls.CurveID) []tls.CurveID {
var advertisedCurves []tls.CurveID
ts := httptest.NewUnstartedServer(nil)
ts.TLS = &tls.Config{ // nolint: gosec
GetConfigForClient: func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
advertisedCurves = slices.Clone(chi.SupportedCurves)
return nil, nil
},
}
ts.StartTLS()
defer ts.Close()
clientTLSConfig := ts.Client().Transport.(*http.Transport).TLSClientConfig
clientTLSConfig.CurvePreferences = curves
resp, err := ts.Client().Head(ts.URL)
if err != nil {
t.Error(err)
return nil
}
defer func() { _ = resp.Body.Close() }()
return advertisedCurves
}
// TestSupportedCurvesNegotiation verifies that the curves returned by
// GetCurvePreferences survive a real TLS handshake unchanged, i.e. the
// standard library advertises exactly the curves we expect. Currently only
// PostQuantumPrefer is exercised because PostQuantumStrict would cause the
// handshake to fail against httptest servers that do not support
// X25519MLKEM768 server-side.
func TestSupportedCurvesNegotiation(t *testing.T) {
t.Parallel()
for _, tcase := range []features.PostQuantumMode{features.PostQuantumPrefer} {
curves, err := getCurvePreferences(tcase)
require.NoError(t, err)
advertisedCurves := runClientServerHandshake(t, curves)
require.True(t, slices.Contains(advertisedCurves, tls.CurveP256))
require.True(t, slices.Contains(advertisedCurves, tls.X25519MLKEM768))
expectedLength := 2
if runtime.GOOS == "linux" {
// P256Kyber768Draft00 only exists in linux
require.True(t, slices.Contains(advertisedCurves, P256Kyber768Draft00))
expectedLength = 3
}
require.Len(t, advertisedCurves, expectedLength)
}
}