Skip to content

crypto/tls: 25519MLKEM768 breaks fips140 #78298

Description

@panga

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions