| // Copyright 2024 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package fips implements support for the GOFIPS140 build setting. |
| // |
| // The GOFIPS140 build setting controls two aspects of the build: |
| // |
| // - Whether binaries are built to default to running in FIPS-140 mode, |
| // meaning whether they default to GODEBUG=fips140=on or =off. |
| // |
| // - Which copy of the crypto/internal/fips140 source code to use. |
| // The default is obviously GOROOT/src/crypto/internal/fips140, |
| // but earlier snapshots that have differing levels of external |
| // validation and certification are stored in GOROOT/lib/fips140 |
| // and can be substituted into the build instead. |
| // |
| // This package provides the logic needed by the rest of the go command |
| // to make those decisions and implement the resulting policy. |
| // |
| // [Init] must be called to initialize the FIPS logic. It may fail and |
| // call base.Fatalf. |
| // |
| // When GOFIPS140=off, [Enabled] returns false, and the build is |
| // unchanged from its usual behaviors. |
| // |
| // When GOFIPS140 is anything else, [Enabled] returns true, and the build |
| // sets the default GODEBUG to include fips140=on. This will make |
| // binaries change their behavior at runtime to confirm to various |
| // FIPS-140 details. [cmd/go/internal/load.defaultGODEBUG] calls |
| // [fips.Enabled] when preparing the default settings. |
| // |
| // For all builds, FIPS code and data is laid out in contiguous regions |
| // that are conceptually concatenated into a "fips object file" that the |
| // linker hashes and then binaries can re-hash at startup to detect |
| // corruption of those symbols. When [Enabled] is true, the link step |
| // passes -fipso={a.Objdir}/fips.o to the linker to save a copy of the |
| // fips.o file. Since the first build target always uses a.Objdir set to |
| // $WORK/b001, a build like |
| // |
| // GOFIPS140=latest go build -work my/binary |
| // |
| // will leave fips.o behind in $WORK/b001 |
| // (unless the build result is cached, of course). |
| // |
| // When GOFIPS140 is set to something besides off and latest, [Snapshot] |
| // returns true, indicating that the build should replace the latest copy |
| // of crypto/internal/fips140 with an earlier snapshot. The reason to do |
| // this is to use a copy that has been through additional lab validation |
| // (an "in-process" module) or NIST certification (a "certified" module). |
| // The snapshots are stored in GOROOT/lib/fips140 in module zip form. |
| // When a snapshot is being used, Init unpacks it into the module cache |
| // and then uses that directory as the source location. |
| // |
| // A FIPS snapshot like v1.2.3 is integrated into the build in two different ways. |
| // |
| // First, the snapshot's fips140 directory replaces crypto/internal/fips140 |
| // using fsys.Bind. The effect is to appear to have deleted crypto/internal/fips140 |
| // and everything below it, replacing it with the single subdirectory |
| // crypto/internal/fips140/v1.2.3, which now has the FIPS packages. |
| // This virtual file system replacement makes patterns like std and crypto... |
| // automatically see the snapshot packages instead of the original packages |
| // as they walk GOROOT/src/crypto/internal/fips140. |
| // |
| // Second, ResolveImport is called to resolve an import like crypto/internal/fips140/sha256. |
| // When snapshot v1.2.3 is being used, ResolveImport translates that path to |
| // crypto/internal/fips140/v1.2.3/sha256 and returns the actual source directory |
| // in the unpacked snapshot. Using the actual directory instead of the |
| // virtual directory GOROOT/src/crypto/internal/fips140/v1.2.3 makes sure |
| // that other tools using go list -json output can find the sources, |
| // as well as making sure builds have a real directory in which to run the |
| // assembler, compiler, and so on. The translation of the import path happens |
| // in the same code that handles mapping golang.org/x/mod to |
| // cmd/vendor/golang.org/x/mod when building commands. |
| // |
| // It is not strictly required to include v1.2.3 in the import path when using |
| // a snapshot - we could make things work without doing that - but including |
| // the v1.2.3 gives a different version of the code a different name, which is |
| // always a good general rule. In particular, it will mean that govulncheck need |
| // not have any special cases for crypto/internal/fips140 at all. The reports simply |
| // need to list the relevant symbols in a given Go version. (For example, if a bug |
| // is only in the in-tree copy but not the snapshots, it doesn't list the snapshot |
| // symbols; if it's in any snapshots, it has to list the specific snapshot symbols |
| // in addition to the “normal” symbol.) |
| package fips140 |
| |
| import ( |
| "context" |
| "crypto/sha256" |
| "fmt" |
| "io" |
| "os" |
| "path" |
| "path/filepath" |
| "slices" |
| "strings" |
| |
| "cmd/go/internal/base" |
| "cmd/go/internal/cfg" |
| "cmd/go/internal/fsys" |
| "cmd/go/internal/modfetch" |
| "cmd/go/internal/str" |
| |
| "golang.org/x/mod/module" |
| "golang.org/x/mod/semver" |
| ) |
| |
| // Init initializes the FIPS settings. |
| // It must be called before using any other functions in this package. |
| // If initialization fails, Init calls base.Fatalf. |
| func Init() { |
| if initDone { |
| return |
| } |
| initDone = true |
| initVersion() |
| initDir() |
| if Snapshot() { |
| fsys.Bind(Dir(), filepath.Join(cfg.GOROOT, "src/crypto/internal/fips140")) |
| } |
| |
| // ExperimentErr != nil if GOEXPERIMENT failed to parse. Typically |
| // cmd/go main will exit in this case, but it is allowed during |
| // toolchain selection, as the GOEXPERIMENT may be valid for the |
| // selected toolchain version. |
| if cfg.ExperimentErr == nil && cfg.Experiment.BoringCrypto && Enabled() { |
| base.Fatalf("go: cannot use GOFIPS140 with GOEXPERIMENT=boringcrypto") |
| } |
| if slices.Contains(cfg.BuildContext.BuildTags, "purego") && Enabled() { |
| base.Fatalf("go: cannot use GOFIPS140 with the purego build tag") |
| } |
| } |
| |
| var initDone bool |
| |
| // checkInit panics if Init has not been called. |
| func checkInit() { |
| if !initDone { |
| panic("fips: not initialized") |
| } |
| } |
| |
| // Version reports the GOFIPS140 version in use, |
| // which is either "off", "latest", or a version like "v1.2.3". |
| // If GOFIPS140 is set to an alias like "inprocess" or "certified", |
| // Version returns the underlying version. |
| func Version() string { |
| checkInit() |
| return version |
| } |
| |
| // Enabled reports whether FIPS mode is enabled at all. |
| // That is, it reports whether GOFIPS140 is set to something besides "off". |
| func Enabled() bool { |
| checkInit() |
| return version != "off" |
| } |
| |
| // Snapshot reports whether FIPS mode is using a source snapshot |
| // rather than $GOROOT/src/crypto/internal/fips140. |
| // That is, it reports whether GOFIPS140 is set to something besides "latest" or "off". |
| func Snapshot() bool { |
| checkInit() |
| return version != "latest" && version != "off" |
| } |
| |
| var version string |
| |
| func initVersion() { |
| // For off and latest, use the local source tree. |
| v := cfg.GOFIPS140 |
| if v == "off" || v == "" { |
| version = "off" |
| return |
| } |
| if v == "latest" { |
| version = "latest" |
| return |
| } |
| |
| // Otherwise version must exist in lib/fips140, either as |
| // a .zip (a source snapshot like v1.2.0.zip) |
| // or a .txt (a redirect like inprocess.txt, containing a version number). |
| if strings.Contains(v, "/") || strings.Contains(v, `\`) || strings.Contains(v, "..") { |
| base.Fatalf("go: malformed GOFIPS140 version %q", cfg.GOFIPS140) |
| } |
| if cfg.GOROOT == "" { |
| base.Fatalf("go: missing GOROOT for GOFIPS140") |
| } |
| |
| file := filepath.Join(cfg.GOROOT, "lib", "fips140", v) |
| if data, err := os.ReadFile(file + ".txt"); err == nil { |
| v = strings.TrimSpace(string(data)) |
| file = filepath.Join(cfg.GOROOT, "lib", "fips140", v) |
| if _, err := os.Stat(file + ".zip"); err != nil { |
| base.Fatalf("go: unknown GOFIPS140 version %q (from %q)", v, cfg.GOFIPS140) |
| } |
| } |
| |
| if _, err := os.Stat(file + ".zip"); err == nil { |
| // Found version. Add a build tag. |
| cfg.BuildContext.BuildTags = append(cfg.BuildContext.BuildTags, "fips140"+semver.MajorMinor(v)) |
| version = v |
| return |
| } |
| |
| base.Fatalf("go: unknown GOFIPS140 version %q", v) |
| } |
| |
| // Dir reports the directory containing the crypto/internal/fips140 source code. |
| // If Snapshot() is false, Dir returns GOROOT/src/crypto/internal/fips140. |
| // Otherwise Dir ensures that the snapshot has been unpacked into the |
| // module cache and then returns the directory in the module cache |
| // corresponding to the crypto/internal/fips140 directory. |
| func Dir() string { |
| checkInit() |
| return dir |
| } |
| |
| var dir string |
| |
| func initDir() { |
| v := version |
| if v == "latest" || v == "off" { |
| dir = filepath.Join(cfg.GOROOT, "src/crypto/internal/fips140") |
| return |
| } |
| |
| mod := module.Version{Path: "golang.org/fips140", Version: v} |
| file := filepath.Join(cfg.GOROOT, "lib/fips140", v+".zip") |
| ctx := context.Background() |
| |
| // The FIPS 140-3 Security Policy require checking the SHA-256 hash of the |
| // zip file. Verify it once against fips140.sum before unpacking it. |
| if _, err := modfetch.DownloadDir(ctx, mod); err != nil { |
| sumfile := filepath.Join(cfg.GOROOT, "lib/fips140/fips140.sum") |
| if err := verifyZipSum(file, sumfile); err != nil { |
| base.Fatalf("go: verifying GOFIPS140=%v: %v", v, err) |
| } |
| } |
| |
| zdir, err := modfetch.NewFetcher().Unzip(ctx, mod, file) |
| if err != nil { |
| base.Fatalf("go: unpacking GOFIPS140=%v: %v", v, err) |
| } |
| dir = filepath.Join(zdir, "fips140") |
| } |
| |
| // verifyZipSum checks that the SHA-256 hash of zipfile matches the entry |
| // for its base name in sumfile, which is expected to be in the format of |
| // GOROOT/lib/fips140/fips140.sum: "NAME SHA256HEX" lines, with "#" comments. |
| func verifyZipSum(zipfile, sumfile string) error { |
| sums, err := os.ReadFile(sumfile) |
| if err != nil { |
| return err |
| } |
| name := filepath.Base(zipfile) |
| var want string |
| for line := range strings.SplitSeq(string(sums), "\n") { |
| line = strings.TrimSpace(line) |
| if line == "" || strings.HasPrefix(line, "#") { |
| continue |
| } |
| n, h, ok := strings.Cut(line, " ") |
| if !ok { |
| continue |
| } |
| if n == name { |
| want = strings.TrimSpace(h) |
| break |
| } |
| } |
| if want == "" { |
| return fmt.Errorf("no SHA-256 hash for %s in %s", name, sumfile) |
| } |
| f, err := os.Open(zipfile) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| h := sha256.New() |
| if _, err := io.Copy(h, f); err != nil { |
| return err |
| } |
| if got := fmt.Sprintf("%x", h.Sum(nil)); got != want { |
| return fmt.Errorf("SHA-256 hash of %s is %s, want %s (from %s)", name, got, want, sumfile) |
| } |
| return nil |
| } |
| |
| // ResolveImport resolves the import path imp. |
| // If it is of the form crypto/internal/fips140/foo |
| // (not crypto/internal/fips140/v1.2.3/foo) |
| // and we are using a snapshot, then LookupImport |
| // rewrites the path to crypto/internal/fips140/v1.2.3/foo |
| // and returns that path and its location in the unpacked |
| // FIPS snapshot. |
| func ResolveImport(imp string) (newPath, dir string, ok bool) { |
| checkInit() |
| const fips = "crypto/internal/fips140" |
| if !Snapshot() || !str.HasPathPrefix(imp, fips) { |
| return "", "", false |
| } |
| fipsv := path.Join(fips, version) |
| var sub string |
| if str.HasPathPrefix(imp, fipsv) { |
| sub = "." + imp[len(fipsv):] |
| } else { |
| sub = "." + imp[len(fips):] |
| } |
| newPath = path.Join(fips, version, sub) |
| dir = filepath.Join(Dir(), version, sub) |
| return newPath, dir, true |
| } |