Go version
go1.26.1
Output of go env in your module/workspace:
AR='ar'
CC='cc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='c++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/x/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/x/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=/var/folders/4j/cg78czc50dg818bdt5_fjtz80000gp/T/go-build4165245989=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/x/go.mod'
GOMODCACHE='/Users/x/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/x/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/x/homebrew/Cellar/go/1.26.1/libexec'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/x/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/x/homebrew/Cellar/go/1.26.1/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.26.1'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
Summary
Go 1.24+ has a bug where GODEBUG=fips140=only causes TLS connections to fail with:
crypto/ecdh: use of X25519 is not allowed in FIPS 140-only mode
The root cause is an inconsistency between two parts of the standard library:
crypto/tls/defaults_fips140.go lists X25519MLKEM768 as a FIPS-approved curve
crypto/ecdh/x25519.go blocks all X25519 operations in FIPS-only mode
Since X25519MLKEM768 is a hybrid that internally uses ecdh.X25519(), any TLS handshake in FIPS mode attempts to generate an X25519 key share and gets rejected by the crypto primitive layer.
Affected Versions
- Go 1.24+ (when
X25519MLKEM768 was added to the default curve preferences)
- Confirmed on Go 1.26.1
Impact
Any Go program making TLS connections with GODEBUG=fips140=only will fail.
- Standard
net/http clients (no custom tls.Config)
- gRPC connections via
grpc-go
- Any TLS client using default curve preferences
Call Chain
The failure follows this path through the standard library:
1. FIPS allowed curves include X25519MLKEM768
src/crypto/tls/defaults_fips140.go:33-39
allowedCurvePreferencesFIPS = []CurveID{
X25519MLKEM768, // <-- listed as FIPS-allowed
SecP256r1MLKEM768,
SecP384r1MLKEM1024,
CurveP256,
CurveP384,
CurveP521,
}
2. curvePreferences() filters defaults against the FIPS allowlist
src/crypto/tls/common.go:1270-1286
func (c *Config) curvePreferences(version uint16) []CurveID {
curvePreferences := defaultCurvePreferences()
if fips140tls.Required() {
curvePreferences = slices.DeleteFunc(curvePreferences, func(x CurveID) bool {
return !slices.Contains(allowedCurvePreferencesFIPS, x)
})
}
// ...
}
Result in FIPS mode: [X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024, P-256, P-384, P-521]
3. Client picks supportedCurves[0] for the initial key share
src/crypto/tls/handshake_client.go:147-148
curveID := hello.supportedCurves[0] // --> X25519MLKEM768
ke, err := keyExchangeForCurveID(curveID)
4. X25519MLKEM768 creates a hybrid key exchange that uses ecdh.X25519()
src/crypto/tls/key_schedule.go:97-100
case X25519MLKEM768:
return &hybridKeyExchange{id, ecdhKeyExchange{X25519, ecdh.X25519()},
32, mlkem.EncapsulationKeySize768, mlkem.CiphertextSize768,
newMLKEMPrivateKey768, newMLKEMPublicKey768}, nil
5. Hybrid key share generation calls X25519 GenerateKey
src/crypto/tls/key_schedule.go:167-168 calls
src/crypto/tls/key_schedule.go:119-120
func (ke *hybridKeyExchange) keyShares(rand io.Reader) (...) {
priv, ecdhShares, err := ke.ecdh.keyShares(rand) // calls into X25519
// ...
func (ke *ecdhKeyExchange) keyShares(rand io.Reader) (...) {
priv, err := ke.curve.GenerateKey(rand) // ecdh.X25519().GenerateKey()
6. crypto/ecdh rejects X25519 in FIPS-only mode
src/crypto/ecdh/x25519.go:37-39
func (c *x25519Curve) GenerateKey(r io.Reader) (*PrivateKey, error) {
if fips140only.Enforced() {
return nil, errors.New("crypto/ecdh: use of X25519 is not allowed in FIPS 140-only mode")
}
What did you see happen?
Reproducing the Bug
Test program (main.go)
A minimal program that triggers the bug with no custom TLS configuration:
package main
import (
"crypto/tls"
"fmt"
"net/http"
"os"
)
func main() {
fmt.Printf("GODEBUG=%s\n\n", os.Getenv("GODEBUG"))
client := &http.Client{}
resp, err := client.Get("https://example.com")
// Breaks on FIPS endpoint too: https://s3-fips.us-gov-west-1.amazonaws.com/dummy
if err != nil {
fmt.Fprintf(os.Stderr, "request failed: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
fmt.Printf("Status: %s\n", resp.Status)
fmt.Printf("Protocol: %s\n", resp.Proto)
fmt.Printf("TLS Version: 0x%04x\n", resp.TLS.Version)
fmt.Printf("Cipher Suite: %s\n", tls.CipherSuiteName(resp.TLS.CipherSuite))
fmt.Printf("Server Name: %s\n", resp.TLS.ServerName)
fmt.Printf("Negotiated Protocol: %s\n", resp.TLS.NegotiatedProtocol)
}
Expected output (without FIPS)
GODEBUG=
Status: 200 OK
Protocol: HTTP/1.1
TLS Version: 0x0304
Cipher Suite: TLS_AES_256_GCM_SHA384
Server Name: example.com
Negotiated Protocol:
Actual output (with FIPS)
GODEBUG=fips140=only
request failed: Get "https://example.com": crypto/ecdh: use of X25519 is not allowed in FIPS 140-only mode
exit status 1
Workarounds
Option A: Disable post-quantum curves via GODEBUG
GODEBUG=fips140=only,tlsmlkem=0
Setting tlsmlkem=0 removes all MLKEM hybrid curves from the default preferences (see defaults.go:24), falling back to [X25519, P-256, P-384, P-521]. Combined with FIPS mode filtering out X25519, the effective list becomes [P-256, P-384, P-521].
Option B: Set CurvePreferences explicitly in application code
If you can't patch the Go installation, restrict curves in your tls.Config:
tlsCfg := &tls.Config{
CurvePreferences: []tls.CurveID{
tls.CurveP256,
tls.CurveP384,
},
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsCfg,
},
}
This prevents Go from offering X25519MLKEM768 in the ClientHello, avoiding the bug entirely. However, this requires modifying every TLS client in your codebase (or forking third-party tools).
Option C: Patch the Go standard library
Remove X25519MLKEM768 from allowedCurvePreferencesFIPS in the Go installation.
What did you expect to see?
The fix should ensure consistency between the FIPS curve allowlist and the crypto primitive enforcement:
Allow X25519 in crypto/ecdh when used as part of a hybrid X25519MLKEM768 as a FIPS-approved composite algorithm.
Go version
go1.26.1
Output of
go envin your module/workspace:What did you do?
Summary
Go 1.24+ has a bug where
GODEBUG=fips140=onlycauses TLS connections to fail with:The root cause is an inconsistency between two parts of the standard library:
crypto/tls/defaults_fips140.golistsX25519MLKEM768as a FIPS-approved curvecrypto/ecdh/x25519.goblocks all X25519 operations in FIPS-only modeSince
X25519MLKEM768is a hybrid that internally usesecdh.X25519(), any TLS handshake in FIPS mode attempts to generate an X25519 key share and gets rejected by the crypto primitive layer.Affected Versions
X25519MLKEM768was added to the default curve preferences)Impact
Any Go program making TLS connections with
GODEBUG=fips140=onlywill fail.net/httpclients (no customtls.Config)grpc-goCall Chain
The failure follows this path through the standard library:
1. FIPS allowed curves include X25519MLKEM768
src/crypto/tls/defaults_fips140.go:33-392. curvePreferences() filters defaults against the FIPS allowlist
src/crypto/tls/common.go:1270-1286Result in FIPS mode:
[X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024, P-256, P-384, P-521]3. Client picks supportedCurves[0] for the initial key share
src/crypto/tls/handshake_client.go:147-1484. X25519MLKEM768 creates a hybrid key exchange that uses ecdh.X25519()
src/crypto/tls/key_schedule.go:97-1005. Hybrid key share generation calls X25519 GenerateKey
src/crypto/tls/key_schedule.go:167-168callssrc/crypto/tls/key_schedule.go:119-1206. crypto/ecdh rejects X25519 in FIPS-only mode
src/crypto/ecdh/x25519.go:37-39What did you see happen?
Reproducing the Bug
Test program (
main.go)A minimal program that triggers the bug with no custom TLS configuration:
Expected output (without FIPS)
Actual output (with FIPS)
Workarounds
Option A: Disable post-quantum curves via GODEBUG
Setting
tlsmlkem=0removes all MLKEM hybrid curves from the default preferences (seedefaults.go:24), falling back to[X25519, P-256, P-384, P-521]. Combined with FIPS mode filtering out X25519, the effective list becomes[P-256, P-384, P-521].Option B: Set CurvePreferences explicitly in application code
If you can't patch the Go installation, restrict curves in your
tls.Config:This prevents Go from offering
X25519MLKEM768in the ClientHello, avoiding the bug entirely. However, this requires modifying every TLS client in your codebase (or forking third-party tools).Option C: Patch the Go standard library
Remove
X25519MLKEM768fromallowedCurvePreferencesFIPSin the Go installation.What did you expect to see?
The fix should ensure consistency between the FIPS curve allowlist and the crypto primitive enforcement:
Allow X25519 in crypto/ecdh when used as part of a hybrid X25519MLKEM768 as a FIPS-approved composite algorithm.