Go version
go1.25.8
Output of go env in your module/workspace:
AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/shaunak/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/shaunak/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=...=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/shaunak/.gvm/pkgsets/go1.25.8/global/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/shaunak/.gvm/pkgsets/go1.25.8/global'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/shaunak/.gvm/gos/go1.25.8'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/shaunak/.gvm/gos/go1.25.8/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.25.8'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
Running a TLS test suite with GODEBUG=fips140=only (no tlsmlkem=0) against a TLS server using the default curve preferences.
What did you see happen?
TLS handshakes fail immediately with:
crypto/ecdh: use of X25519 is not allowed in FIPS 140-only mode
This happens even though the TLS FIPS policy in defaults_fips140.go lists X25519MLKEM768 in allowedCurvePreferencesFIPS, implying it should be permitted.
What did you expect to see?
Either:
- The handshake succeeds by negotiating
X25519MLKEM768 (if it is truly FIPS-permitted), or
X25519MLKEM768 is absent from allowedCurvePreferencesFIPS so it is never offered, consistent with the primitive-layer enforcement in src/crypto/ecdh/x25519.go lines 39, 51, and 71
Root cause
X25519MLKEM768 is a hybrid key exchange combining ML-KEM-768 and X25519. When negotiated, it invokes ecdh.X25519() operations, which are unconditionally rejected in fips140=only mode at src/crypto/ecdh/x25519.go lines 39, 51, and 71:
if fips140only.Enforced() {
return nil, errors.New("crypto/ecdh: use of X25519 is not allowed in FIPS 140-only mode")
}
X25519/Curve25519 is correctly blocked here — it is not a NIST-approved curve and has never been added to the FIPS 140 approved list. However, allowedCurvePreferencesFIPS in src/crypto/tls/defaults_fips140.go lines 33–39 currently includes it:
allowedCurvePreferencesFIPS = []CurveID{
X25519MLKEM768, // ← always fails under fips140=only
SecP256r1MLKEM768,
SecP384r1MLKEM1024,
CurveP256,
CurveP384,
CurveP521,
}
This creates an inconsistency: the TLS policy layer permits X25519MLKEM768, so the client advertises it in the ClientHello and attempts to generate a key share — but the crypto primitive layer then immediately rejects it. The result is a broken handshake rather than graceful fallback to a supported group.
History
X25519MLKEM768 was added to allowedCurvePreferencesFIPS in commit 6114b69e0c ("crypto/tls: relax native FIPS 140-3 mode", fixing #71757) with the intent of allowing ML-KEM under the native FIPS 140-3 module. At that time, X25519MLKEM768 was the only ML-KEM TLS option available. The FIPS-safe alternatives SecP256r1MLKEM768 (ML-KEM-768 + P-256) and SecP384r1MLKEM1024 (ML-KEM-1024 + P-384) were added to the allowlist later in commit 1768cb40b8. Now that those exist in allowedCurvePreferencesFIPS, X25519MLKEM768 is both unreachable and broken in fips140=only mode.
Suggested fix
Remove X25519MLKEM768 from allowedCurvePreferencesFIPS in src/crypto/tls/defaults_fips140.go. The FIPS-approved post-quantum options SecP256r1MLKEM768 and SecP384r1MLKEM1024 already cover ML-KEM under fips140=only mode, paired with NIST-approved P-curves as required.
Impact
Any program running with GODEBUG=fips140=only using the default TLS curve preferences must currently also set GODEBUG=tlsmlkem=0 as a workaround, even though tlsmlkem=0 disables all MLKEM variants (including the FIPS-valid ones SecP256r1MLKEM768 and SecP384r1MLKEM1024). Removing X25519MLKEM768 from the allowlist would eliminate the need for this workaround and allow those groups to be negotiated correctly in FIPS-only mode.
Go version
go1.25.8
Output of
go envin your module/workspace:What did you do?
Running a TLS test suite with
GODEBUG=fips140=only(notlsmlkem=0) against a TLS server using the default curve preferences.What did you see happen?
TLS handshakes fail immediately with:
This happens even though the TLS FIPS policy in
defaults_fips140.golistsX25519MLKEM768inallowedCurvePreferencesFIPS, implying it should be permitted.What did you expect to see?
Either:
X25519MLKEM768(if it is truly FIPS-permitted), orX25519MLKEM768is absent fromallowedCurvePreferencesFIPSso it is never offered, consistent with the primitive-layer enforcement insrc/crypto/ecdh/x25519.golines 39, 51, and 71Root cause
X25519MLKEM768is a hybrid key exchange combining ML-KEM-768 and X25519. When negotiated, it invokesecdh.X25519()operations, which are unconditionally rejected infips140=onlymode atsrc/crypto/ecdh/x25519.golines 39, 51, and 71:X25519/Curve25519 is correctly blocked here — it is not a NIST-approved curve and has never been added to the FIPS 140 approved list. However,
allowedCurvePreferencesFIPSinsrc/crypto/tls/defaults_fips140.golines 33–39 currently includes it:This creates an inconsistency: the TLS policy layer permits
X25519MLKEM768, so the client advertises it in the ClientHello and attempts to generate a key share — but the crypto primitive layer then immediately rejects it. The result is a broken handshake rather than graceful fallback to a supported group.History
X25519MLKEM768was added toallowedCurvePreferencesFIPSin commit6114b69e0c("crypto/tls: relax native FIPS 140-3 mode", fixing #71757) with the intent of allowing ML-KEM under the native FIPS 140-3 module. At that time,X25519MLKEM768was the only ML-KEM TLS option available. The FIPS-safe alternativesSecP256r1MLKEM768(ML-KEM-768 + P-256) andSecP384r1MLKEM1024(ML-KEM-1024 + P-384) were added to the allowlist later in commit1768cb40b8. Now that those exist inallowedCurvePreferencesFIPS,X25519MLKEM768is both unreachable and broken infips140=onlymode.Suggested fix
Remove
X25519MLKEM768fromallowedCurvePreferencesFIPSinsrc/crypto/tls/defaults_fips140.go. The FIPS-approved post-quantum optionsSecP256r1MLKEM768andSecP384r1MLKEM1024already cover ML-KEM underfips140=onlymode, paired with NIST-approved P-curves as required.Impact
Any program running with
GODEBUG=fips140=onlyusing the default TLS curve preferences must currently also setGODEBUG=tlsmlkem=0as a workaround, even thoughtlsmlkem=0disables all MLKEM variants (including the FIPS-valid onesSecP256r1MLKEM768andSecP384r1MLKEM1024). RemovingX25519MLKEM768from the allowlist would eliminate the need for this workaround and allow those groups to be negotiated correctly in FIPS-only mode.