fix: support Go's native FIPS 140-3 module in fips package

What does this MR do?

Makes the fips package aware of upstream Go's native FIPS 140-3 module (crypto/fips140, selected via GOFIPS140; module v1.0.0 holds CMVP certificate 5247), in addition to the existing golang-fips/BoringCrypto backend.

GitLab is migrating FIPS builds from the golang-fips toolchain (OpenSSL via CGO, GOEXPERIMENT=boringcrypto) to the native module — see CNG MR !3010 and work item gitlab-org/distribution/team-tasks#1706 (in particular this note).

The problem

  1. Primary — compile failure on native-FIPS toolchains. fips.go (build tag fips) imports crypto/boring unconditionally. In the standard library, crypto/boring is constrained //go:build boringcrypto, which is only satisfied under GOEXPERIMENT=boringcrypto. On an upstream Go toolchain using GOFIPS140 (no boringcrypto experiment), any build with -tags fips fails to compile:

    imports crypto/boring: build constraints exclude all Go files in .../src/crypto/boring

    This blocks native-FIPS builds of every consumer that builds with -tags fips (gitlab-shell, workhorse, gitaly, gitlab-pages, ...).

  2. Secondary — wrong runtime answer. Even where crypto/boring is buildable (e.g. the current golang-fips CI images, which relax the constraint), boring.Enabled() is always false under the native module. Enabled() would misreport FIPS as disabled — gitlab-shell selects its SSH algorithm sets via fips.Enabled(), so this is a functional break — and Check() logs a misleading "Binary was compiled with FIPS mode, but an external SSL library was not enabled".

The fix

  • Isolate the crypto/boring probe behind the boringcrypto build tag: new fips/boring.go (//go:build fips && boringcrypto) returns boring.Enabled(); new fips/notboring.go (//go:build fips && !boringcrypto) returns false. The fips tag no longer requires crypto/boring to be buildable.
  • Enabled() now returns boringEnabled() || fips140.Enabled(). crypto/fips140 is available since Go 1.24; the root module's go directive already requires 1.25.8.
  • Check() now logs which backend is active: external SSL library (BoringCrypto-compatible), native Go Cryptographic Module, or neither.
  • Tests updated: an invariant test (Enabled() == boringEnabled() || fips140.Enabled()), the kernel-FIPS-flag correlation test (skipped when the native module is active, or when upstream static BoringCrypto is enabled independently of the kernel flag), and a new TestEnabledNativeModule that discriminates the native-module case (boringEnabled() false, Enabled() true).

Non-goal

fips/notfips.go (built without the fips tag) intentionally still returns false from Enabled()/Check() even when a binary is built with GOFIPS140 but without -tags fips. Returning false without the tag is the existing semantic contract of this package and is left unchanged; consumers opting into FIPS behavior must keep building with -tags fips. fips/ssh.go is untouched.

Backwards compatibility

Legacy golang-fips/OpenSSL builds set GOEXPERIMENT=boringcrypto, which satisfies the boringcrypto build tag, so those builds keep the exact boring.Enabled() probe they have today (verified empirically below).

Empirical testing

All on go1.26.4 (mise/.tool-versions) unless noted.

Master reproduces the compile failure (vanilla upstream toolchain, -tags fips):

$ go test -v -tags fips ./fips/...
package gitlab.com/gitlab-org/labkit/fips
        imports crypto/boring: build constraints exclude all Go files in .../go/1.26.4/src/crypto/boring
FAIL    gitlab.com/gitlab-org/labkit/fips [setup failed]

With this MR:

$ go build ./... && go test -short ./fips/...          # no tag — unchanged
ok      gitlab.com/gitlab-org/labkit/fips

$ go test -count=1 -tags fips ./fips/...               # vanilla toolchain: now compiles
ok      gitlab.com/gitlab-org/labkit/fips       0.319s
  --- PASS: TestEnabled
  --- PASS: TestEnabledKernelFIPSMode
  --- SKIP: TestEnabledNativeModule (rebuild with GOFIPS140=v1.0.0)

$ GOFIPS140=v1.0.0 go test -count=1 -tags fips ./fips/...   # native module active
ok      gitlab.com/gitlab-org/labkit/fips       0.246s
  --- PASS: TestEnabled
  --- SKIP: TestEnabledKernelFIPSMode (native module enabled independently of kernel FIPS mode)
  --- PASS: TestEnabledNativeModule            # boringEnabled()==false, Enabled()==true

$ GOEXPERIMENT=boringcrypto go build -tags fips ./fips/...  # boring probe still compiles
$ GOEXPERIMENT=boringcrypto go vet -tags fips ./fips/...
$ go vet ./... && ./scripts/golangci-lint.sh                # 0 issues (both modules)
$ golangci-lint run --build-tags fips ./fips/...            # 0 issues

In the test-fips CI image (registry.gitlab.com/gitlab-com/gl-infra/common-ci-tasks-images/golang-fips:1.26, linux/amd64):

$ go test -count=1 -v -tags fips -run TestEnabled .
--- PASS: TestEnabled
--- PASS: TestEnabledKernelFIPSMode
--- SKIP: TestEnabledNativeModule
ok      gitlab.com/gitlab-org/labkit/fips       0.060s

$ GOFIPS140=v1.0.0 go test -count=1 -v -tags fips -run TestEnabled .
--- PASS: TestEnabled
--- SKIP: TestEnabledKernelFIPSMode
--- PASS: TestEnabledNativeModule
ok      gitlab.com/gitlab-org/labkit/fips       0.064s

Upstream Go + GOEXPERIMENT=boringcrypto on linux/amd64 (boringcrypto tag set; static BoringCrypto active): TestEnabled passes with Enabled()==true via the boring.Enabled() path, confirming the fips && boringcrypto probe file is selected.

References

🤖 Generated with Claude Code

Related to #129 (closed)

Edited by Jason Plum

Merge request reports

Loading
Loading