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
-
Primary — compile failure on native-FIPS toolchains.
fips.go(build tagfips) importscrypto/boringunconditionally. In the standard library,crypto/boringis constrained//go:build boringcrypto, which is only satisfied underGOEXPERIMENT=boringcrypto. On an upstream Go toolchain usingGOFIPS140(no boringcrypto experiment), any build with-tags fipsfails to compile:imports crypto/boring: build constraints exclude all Go files in .../src/crypto/boringThis blocks native-FIPS builds of every consumer that builds with
-tags fips(gitlab-shell, workhorse, gitaly, gitlab-pages, ...). -
Secondary — wrong runtime answer. Even where
crypto/boringis buildable (e.g. the currentgolang-fipsCI images, which relax the constraint),boring.Enabled()is alwaysfalseunder the native module.Enabled()would misreport FIPS as disabled — gitlab-shell selects its SSH algorithm sets viafips.Enabled(), so this is a functional break — andCheck()logs a misleading "Binary was compiled with FIPS mode, but an external SSL library was not enabled".
The fix
- Isolate the
crypto/boringprobe behind theboringcryptobuild tag: newfips/boring.go(//go:build fips && boringcrypto) returnsboring.Enabled(); newfips/notboring.go(//go:build fips && !boringcrypto) returnsfalse. Thefipstag no longer requirescrypto/boringto be buildable. Enabled()now returnsboringEnabled() || fips140.Enabled().crypto/fips140is available since Go 1.24; the root module'sgodirective 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 newTestEnabledNativeModulethat 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 issuesIn 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.064sUpstream 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
- Work item: https://gitlab.com/gitlab-org/distribution/team-tasks/-/work_items/1706
- CNG migration MR: gitlab-org/build/CNG!3010 (merged)
- Go FIPS 140-3 docs: https://go.dev/doc/security/fips140
- CMVP certificate 5247: https://csrc.nist.gov/projects/cryptographic-module-validation-program/certificate/5247
Related to #129 (closed)