feat(mode): Add gofips140 mode
What type of PR is this?
Feature
What does this PR do? Why is it needed?
This PR adds support for the GOFIPS140 environment variable can be used with go build, go install, and go test to select the version of the Go Cryptographic Module to be linked into the executable program.
See: https://go.dev/doc/security/fips140
Which issues(s) does this PR fix?
Fixes #4293
Other notes for review
None.
Trying to get the tests working, will mark as Ready for Review once those are passing.
Hi, a gentle ping on this PR -- I'm wondering how we can get this merged. It would be great to have support for FIPS mode in rules_go. There are workarounds like using --action_env, but it unnecessarily busts the cache on non-Go code.
This generally looks good to me, we just have to get CI green. You may have to configure a more recent SDK foot the integration test.
Having spent some time trying to get this working, it seems at least the following patches are required if you are using GOFIPS140=v1.0.0:
diff --git a/go/private/BUILD.sdk.bazel b/go/private/BUILD.sdk.bazel
index 686b2c62..d6fca19e 100644
--- a/go/private/BUILD.sdk.bazel
+++ b/go/private/BUILD.sdk.bazel
@@ -24,7 +24,10 @@ filegroup(
filegroup(
name = "srcs",
srcs = glob(
- ["src/**/*"],
+ [
+ "lib/fips140/**",
+ "src/**/*",
+ ],
exclude = [
"src/**/*_test.go",
"src/**/testdata/**",
diff --git a/go/tools/builders/stdlib.go b/go/tools/builders/stdlib.go
index cfcb9910..326a2d08 100644
--- a/go/tools/builders/stdlib.go
+++ b/go/tools/builders/stdlib.go
@@ -58,7 +58,7 @@ You may need to use the flags --cpu=x64_windows --compiler=mingw-gcc.`)
}
// Link in the bare minimum needed to the new GOROOT
- if err := replicate(goroot, output, replicatePaths("src", "pkg/tool", "pkg/include")); err != nil {
+ if err := replicate(goroot, output, replicatePaths("src", "pkg/tool", "pkg/include", "lib")); err != nil {
return err
}
That doesn't seem to be entirely sufficient, and the build now fails for me with the error go: module cache not found: neither GOMODCACHE nor GOPATH is set. I'm still looking into that.
Still looking into the GOFIPS140=v1.0.0 use case. These patches seem to get the stdlib building (though I'm not sure if using a temporary GOMODCACHE is the correct thing to do in this case), but then you get a bunch of errors that look like cannot find package crypto/internal/fips140/v1.0.0-c2097c7c/aes (using -importcfg) -- I can't quite tell what the problem is there.
diff --git a/go/private/BUILD.sdk.bazel b/go/private/BUILD.sdk.bazel
index 686b2c62..d6fca19e 100644
--- a/go/private/BUILD.sdk.bazel
+++ b/go/private/BUILD.sdk.bazel
@@ -24,7 +24,10 @@ filegroup(
filegroup(
name = "srcs",
srcs = glob(
- ["src/**/*"],
+ [
+ "lib/fips140/**",
+ "src/**/*",
+ ],
exclude = [
"src/**/*_test.go",
"src/**/testdata/**",
diff --git a/go/tools/builders/stdlib.go b/go/tools/builders/stdlib.go
index cfcb9910..ad79fa74 100644
--- a/go/tools/builders/stdlib.go
+++ b/go/tools/builders/stdlib.go
@@ -58,7 +58,7 @@ You may need to use the flags --cpu=x64_windows --compiler=mingw-gcc.`)
}
// Link in the bare minimum needed to the new GOROOT
- if err := replicate(goroot, output, replicatePaths("src", "pkg/tool", "pkg/include")); err != nil {
+ if err := replicate(goroot, output, replicatePaths("src", "pkg/tool", "pkg/include", "lib")); err != nil {
return err
}
@@ -75,6 +75,11 @@ You may need to use the flags --cpu=x64_windows --compiler=mingw-gcc.`)
cachePath := filepath.Join(output, ".gocache")
os.Setenv("GOCACHE", cachePath)
defer os.RemoveAll(cachePath)
+ // Create a temporary modcache directory.
+ modCachePath := filepath.Join(output, ".gomodcache")
+ os.Setenv("GOMODCACHE", modCachePath)
+ defer os.RemoveAll(modCachePath)
+
// Disable modules for the 'go install' command. Depending on the sandboxing
// mode, there may be a go.mod file in a parent directory which will turn
I'm no expert and am slightly out of my depth here, so it's very possible I'm missing something. But I think supporting GOFIPS140 values besides latest is going to be challenging and may require some more heavy lifting. You do need to apply the patches I provided above to expose the .zip file containing the FIPS libraries, but then that is not sufficient.
- In the Go toolchain, first the lookup to find the
.zipcontaining the FIPS binary occurs; and then - the toolchain will unpack the zip into the
GOMODCACHE.
On the rules_go side, I believe what needs to happen is that we need to separately compile these .go files and capture them in the importcfg that we're passing down to go tool compile.
I hope I'm misreading the situation and it's much more straightforward than I think it is :)
Could we limit support to latest for now then with this PR? Is that already a useful mode for companies that need FIPS?
I wonder instead if fips40 should be an attribute on the toolchain rather than an attribute on go_binary. The go_sdk module extension could let users pick which version they want, if not latest, and we could build that as part of GoStdLib.
Could we limit support to latest for now then with this PR?
Unfortunately I also see that building with GOFIPS140=latest does not work with this PR; while the build succeeds, the resultant binary does not have FIPS mode enabled (i.e. crypto/fips140.Enabled() returns false). I have not been able to figure out why. Setting GOFIPS140=latest in the environment when building the stdlib, running go tool compile, and then running go tool link does not seem to be sufficient to get the desired behavior of GOFIPS140=latest.
This gist captures my current version that I've been iterating on, derived from the original code in this PR. (It attempts to integrate jayconrod's advice to associate this configuration with the toolchain rather than the binary or test binary.) When attempting to build with --@io_bazel_rules_go//go/config:gofips140=latest, I see that the GoStdlib, GoCompilePkg, and GoLink all correctly run with GOFIPS140=latest, but still the result binary has crypto/fips140.Enabled() == false.
- Setting the flag does something. I can tell because building the
GoStdlibfails if you set the config to a nonsense string like@io_bazel_rules_go//go/config:gofips140=asdflkjsf(invalid GOFIPS140error). - If you build with
@io_bazel_rules_go//go/config:gofips140=latest, the build does "work", but I can't tell if the resultGoStdlibis correct or not.
I found that the following diff is necessary to support the GOFIPS140=latest case:
diff --git a/go/tools/builders/link.go b/go/tools/builders/link.go
index 11dc0abf..3a94167d 100644
--- a/go/tools/builders/link.go
+++ b/go/tools/builders/link.go
@@ -136,6 +136,11 @@ func link(args []string) error {
}
}
+ gofips140 := os.Getenv("GOFIPS140")
+ if gofips140 != "off" {
+ goargs = append(goargs, "-X", "runtime.godebugDefault=fips140=on")
+ }
+
if *buildmode != "" {
goargs = append(goargs, "-buildmode", *buildmode)
}
This ensures that the binary defaults to GODEBUG=fips140=on, which is the correct behavior.
GOFIPS140=v1.0.0 is hairier. Eventually, the build will fail with the following error:
cannot find package crypto/internal/fips140/v1.0.0-c2097c7c (using -importcfg)
cannot find package crypto/internal/fips140/v1.0.0-c2097c7c/check (using -importcfg)
The problem is that some of the built crypto packages (I believe, crypto/internal/fips140 and its subpackages) are "replaced" transparently by a versioned package like crypto/internal/fips140/v1.0.0-c2097c7c. This breaks a prior assumption in rules_go: that each package in the Go SDK has a simple, one-to-one mapping with its package name. This means that the logic used to compute the importcfg and/or the package_list needs to be augmented to include the real names of all stdlib packages, with their real corresponding .a files.
One option could be to use the gcexportdata package to read all the .a files from the stdlib and get their "real" package names, but it's not a low-touch change. For one, I'm not even sure where the .a archives for crypto/internal/fips140/v1.0.0-c2097c7c end up right now.
Edit: this gist captures my latest version of this, which supports GOFIPS140=latest but not GOFIPS140=v1.0.0.