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

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.
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.
| Stack | Role | FIPS mode | Groups offered | Negotiated group | Outcome |
|---|---|---|---|---|---|
| Go 1.24 | Client | fips140=on / =only | secp256r1, secp384r1, secp521r1 | secp256r1 | Classical only, no post-quantum |
| Go 1.24 | Server | fips on | NIST curves; sends HelloRetryRequest | secp256r1 | Forces peer down to classical |
| OpenSSL 3.5.2 | Client | fips on | X25519MLKEM768, secp256r1/384/521, ffdhe2048/3072 | X25519MLKEM768 | Post-quantum, but non-approved hybrid |
| OpenSSL 3.5.2 | Server | fips on | Accepts X25519MLKEM768 | X25519MLKEM768 | Post-quantum, but non-approved hybrid |
| OpenSSL 3.5.2 | Both, forced | fips on | X25519MLKEM768 (forced) | X25519MLKEM768 | Handshake succeeds under FIPS |
| OpenSSL 3.5.2 | Both, forced | fips on | SecP256r1MLKEM768 (forced) | SecP256r1MLKEM768 | Approved 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:
- The wire itself. TLS 1.3 supported_groups and key_share are cleartext, so we read the exact group code points off the handshake. X25519MLKEM768 is 0x11ec, SecP256r1MLKEM768 is 0x11eb, x25519 is 0x001d, and secp256r1 (P-256) is 0x0017. What a client offered and what it negotiated came straight from those bytes.
- OpenSSL's own self-report. The OpenSSL client also prints the "Negotiated TLS1.3 group," which agreed with what we read on the wire in every case.
- Proof that FIPS was genuinely engaged. Under the OpenSSL FIPS configuration, only the base and fips providers were loaded, with no default provider. Standalone X25519 key generation failed as unsupported with a non-zero exit code, while the same operation succeeded under the default provider, and ML-KEM-768 key generation succeeded under FIPS. That is the module behaving exactly as a real FIPS module should: X25519 non-approved and refused on its own, ML-KEM from FIPS 203 approved and allowed.
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.
- Configure SecP256r1MLKEM768 explicitly. It is the FIPS-legal hybrid, both halves approved, and in our lab it handshakes cleanly with both peers in FIPS mode. But neither Go 1.24 nor OpenSSL 3.5 offers it by default, so you have to name it in your group list yourself.
- Verify on the wire, do not trust the toggle. Capture a real handshake and read the negotiated group. "FIPS on" told us nothing useful about the actual key exchange in either stack; the bytes did.
- In Go, know that
GODEBUG=fips140removes X25519MLKEM768. If you set FIPS mode and did nothing else, your Go services are negotiating classical P-256 with no post-quantum key exchange. Test your actual negotiated group before you assume otherwise. - Decide which risk you are managing. If strict FIPS approval is the binding requirement, the OpenSSL default hybrid is a compliance gap. If quantum resistance is the requirement, the Go default is the gap. SecP256r1MLKEM768 is the configuration that satisfies both.
What this does not prove
A result without its limits is marketing, so here are the boundaries on this one.
- Single library versions. This is Go 1.24 and OpenSSL 3.5.2. Defaults, provider policies, and approval indicators can and do change across versions, so a newer release may behave differently.
- Our configuration. We measured specific FIPS configurations we set up. A different FIPS module, build flag, or policy file could offer a different group set.
- Defaults, not capability. Both stacks can negotiate the approved hybrid when told to. The finding is about what they do out of the box, not what they are able to do.
- One transport. This is TLS 1.3 key exchange. It says nothing about signatures, certificates, or other protocols in your stack.
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
- What the certs prove. Reading TLS assurances off the wire instead of trusting the label on the box.
- How many top domains restrict certificate issuance? The same passive, our-own-data method applied to the public certificate ecosystem.
- Remote MCP server security. Another measurement of a default that most teams adopt faster than they audit.
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