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 fips140 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 sumfile := filepath.Join(cfg.GOROOT, "lib/fips140/fips140.sum") 234 _, ziphash, err := lookupZipSum(sumfile, filepath.Base(file)) 235 if err != nil { 236 base.Fatalf("go: verifying GOFIPS140=%v: %v", v, err) 237 } 238 239 // fips140.sum records both the SHA-256 hash of the zip file and its 240 // go.sum-style module zip hash. Unzip uses the cached copy in the 241 // module cache only if the cache records that module zip hash for it, 242 // and otherwise discards the copy and unpacks the snapshot again. 243 // 244 // The FIPS 140-3 Security Policy requires checking the SHA-256 hash 245 // of the zip file. Unzip calls verify exactly when it is about to 246 // unpack the zip file, so the hash is checked once per unpacking 247 // (whatever the reason for it) rather than on every go command. 248 verify := func() error { return verifyZipSum(file, sumfile) } 249 zdir, err := modfetch.NewFetcher().Unzip(ctx, mod, file, ziphash, verify) 250 if err != nil { 251 base.Fatalf("go: unpacking GOFIPS140=%v: %v", v, err) 252 } 253 dir = filepath.Join(zdir, "fips140") 254 } 255 256 // lookupZipSum returns the SHA-256 hash and module zip hash recorded for 257 // name in sumfile, which is expected to be in the format of 258 // GOROOT/lib/fips140/fips140.sum: "NAME SHA256HEX H1HASH" lines, with "#" 259 // comments. H1HASH is the module zip hash in the format used by go.sum. 260 func lookupZipSum(sumfile, name string) (sha256hex, ziphash string, err error) { 261 sums, err := os.ReadFile(sumfile) 262 if err != nil { 263 return "", "", err 264 } 265 for line := range strings.SplitSeq(string(sums), "\n") { 266 line = strings.TrimSpace(line) 267 if line == "" || strings.HasPrefix(line, "#") { 268 continue 269 } 270 f := strings.Fields(line) 271 if len(f) != 3 || f[0] != name { 272 continue 273 } 274 if !strings.HasPrefix(f[2], "h1:") { 275 return "", "", fmt.Errorf("malformed module zip hash %q for %s in %s", f[2], name, sumfile) 276 } 277 return f[1], f[2], nil 278 } 279 return "", "", fmt.Errorf("no entry for %s in %s", name, sumfile) 280 } 281 282 // verifyZipSum checks that the SHA-256 hash of zipfile matches the entry 283 // for its base name in sumfile. 284 func verifyZipSum(zipfile, sumfile string) error { 285 name := filepath.Base(zipfile) 286 want, _, err := lookupZipSum(sumfile, name) 287 if err != nil { 288 return err 289 } 290 f, err := os.Open(zipfile) 291 if err != nil { 292 return err 293 } 294 defer f.Close() 295 h := sha256.New() 296 if _, err := io.Copy(h, f); err != nil { 297 return err 298 } 299 if got := fmt.Sprintf("%x", h.Sum(nil)); got != want { 300 return fmt.Errorf("SHA-256 hash of %s is %s, want %s (from %s)", name, got, want, sumfile) 301 } 302 return nil 303 } 304 305 // ResolveImport resolves the import path imp. 306 // If it is of the form crypto/internal/fips140/foo 307 // (not crypto/internal/fips140/v1.2.3/foo) 308 // and we are using a snapshot, then LookupImport 309 // rewrites the path to crypto/internal/fips140/v1.2.3/foo 310 // and returns that path and its location in the unpacked 311 // FIPS snapshot. 312 func ResolveImport(imp string) (newPath, dir string, ok bool) { 313 checkInit() 314 const fips = "crypto/internal/fips140" 315 if !Snapshot() || !str.HasPathPrefix(imp, fips) { 316 return "", "", false 317 } 318 fipsv := path.Join(fips, version) 319 var sub string 320 if str.HasPathPrefix(imp, fipsv) { 321 sub = "." + imp[len(fipsv):] 322 } else { 323 sub = "." + imp[len(fips):] 324 } 325 newPath = path.Join(fips, version, sub) 326 dir = filepath.Join(Dir(), version, sub) 327 return newPath, dir, true 328 } 329