Source file src/cmd/go/internal/fips140/fips140.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package fips implements support for the GOFIPS140 build setting.
     6  //
     7  // The GOFIPS140 build setting controls two aspects of the build:
     8  //
     9  //   - Whether binaries are built to default to running in FIPS-140 mode,
    10  //     meaning whether they default to GODEBUG=fips140=on or =off.
    11  //
    12  //   - Which copy of the crypto/internal/fips140 source code to use.
    13  //     The default is obviously GOROOT/src/crypto/internal/fips140,
    14  //     but earlier snapshots that have differing levels of external
    15  //     validation and certification are stored in GOROOT/lib/fips140
    16  //     and can be substituted into the build instead.
    17  //
    18  // This package provides the logic needed by the rest of the go command
    19  // to make those decisions and implement the resulting policy.
    20  //
    21  // [Init] must be called to initialize the FIPS logic. It may fail and
    22  // call base.Fatalf.
    23  //
    24  // When GOFIPS140=off, [Enabled] returns false, and the build is
    25  // unchanged from its usual behaviors.
    26  //
    27  // When GOFIPS140 is anything else, [Enabled] returns true, and the build
    28  // sets the default GODEBUG to include fips140=on. This will make
    29  // binaries change their behavior at runtime to confirm to various
    30  // FIPS-140 details. [cmd/go/internal/load.defaultGODEBUG] calls
    31  // [fips.Enabled] when preparing the default settings.
    32  //
    33  // For all builds, FIPS code and data is laid out in contiguous regions
    34  // that are conceptually concatenated into a "fips object file" that the
    35  // linker hashes and then binaries can re-hash at startup to detect
    36  // corruption of those symbols. When [Enabled] is true, the link step
    37  // passes -fipso={a.Objdir}/fips.o to the linker to save a copy of the
    38  // fips.o file. Since the first build target always uses a.Objdir set to
    39  // $WORK/b001, a build like
    40  //
    41  //	GOFIPS140=latest go build -work my/binary
    42  //
    43  // will leave fips.o behind in $WORK/b001
    44  // (unless the build result is cached, of course).
    45  //
    46  // When GOFIPS140 is set to something besides off and latest, [Snapshot]
    47  // returns true, indicating that the build should replace the latest copy
    48  // of crypto/internal/fips140 with an earlier snapshot. The reason to do
    49  // this is to use a copy that has been through additional lab validation
    50  // (an "in-process" module) or NIST certification (a "certified" module).
    51  // The snapshots are stored in GOROOT/lib/fips140 in module zip form.
    52  // When a snapshot is being used, Init unpacks it into the module cache
    53  // and then uses that directory as the source location.
    54  //
    55  // A FIPS snapshot like v1.2.3 is integrated into the build in two different ways.
    56  //
    57  // First, the snapshot's fips140 directory replaces crypto/internal/fips140
    58  // using fsys.Bind. The effect is to appear to have deleted crypto/internal/fips140
    59  // and everything below it, replacing it with the single subdirectory
    60  // crypto/internal/fips140/v1.2.3, which now has the FIPS packages.
    61  // This virtual file system replacement makes patterns like std and crypto...
    62  // automatically see the snapshot packages instead of the original packages
    63  // as they walk GOROOT/src/crypto/internal/fips140.
    64  //
    65  // Second, ResolveImport is called to resolve an import like crypto/internal/fips140/sha256.
    66  // When snapshot v1.2.3 is being used, ResolveImport translates that path to
    67  // crypto/internal/fips140/v1.2.3/sha256 and returns the actual source directory
    68  // in the unpacked snapshot. Using the actual directory instead of the
    69  // virtual directory GOROOT/src/crypto/internal/fips140/v1.2.3 makes sure
    70  // that other tools using go list -json output can find the sources,
    71  // as well as making sure builds have a real directory in which to run the
    72  // assembler, compiler, and so on. The translation of the import path happens
    73  // in the same code that handles mapping golang.org/x/mod to
    74  // cmd/vendor/golang.org/x/mod when building commands.
    75  //
    76  // It is not strictly required to include v1.2.3 in the import path when using
    77  // a snapshot - we could make things work without doing that - but including
    78  // the v1.2.3 gives a different version of the code a different name, which is
    79  // always a good general rule. In particular, it will mean that govulncheck need
    80  // not have any special cases for crypto/internal/fips140 at all. The reports simply
    81  // need to list the relevant symbols in a given Go version. (For example, if a bug
    82  // is only in the in-tree copy but not the snapshots, it doesn't list the snapshot
    83  // symbols; if it's in any snapshots, it has to list the specific snapshot symbols
    84  // in addition to the “normal” symbol.)
    85  package fips140
    86  
    87  import (
    88  	"context"
    89  	"crypto/sha256"
    90  	"fmt"
    91  	"io"
    92  	"os"
    93  	"path"
    94  	"path/filepath"
    95  	"slices"
    96  	"strings"
    97  
    98  	"cmd/go/internal/base"
    99  	"cmd/go/internal/cfg"
   100  	"cmd/go/internal/fsys"
   101  	"cmd/go/internal/modfetch"
   102  	"cmd/go/internal/str"
   103  
   104  	"golang.org/x/mod/module"
   105  	"golang.org/x/mod/semver"
   106  )
   107  
   108  // Init initializes the FIPS settings.
   109  // It must be called before using any other functions in this package.
   110  // If initialization fails, Init calls base.Fatalf.
   111  func Init() {
   112  	if initDone {
   113  		return
   114  	}
   115  	initDone = true
   116  	initVersion()
   117  	initDir()
   118  	if Snapshot() {
   119  		fsys.Bind(Dir(), filepath.Join(cfg.GOROOT, "src/crypto/internal/fips140"))
   120  	}
   121  
   122  	// ExperimentErr != nil if GOEXPERIMENT failed to parse. Typically
   123  	// cmd/go main will exit in this case, but it is allowed during
   124  	// toolchain selection, as the GOEXPERIMENT may be valid for the
   125  	// selected toolchain version.
   126  	if cfg.ExperimentErr == nil && cfg.Experiment.BoringCrypto && Enabled() {
   127  		base.Fatalf("go: cannot use GOFIPS140 with GOEXPERIMENT=boringcrypto")
   128  	}
   129  	if slices.Contains(cfg.BuildContext.BuildTags, "purego") && Enabled() {
   130  		base.Fatalf("go: cannot use GOFIPS140 with the purego build tag")
   131  	}
   132  }
   133  
   134  var initDone bool
   135  
   136  // checkInit panics if Init has not been called.
   137  func checkInit() {
   138  	if !initDone {
   139  		panic("fips: not initialized")
   140  	}
   141  }
   142  
   143  // Version reports the GOFIPS140 version in use,
   144  // which is either "off", "latest", or a version like "v1.2.3".
   145  // If GOFIPS140 is set to an alias like "inprocess" or "certified",
   146  // Version returns the underlying version.
   147  func Version() string {
   148  	checkInit()
   149  	return version
   150  }
   151  
   152  // Enabled reports whether FIPS mode is enabled at all.
   153  // That is, it reports whether GOFIPS140 is set to something besides "off".
   154  func Enabled() bool {
   155  	checkInit()
   156  	return version != "off"
   157  }
   158  
   159  // Snapshot reports whether FIPS mode is using a source snapshot
   160  // rather than $GOROOT/src/crypto/internal/fips140.
   161  // That is, it reports whether GOFIPS140 is set to something besides "latest" or "off".
   162  func Snapshot() bool {
   163  	checkInit()
   164  	return version != "latest" && version != "off"
   165  }
   166  
   167  var version string
   168  
   169  func initVersion() {
   170  	// For off and latest, use the local source tree.
   171  	v := cfg.GOFIPS140
   172  	if v == "off" || v == "" {
   173  		version = "off"
   174  		return
   175  	}
   176  	if v == "latest" {
   177  		version = "latest"
   178  		return
   179  	}
   180  
   181  	// Otherwise version must exist in lib/fips140, either as
   182  	// a .zip (a source snapshot like v1.2.0.zip)
   183  	// or a .txt (a redirect like inprocess.txt, containing a version number).
   184  	if strings.Contains(v, "/") || strings.Contains(v, `\`) || strings.Contains(v, "..") {
   185  		base.Fatalf("go: malformed GOFIPS140 version %q", cfg.GOFIPS140)
   186  	}
   187  	if cfg.GOROOT == "" {
   188  		base.Fatalf("go: missing GOROOT for GOFIPS140")
   189  	}
   190  
   191  	file := filepath.Join(cfg.GOROOT, "lib", "fips140", v)
   192  	if data, err := os.ReadFile(file + ".txt"); err == nil {
   193  		v = strings.TrimSpace(string(data))
   194  		file = filepath.Join(cfg.GOROOT, "lib", "fips140", v)
   195  		if _, err := os.Stat(file + ".zip"); err != nil {
   196  			base.Fatalf("go: unknown GOFIPS140 version %q (from %q)", v, cfg.GOFIPS140)
   197  		}
   198  	}
   199  
   200  	if _, err := os.Stat(file + ".zip"); err == nil {
   201  		// Found version. Add a build tag.
   202  		cfg.BuildContext.BuildTags = append(cfg.BuildContext.BuildTags, "fips140"+semver.MajorMinor(v))
   203  		version = v
   204  		return
   205  	}
   206  
   207  	base.Fatalf("go: unknown GOFIPS140 version %q", v)
   208  }
   209  
   210  // Dir reports the directory containing the crypto/internal/fips140 source code.
   211  // If Snapshot() is false, Dir returns GOROOT/src/crypto/internal/fips140.
   212  // Otherwise Dir ensures that the snapshot has been unpacked into the
   213  // module cache and then returns the directory in the module cache
   214  // corresponding to the crypto/internal/fips140 directory.
   215  func Dir() string {
   216  	checkInit()
   217  	return dir
   218  }
   219  
   220  var dir string
   221  
   222  func initDir() {
   223  	v := version
   224  	if v == "latest" || v == "off" {
   225  		dir = filepath.Join(cfg.GOROOT, "src/crypto/internal/fips140")
   226  		return
   227  	}
   228  
   229  	mod := module.Version{Path: "golang.org/fips140", Version: v}
   230  	file := filepath.Join(cfg.GOROOT, "lib/fips140", v+".zip")
   231  	ctx := context.Background()
   232  
   233  	// The FIPS 140-3 Security Policy require checking the SHA-256 hash of the
   234  	// zip file. Verify it once against fips140.sum before unpacking it.
   235  	if _, err := modfetch.DownloadDir(ctx, mod); err != nil {
   236  		sumfile := filepath.Join(cfg.GOROOT, "lib/fips140/fips140.sum")
   237  		if err := verifyZipSum(file, sumfile); err != nil {
   238  			base.Fatalf("go: verifying GOFIPS140=%v: %v", v, err)
   239  		}
   240  	}
   241  
   242  	zdir, err := modfetch.NewFetcher().Unzip(ctx, mod, file)
   243  	if err != nil {
   244  		base.Fatalf("go: unpacking GOFIPS140=%v: %v", v, err)
   245  	}
   246  	dir = filepath.Join(zdir, "fips140")
   247  }
   248  
   249  // verifyZipSum checks that the SHA-256 hash of zipfile matches the entry
   250  // for its base name in sumfile, which is expected to be in the format of
   251  // GOROOT/lib/fips140/fips140.sum: "NAME SHA256HEX" lines, with "#" comments.
   252  func verifyZipSum(zipfile, sumfile string) error {
   253  	sums, err := os.ReadFile(sumfile)
   254  	if err != nil {
   255  		return err
   256  	}
   257  	name := filepath.Base(zipfile)
   258  	var want string
   259  	for line := range strings.SplitSeq(string(sums), "\n") {
   260  		line = strings.TrimSpace(line)
   261  		if line == "" || strings.HasPrefix(line, "#") {
   262  			continue
   263  		}
   264  		n, h, ok := strings.Cut(line, " ")
   265  		if !ok {
   266  			continue
   267  		}
   268  		if n == name {
   269  			want = strings.TrimSpace(h)
   270  			break
   271  		}
   272  	}
   273  	if want == "" {
   274  		return fmt.Errorf("no SHA-256 hash for %s in %s", name, sumfile)
   275  	}
   276  	f, err := os.Open(zipfile)
   277  	if err != nil {
   278  		return err
   279  	}
   280  	defer f.Close()
   281  	h := sha256.New()
   282  	if _, err := io.Copy(h, f); err != nil {
   283  		return err
   284  	}
   285  	if got := fmt.Sprintf("%x", h.Sum(nil)); got != want {
   286  		return fmt.Errorf("SHA-256 hash of %s is %s, want %s (from %s)", name, got, want, sumfile)
   287  	}
   288  	return nil
   289  }
   290  
   291  // ResolveImport resolves the import path imp.
   292  // If it is of the form crypto/internal/fips140/foo
   293  // (not crypto/internal/fips140/v1.2.3/foo)
   294  // and we are using a snapshot, then LookupImport
   295  // rewrites the path to crypto/internal/fips140/v1.2.3/foo
   296  // and returns that path and its location in the unpacked
   297  // FIPS snapshot.
   298  func ResolveImport(imp string) (newPath, dir string, ok bool) {
   299  	checkInit()
   300  	const fips = "crypto/internal/fips140"
   301  	if !Snapshot() || !str.HasPathPrefix(imp, fips) {
   302  		return "", "", false
   303  	}
   304  	fipsv := path.Join(fips, version)
   305  	var sub string
   306  	if str.HasPathPrefix(imp, fipsv) {
   307  		sub = "." + imp[len(fipsv):]
   308  	} else {
   309  		sub = "." + imp[len(fips):]
   310  	}
   311  	newPath = path.Join(fips, version, sub)
   312  	dir = filepath.Join(Dir(), version, sub)
   313  	return newPath, dir, true
   314  }
   315  

View as plain text