All research

Does FIPS 140-3 disable post-quantum TLS? It silently downgrades you

FIPS-140-3 TLS truth table: Go strips post-quantum, OpenSSL keeps the non-approved hybrid.

The short answer to "does FIPS disable post-quantum TLS" is: it does not give you compliant post-quantum TLS, and it can quietly turn it off. In our own lab, on the wire, two major stacks did opposite things once we put them in FIPS mode, and neither offered the FIPS-approved post-quantum group by default. Go 1.24, under GODEBUG=fips140=on and =only, stripped every post-quantum group from its ClientHello and silently negotiated classical P-256, so a connection that "succeeded" carried zero quantum-resistant key exchange. OpenSSL 3.5.2's FIPS provider did the opposite: it still offered and negotiated X25519MLKEM768, the hybrid whose X25519 half is not FIPS-approved. Its FIPS provider gates that half through an approval indicator rather than blocking it. So FIPS 140-3 post-quantum TLS lands in one of two failure modes: turn FIPS on and either post-quantum protection goes off (Go) or a non-compliant group stays on (OpenSSL). Harvest-now-decrypt-later protection is silently mis-set for exactly the regulated orgs mandated to run FIPS.

The blunt version "FIPS on" is not "post-quantum on." Go 1.24 in FIPS mode removes the post-quantum hybrid entirely and falls back to classical P-256 with no error. OpenSSL 3.5 in FIPS mode keeps X25519MLKEM768, whose classical half is not on the FIPS approved list. Neither one offers SecP256r1MLKEM768, the hybrid that is actually FIPS-legal, unless you configure it by hand.

What we found when we flipped the FIPS switch

We built two clients and two servers, one pair on Go 1.24, one pair on OpenSSL 3.5.2, and watched their TLS 1.3 handshakes on the wire with FIPS mode off and then on. TLS 1.3 sends the supported_groups and key_share extensions in the clear, so the ClientHello is ground truth: you can read exactly which key-exchange groups a client is willing to use and which one it prepared a share for. No guessing from a library's documentation, just the bytes it put on the socket.

With Go 1.24 in FIPS mode the ClientHello collapsed to the NIST curves only. The supported_groups list was secp256r1, secp384r1, secp521r1 and nothing else: no bare x25519, no X25519MLKEM768, and, notably, not even the FIPS-approved SecP256r1MLKEM768. The key_share was secp256r1 and the negotiated group was secp256r1, classical elliptic-curve Diffie-Hellman with no post-quantum component at all. The behavior was identical for fips140=on and the stricter fips140=only. A Go server in FIPS mode, offered a post-quantum group by a non-FIPS peer, answered with a HelloRetryRequest and forced the handshake down to secp256r1. In every case the handshake completed successfully. That is the dangerous part: nothing failed, nothing warned, the connection just quietly lost its quantum resistance.

With OpenSSL 3.5.2 the FIPS provider went the other way. Its FIPS-mode ClientHello led with X25519MLKEM768 and carried a key_share for it, followed by P-256, P-384, P-521, and the finite-field groups. Compared to its non-FIPS baseline, the only groups FIPS mode removed were the standalone curves x25519 and x448; the hybrid X25519MLKEM768 stayed at the top of the list with a live key_share, and it negotiated that group against a peer that supported it. The OpenSSL server accepted X25519MLKEM768 too. So OpenSSL in FIPS mode keeps offering a group whose classical half it will not let you use on its own.

The truth table, on the wire

Here is every case we measured, with the groups each stack offered, the group it negotiated, and what that means for you. The Go rows and the OpenSSL rows are the story: same intent, "make TLS FIPS-compliant," opposite result.

FIPS-140-3 TLS behavior measured on the wire, lab run 2026-08-04
StackRoleFIPS modeGroups offeredNegotiated groupOutcome
Go 1.24Clientfips140=on / =onlysecp256r1, secp384r1, secp521r1secp256r1Classical only, no post-quantum
Go 1.24Serverfips onNIST curves; sends HelloRetryRequestsecp256r1Forces peer down to classical
OpenSSL 3.5.2Clientfips onX25519MLKEM768, secp256r1/384/521, ffdhe2048/3072X25519MLKEM768Post-quantum, but non-approved hybrid
OpenSSL 3.5.2Serverfips onAccepts X25519MLKEM768X25519MLKEM768Post-quantum, but non-approved hybrid
OpenSSL 3.5.2Both, forcedfips onX25519MLKEM768 (forced)X25519MLKEM768Handshake succeeds under FIPS
OpenSSL 3.5.2Both, forcedfips onSecP256r1MLKEM768 (forced)SecP256r1MLKEM768Approved hybrid works, but never default

Read the last two rows against everything above them. Both post-quantum hybrids handshake cleanly with both peers in FIPS mode, including the fully approved SecP256r1MLKEM768. The stacks are capable of doing the right thing. They just do not pick it for you: Go removes all of them, OpenSSL defaults to the non-approved one.

How we measured it, so a skeptic can trust the numbers

The dataset is our own, built on our own systems, aggregate only. We ran everything in Docker containers we built, on an ephemeral cloud VM, talking to each other over loopback. No third party's server was touched, no external endpoint was contacted, and nothing about any individual was collected. This is a lab, not a scan of anyone else's infrastructure.

We used three independent checks so no single tool has to be trusted on faith:

Why FIPS does this: X25519 is not approved, ML-KEM is

The mechanism is a mismatch between what the standards approve and what the ecosystem defaults to. Under NIST's key-agreement rules in SP 800-56A, X25519 is not a FIPS-approved scheme, which is why a strict module refuses it on its own. ML-KEM, standardized as FIPS 203, is approved. The only widely specified hybrid that is FIPS-legal on both halves is SecP256r1MLKEM768: NIST P-256, an approved curve, bonded to ML-KEM-768. The competing hybrid X25519MLKEM768 pairs approved ML-KEM-768 with non-approved X25519, so half of it is off the list.

The problem is that browsers, Go, and OpenSSL all default to X25519MLKEM768, not the approved SecP256r1MLKEM768, because X25519 is fast and ubiquitous outside the FIPS world. Both hybrids are specified in the same IETF draft for ECDHE-MLKEM key exchange. So a FIPS switch that strips X25519 also strips the post-quantum hybrid built on it, unless the stack special-cases the hybrid. That is exactly the fork we measured. Go over-restricts: it drops every post-quantum group and reverts to classical, which is safe for compliance but throws away quantum resistance. OpenSSL under-restricts: it keeps the non-approved hybrid live, gating it through a FIPS approval indicator rather than blocking it, a design tracked in openssl/openssl #27061. Neither behavior is a bug in the usual sense; both are defensible library choices that produce a non-obvious result once you actually look at the wire. The Go behavior is discussed in golang/go #78178 and #78298, and OpenSSL 3.5's post-quantum support arrived in its April 2025 release.

What to do if you run FIPS and care about harvest-now-decrypt-later

The whole point of a post-quantum hybrid is to defeat a "harvest now, decrypt later" adversary, one who records your TLS traffic today and decrypts it once a quantum computer exists. A FIPS switch that silently removes that protection defeats the point for the organizations most likely to be targeted. If you run FIPS, do not assume the toggle handled it.

What this does not prove

A result without its limits is marketing, so here are the boundaries on this one.

Frequently asked questions

Does enabling FIPS mode disable post-quantum TLS?

It can, and in our lab it did. With Go 1.24 under GODEBUG=fips140=on or fips140=only, the ClientHello dropped every post-quantum group and the connection negotiated classical P-256, so a handshake that succeeded had zero quantum-resistant key exchange. OpenSSL 3.5.2 did the opposite: its FIPS provider still offered and negotiated X25519MLKEM768, whose X25519 half is not FIPS-approved. Neither stack offered the FIPS-approved post-quantum group by default.

Is X25519 FIPS-approved?

No. X25519 is not an approved key-agreement scheme under NIST SP 800-56A, so a strict FIPS module treats it as non-approved. We confirmed it directly: under the OpenSSL 3.5.2 FIPS provider, standalone X25519 key generation failed as unsupported with a non-zero exit code, while the same operation succeeded under the default provider.

Is X25519MLKEM768 FIPS-approved?

Not cleanly. It is a hybrid: the ML-KEM-768 half is FIPS 203 approved, but the X25519 half is not approved for key agreement. OpenSSL's FIPS provider does not block the hybrid; it gates it behind a FIPS approval indicator and still offers and negotiates it by default. The group works under FIPS, but its classical half is off the approved list.

What is the FIPS-approved post-quantum TLS group?

The widely specified FIPS-legal hybrid is SecP256r1MLKEM768: NIST P-256, an SP 800-56A approved curve, paired with ML-KEM-768 from FIPS 203. Both halves are approved. In our lab, forcing SecP256r1MLKEM768 with both peers in FIPS mode produced a successful handshake, but neither Go 1.24 nor OpenSSL 3.5.2 offered it by default.

Why does my Go TLS handshake change under GODEBUG=fips140?

Because Go 1.24's FIPS mode restricts the offered curves to the NIST set. Under fips140=on or fips140=only, the client ClientHello we captured advertised only secp256r1, secp384r1, and secp521r1, with no x25519 and no post-quantum hybrid, and negotiated classical P-256. A Go server in FIPS mode sent a HelloRetryRequest to force a peer down to secp256r1. The handshake still succeeds, so the downgrade is silent unless you inspect the negotiated group.

Does FIPS break harvest-now-decrypt-later protection?

In the default configs we tested, yes, in the way that counts. Go in FIPS mode removed all post-quantum key exchange and reverted to classical crypto, exactly the traffic a harvest-now-decrypt-later adversary wants to record and break later. OpenSSL kept post-quantum key exchange but through a hybrid whose classical half is not FIPS-approved, a compliance problem rather than a cryptographic one. Either way, turning on FIPS did not deliver compliant post-quantum protection by default.

Related reading

Running FIPS and assuming it handled post-quantum?

Our $100 check reads your real crypto posture the way an attacker records it, on scope you have verified you own and authorized in writing, with a senior operator on the readout. What your services actually negotiate on the wire is part of that surface.

Book a $100 check