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. Auditors like to be able to 44 // see that file. Accordingly, when [Enabled] returns true, 45 // [cmd/go/internal/work.Builder.useCache] arranges never to cache linker 46 // output, so that the link step always runs, and fips.o is always left 47 // behind in the link step. If this proves too slow, we could always 48 // cache fips.o as an extra link output and then restore it when -work is 49 // set, but we went a very long time never caching link steps at all, so 50 // not caching them in FIPS mode seems perfectly fine. 51 // 52 // When GOFIPS140 is set to something besides off and latest, [Snapshot] 53 // returns true, indicating that the build should replace the latest copy 54 // of crypto/internal/fips140 with an earlier snapshot. The reason to do 55 // this is to use a copy that has been through additional lab validation 56 // (an "in-process" module) or NIST certification (a "certified" module). 57 // The snapshots are stored in GOROOT/lib/fips140 in module zip form. 58 // When a snapshot is being used, Init unpacks it into the module cache 59 // and then uses that directory as the source location. 60 // 61 // A FIPS snapshot like v1.2.3 is integrated into the build in two different ways. 62 // 63 // First, the snapshot's fips140 directory replaces crypto/internal/fips140 64 // using fsys.Bind. The effect is to appear to have deleted crypto/internal/fips140 65 // and everything below it, replacing it with the single subdirectory 66 // crypto/internal/fips140/v1.2.3, which now has the FIPS packages. 67 // This virtual file system replacement makes patterns like std and crypto... 68 // automatically see the snapshot packages instead of the original packages 69 // as they walk GOROOT/src/crypto/internal/fips140. 70 // 71 // Second, ResolveImport is called to resolve an import like crypto/internal/fips140/sha256. 72 // When snapshot v1.2.3 is being used, ResolveImport translates that path to 73 // crypto/internal/fips140/v1.2.3/sha256 and returns the actual source directory 74 // in the unpacked snapshot. Using the actual directory instead of the 75 // virtual directory GOROOT/src/crypto/internal/fips140/v1.2.3 makes sure 76 // that other tools using go list -json output can find the sources, 77 // as well as making sure builds have a real directory in which to run the 78 // assembler, compiler, and so on. The translation of the import path happens 79 // in the same code that handles mapping golang.org/x/mod to 80 // cmd/vendor/golang.org/x/mod when building commands. 81 // 82 // It is not strictly required to include v1.2.3 in the import path when using 83 // a snapshot - we could make things work without doing that - but including 84 // the v1.2.3 gives a different version of the code a different name, which is 85 // always a good general rule. In particular, it will mean that govulncheck need 86 // not have any special cases for crypto/internal/fips140 at all. The reports simply 87 // need to list the relevant symbols in a given Go version. (For example, if a bug 88 // is only in the in-tree copy but not the snapshots, it doesn't list the snapshot 89 // symbols; if it's in any snapshots, it has to list the specific snapshot symbols 90 // in addition to the “normal” symbol.) 91 package fips140 92 93 import ( 94 "cmd/go/internal/base" 95 "cmd/go/internal/cfg" 96 "cmd/go/internal/fsys" 97 "cmd/go/internal/modfetch" 98 "cmd/go/internal/str" 99 "context" 100 "os" 101 "path" 102 "path/filepath" 103 "strings" 104 105 "golang.org/x/mod/module" 106 "golang.org/x/mod/semver" 107 ) 108 109 // Init initializes the FIPS settings. 110 // It must be called before using any other functions in this package. 111 // If initialization fails, Init calls base.Fatalf. 112 func Init() { 113 if initDone { 114 return 115 } 116 initDone = true 117 initVersion() 118 initDir() 119 if Snapshot() { 120 fsys.Bind(Dir(), filepath.Join(cfg.GOROOT, "src/crypto/internal/fips140")) 121 } 122 } 123 124 var initDone bool 125 126 // checkInit panics if Init has not been called. 127 func checkInit() { 128 if !initDone { 129 panic("fips: not initialized") 130 } 131 } 132 133 // Version reports the GOFIPS140 version in use, 134 // which is either "off", "latest", or a version like "v1.2.3". 135 // If GOFIPS140 is set to an alias like "inprocess" or "certified", 136 // Version returns the underlying version. 137 func Version() string { 138 checkInit() 139 return version 140 } 141 142 // Enabled reports whether FIPS mode is enabled at all. 143 // That is, it reports whether GOFIPS140 is set to something besides "off". 144 func Enabled() bool { 145 checkInit() 146 return version != "off" 147 } 148 149 // Snapshot reports whether FIPS mode is using a source snapshot 150 // rather than $GOROOT/src/crypto/internal/fips140. 151 // That is, it reports whether GOFIPS140 is set to something besides "latest" or "off". 152 func Snapshot() bool { 153 checkInit() 154 return version != "latest" && version != "off" 155 } 156 157 var version string 158 159 func initVersion() { 160 // For off and latest, use the local source tree. 161 v := cfg.GOFIPS140 162 if v == "off" || v == "" { 163 version = "off" 164 return 165 } 166 if v == "latest" { 167 version = "latest" 168 return 169 } 170 171 // Otherwise version must exist in lib/fips140, either as 172 // a .zip (a source snapshot like v1.2.0.zip) 173 // or a .txt (a redirect like inprocess.txt, containing a version number). 174 if strings.Contains(v, "/") || strings.Contains(v, `\`) || strings.Contains(v, "..") { 175 base.Fatalf("go: malformed GOFIPS140 version %q", cfg.GOFIPS140) 176 } 177 if cfg.GOROOT == "" { 178 base.Fatalf("go: missing GOROOT for GOFIPS140") 179 } 180 181 file := filepath.Join(cfg.GOROOT, "lib", "fips140", v) 182 if data, err := os.ReadFile(file + ".txt"); err == nil { 183 v = strings.TrimSpace(string(data)) 184 file = filepath.Join(cfg.GOROOT, "lib", "fips140", v) 185 if _, err := os.Stat(file + ".zip"); err != nil { 186 base.Fatalf("go: unknown GOFIPS140 version %q (from %q)", v, cfg.GOFIPS140) 187 } 188 } 189 190 if _, err := os.Stat(file + ".zip"); err == nil { 191 // Found version. Add a build tag. 192 cfg.BuildContext.BuildTags = append(cfg.BuildContext.BuildTags, "fips140"+semver.MajorMinor(v)) 193 version = v 194 return 195 } 196 197 base.Fatalf("go: unknown GOFIPS140 version %q", v) 198 } 199 200 // Dir reports the directory containing the crypto/internal/fips140 source code. 201 // If Snapshot() is false, Dir returns GOROOT/src/crypto/internal/fips140. 202 // Otherwise Dir ensures that the snapshot has been unpacked into the 203 // module cache and then returns the directory in the module cache 204 // corresponding to the crypto/internal/fips140 directory. 205 func Dir() string { 206 checkInit() 207 return dir 208 } 209 210 var dir string 211 212 func initDir() { 213 v := version 214 if v == "latest" || v == "off" { 215 dir = filepath.Join(cfg.GOROOT, "src/crypto/internal/fips140") 216 return 217 } 218 219 mod := module.Version{Path: "golang.org/fips140", Version: v} 220 file := filepath.Join(cfg.GOROOT, "lib/fips140", v+".zip") 221 zdir, err := modfetch.Unzip(context.Background(), mod, file) 222 if err != nil { 223 base.Fatalf("go: unpacking GOFIPS140=%v: %v", v, err) 224 } 225 dir = filepath.Join(zdir, "fips140") 226 return 227 } 228 229 // ResolveImport resolves the import path imp. 230 // If it is of the form crypto/internal/fips140/foo 231 // (not crypto/internal/fips140/v1.2.3/foo) 232 // and we are using a snapshot, then LookupImport 233 // rewrites the path to crypto/internal/fips140/v1.2.3/foo 234 // and returns that path and its location in the unpacked 235 // FIPS snapshot. 236 func ResolveImport(imp string) (newPath, dir string, ok bool) { 237 checkInit() 238 const fips = "crypto/internal/fips140" 239 if !Snapshot() || !str.HasPathPrefix(imp, fips) { 240 return "", "", false 241 } 242 fipsv := path.Join(fips, version) 243 var sub string 244 if str.HasPathPrefix(imp, fipsv) { 245 sub = "." + imp[len(fipsv):] 246 } else { 247 sub = "." + imp[len(fips):] 248 } 249 newPath = path.Join(fips, version, sub) 250 dir = filepath.Join(Dir(), version, sub) 251 return newPath, dir, true 252 } 253