skip to content

Why did FIPS builds need GOEXPERIMENT=boringcrypto before Go 1.24, and what changed after it?

level: middleimportance: nice to knowfreq 30%

answer

  1. the old way pulled in C
  2. cgo, and only a narrow platform set
  3. an unsupported toolchain experiment
  4. the new one ships inside the toolchain
  5. a build setting, not an experiment

basics

~20 s

Before Go 1.24 a FIPS build meant GOEXPERIMENT=boringcrypto, which reached a C cryptographic library through cgo and worked on only a couple of Linux platforms. Go 1.24 shipped a pure-Go Go Cryptographic Module selected by the GOFIPS140 build setting.

solid answer

~40 s

`GOEXPERIMENT=boringcrypto` swapped parts of Go's cryptography for a C library reached through cgo. That meant `CGO_ENABLED=1`, a C toolchain in every build, support on only a narrow set of platforms, and the loss of the pure-Go cross-compile and dependency-free static binary that most Go release pipelines are built around. It was also a `GOEXPERIMENT`, a setting the toolchain documents as unsupported outside its own development. Since Go 1.24 the FIPS 140-3 Go Cryptographic Module is written in Go and ships inside the toolchain, selected by the `GOFIPS140` build setting and switched at run time by `GODEBUG=fips140`. No cgo, no C dependency, the same cross-compilation story as any other Go build, and the mode is recorded in the binary's build settings. The old path still exists; for new work `GOFIPS140` is the supported one.

go deeper

for a junior

Know that FIPS support is now built into the Go toolchain and selected with an environment variable at build time, rather than needing a special toolchain build or a C library.

for a middle

Explain the concrete costs of the old path — cgo, a C toolchain, a narrow platform set, an unsupported experiment — and which of them the in-tree module removes.

for a senior

Talk about the migration as pipeline work: what leaves the builder, what build metadata starts appearing in the artefact, and what new run-time failures strict mode introduces.

for a principal

Weigh the supportability argument: pinning a release pipeline to a documented build setting versus to a knob the toolchain says may change arbitrarily between releases.

## Where FIPS builds came from For most of Go's life there was no in-tree answer for teams that had to demonstrate their cryptography came from a validated module. The workaround was `GOEXPERIMENT=boringcrypto`: a build of the toolchain that replaced parts of `crypto/*` with calls into a C cryptographic library derived from BoringSSL. It worked, and regulated Go shops used it for years, but it cost a lot. ### What the old path cost **It required cgo.** Reaching a C library from Go means `CGO_ENABLED=1` and a C toolchain present wherever you build. For a language whose release story is "one static binary, cross-compiled from anywhere", that is a large change to the pipeline: builders now need a C compiler, and cross-compiling to another OS or architecture needs a cross C toolchain rather than just `GOOS`/`GOARCH`. **It was platform-limited.** The supported combinations were a narrow slice of Linux, so a fleet that also ran on other platforms could not build the same way everywhere. **It was a GOEXPERIMENT.** The `GOEXPERIMENT` variable is documented as provided for development and testing of the Go toolchain itself, with use beyond that explicitly unsupported. Building your regulated production artefact on an unsupported knob is not a comfortable position to defend in a review. **TLS restriction was a separate opt-in.** Constraining the TLS stack to approved algorithms was a distinct step rather than a consequence of one mode. ### What Go 1.24 changed Go 1.24 introduced the **Go Cryptographic Module** — the FIPS 140-3 implementation living under `crypto/internal/fips140`, written in ordinary Go and shipped inside the toolchain. Two controls replace the old experiment: - `GOFIPS140` at build time, which selects which snapshot of that module is compiled in and stamps `fips140=on` as the binary's default GODEBUG. Its default is `off`. - `GODEBUG=fips140=on` or `=only` at run time, which decides whether the binary merely runs through the module or additionally rejects non-approved standard-library cryptography. Because the module is Go code, none of the old costs apply. `CGO_ENABLED=0` still works. Cross-compilation is the same `GOOS`/`GOARCH` story as any other Go build. The resulting binary has no external library to match at run time. The toolchain also lays the module's code and data out contiguously so that the linker can hash them and the binary can re-verify itself at startup — an integrity check that the C-library approach handled inside the library. ### The build metadata angle The practical difference an engineer notices is what ends up in the artefact. A `GOFIPS140` build records `GOFIPS140=<version>` and `DefaultGODEBUG=fips140=on` in the binary's build settings, readable with `go version -m` or `runtime/debug.ReadBuildInfo`. That is a much better answer to "prove this artefact was built in FIPS mode" than "we set an environment variable on a builder six weeks ago". ### What has not changed Neither approach makes your deployment certified by itself. Both give you cryptography from a module that has been through a validation process; the regime around that — which version is covered, what the security policy requires of an operator — sits outside the toolchain. And neither constrains code you wrote yourself or vendored: a hand-rolled cipher compiled into the same binary is unaffected by any of these switches. ### Migrating If a service still builds with the old experiment, the move is mostly pipeline work: drop `CGO_ENABLED=1` and the C toolchain from the builder, set `GOFIPS140` to a pinned snapshot version instead, and then — this is the part teams underestimate — exercise the service under `GODEBUG=fips140=only` before promoting it, because strict enforcement rejects standard-library calls the older path tolerated, and it rejects them at the call site, at run time.

  • Does the Go Cryptographic Module require cgo or a C library at run time?
    No. It is ordinary Go code compiled into your binary, so a `GOFIPS140` build still cross-compiles with `GOOS`/`GOARCH` and still produces a binary with no external library to match on the host. That is the main practical difference from the older boringcrypto path, which needed `CGO_ENABLED=1` and a C toolchain wherever you built.
  • If a service still builds with GOEXPERIMENT=boringcrypto, what changes in the release pipeline when it moves to GOFIPS140?
    The builder stops needing a C toolchain and a narrow platform, so the service can cross-compile like the rest of the fleet. The recorded build metadata changes: `go version -m` now shows `GOFIPS140` and `DefaultGODEBUG=fips140=on`. And there is a new gate to add — run the tests under `GODEBUG=fips140=only`, because strict enforcement rejects standard-library calls the old path allowed.
  • Why is building production artefacts with a GOEXPERIMENT setting a concern in its own right?
    The go command documents GOEXPERIMENT as provided for development and testing of the Go toolchain itself, with use beyond that unsupported, and it says the list of experiments may change arbitrarily over time. A toolchain upgrade can therefore change or remove the behaviour your release depends on with no compatibility promise. `GOFIPS140` is a documented build setting with defined values, which is a much safer thing to pin a pipeline to.

saying these in an interview costs you the question

  • Thinks boringcrypto is still the recommended FIPS path
  • Believes the Go Cryptographic Module needs cgo
  • Treats a GOEXPERIMENT as a supported production setting
  • Claims FIPS builds cannot be cross-compiled
  • Assumes either approach certifies the deployment