quic-go is ready for FIPS 140-3

Starting with quic-go v0.60.0, quic-go is ready for use with Go’s native FIPS 140-3 mode.

This does not mean that quic-go is a FIPS 140-3 validated cryptographic module. It is not, and it doesn’t try to be one. Instead, quic-go relies on the Go Cryptographic Module and the Go standard library’s cryptography packages. What changed in v0.60.0 is that the QUIC-specific cryptographic operations in quic-go now stay on standard library primitives when Go’s FIPS 140-3 mode is active.

This work is the result of a long series of pull requests (all linked in the tracking issue quic-go#5077), and the final state is documented in the new FIPS 140-3 document in the quic-go repository.

What Go Provides

Starting with Go 1.24, Go binaries can operate in a mode that facilitates FIPS 140-3 compliance. Go 1.26 takes this further by providing the Go Cryptographic Module v1.26.0, including the AES-GCM functionality that made the QUIC integration possible.

For most Go applications, this happens mostly behind the scenes. The public crypto/* packages transparently use the Go Cryptographic Module for FIPS-approved algorithms, and crypto/tls restricts TLS negotiation to approved protocol versions, cipher suites, signature algorithms, and key exchange mechanisms.

Applications still need to enable FIPS 140-3 mode themselves. Build with GOFIPS140=latest, or with a specific module version such as GOFIPS140=v1.26.0, to make FIPS mode the default for the binary. Alternatively, set GODEBUG=fips140=on when starting the program. GODEBUG=fips140=only is the stricter testing mode and is not recommended for production use.

QUIC is a little different. QUIC uses TLS 1.3 for the handshake, certificate handling, cipher suite selection, and the TLS key schedule, so all of that is delegated to crypto/tls. However, QUIC does not use TLS records. The QUIC stack itself protects packets using keys derived by TLS. This means a QUIC implementation has a few cryptographic operations of its own that need to be reviewed carefully.

Packet Protection

TLS 1.3 authenticates the endpoints and produces traffic secrets. QUIC uses those secrets to derive packet protection keys and IVs, and then encrypts Handshake, 0-RTT, and 1-RTT packets itself.

This is the main FIPS-relevant operation inside quic-go. While QUIC also uses AES for Initial packets, we don’t need to worry about that when it comes to FIPS 140-3 compliance (see below).

For AES-GCM, quic-go now constructs packet protection AEADs through the Go standard library’s TLS 1.3 AES-GCM implementation. The QUIC nonce construction itself does not change: RFC 9001 derives a packet protection IV and XORs it with the packet number. The problem with the old code path was that it used the general-purpose cipher.NewGCM API, where the caller supplies a nonce for every operation. Go’s FIPS 140-3 mode rejects that API for AES-GCM packet protection. quic-go needs to enter the standard library’s constrained TLS-style AES-GCM path instead.

There is one ugly part left here. The Go standard library has the internal functionality we need, but it does not yet expose the QUIC-specific constructor as a public API. Until golang/go#79219 is resolved, quic-go uses go:linkname to reach the unexported crypto/tls.aeadAESGCMTLS13 function. go:linkname lets one package bind a local declaration to a symbol in another package, bypassing normal Go visibility rules. This is hacky, and the Go team understandably dislikes when projects use it. The standard library has a few “hall of shame” comments for packages that depend on unexported internals this way. In this case, however, there is no public API (yet) that lets a QUIC implementation construct the required FIPS-compatible AEAD.

Header Protection

QUIC header protection is separate from packet protection. It masks parts of the packet header: selected bits in the first byte, and most importantly the packet number, so an on-path observer cannot read the packet number.

For Handshake, 0-RTT, and 1-RTT packets, RFC 9001 derives this header protection key from the TLS traffic secret using HKDF-Expand-Label. For AES cipher suites, the header protection algorithm then uses a single AES block operation to produce the mask. In quic-go, this is implemented entirely with standard library functionality: crypto/hkdf for the derivation and crypto/aes for the AES block operation.

What We Excluded

Not every cryptographic-looking operation in QUIC is relevant to FIPS 140-3.

Initial packet protection is one example. RFC 9001 applies the regular packet protection process to Initial packets, using AES-GCM keys derived from a version-specific salt and the packet’s destination connection ID. This lets QUIC use the same protected-packet machinery before the TLS handshake has produced traffic secrets, limits blind packet injection, and makes it a little harder for middleboxes to parse and depend on handshake details. In Go 1.26 FIPS 140-3 mode, quic-go disables FIPS enforcement around Initial packet construction.

The Retry packet integrity tag is another example. RFC 9001 defines it using fixed keys and nonces. It protects against accidental corruption and casual injection, but it does not encrypt packet contents and is not used to protect application data. quic-go treats this as outside the FIPS 140 scope and disables FIPS enforcement around that AEAD construction as well.

What This Means for Users

Starting with quic-go v0.60.0, applications built with Go 1.26 or newer can use quic-go with Go’s native FIPS 140-3 mode. Older Go versions continue to work as before, but quic-go does not attempt to meet FIPS 140 requirements there.

FIPS 140-3 compliance is ultimately about the complete application, the Go toolchain and cryptographic module version used to build it, and the target operating environment. quic-go’s job is to make sure it doesn’t force applications off the standard library cryptographic path for QUIC. As of v0.60.0, that part is in place.