Skip to content

Commit 01103d7

Browse files
rscgopherbot
authored andcommitted
cmd/go: add GOFIPS140 snapshot support
GOFIPS140 does two things: (1) control whether to build binaries that run in FIPS-140 mode by default, and (2) control which version of the crypto/internal/fips source tree to use during a build. This CL implements part (2). The older snapshot source trees are stored in GOROOT/lib/fips140 in module-formatted zip files, even though crypto/internal/fips is not technically a module. (Reusing the module packing and unpacking code avoids reinventing it.) See cmd/go/internal/fips/fips.go for an overview. The documentation for GOFIPS140 is in a follow-up CL. For #70200. Change-Id: I73a610fd2c9ff66d0cced37d51acd8053497238e Reviewed-on: https://go-review.googlesource.com/c/go/+/629201 Reviewed-by: Michael Matloob <[email protected]> Auto-Submit: Russ Cox <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 606a0bd commit 01103d7

File tree

5 files changed

+366
-3
lines changed

5 files changed

+366
-3
lines changed

src/cmd/go/internal/fips/fips.go

+132-1
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,59 @@
5454
// of crypto/internal/fips with an earlier snapshot. The reason to do
5555
// this is to use a copy that has been through additional lab validation
5656
// (an "in-process" module) or NIST certification (a "certified" module).
57-
// This functionality is not yet implemented.
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/fips
64+
// using fsys.Bind. The effect is to appear to have deleted crypto/internal/fips
65+
// and everything below it, replacing it with the single subdirectory
66+
// crypto/internal/fips/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/fips.
70+
//
71+
// Second, ResolveImport is called to resolve an import like crypto/internal/fips/sha256.
72+
// When snapshot v1.2.3 is being used, ResolveImport translates that path to
73+
// crypto/internal/fips/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/fips/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/fips 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+
//
92+
// TODO: crypto/internal/fips is going to move to crypto/internal/fips140,
93+
// at which point all the crypto/internal/fips references need to be updated.
5894
package fips
5995

6096
import (
6197
"cmd/go/internal/base"
6298
"cmd/go/internal/cfg"
99+
"cmd/go/internal/fsys"
100+
"cmd/go/internal/modfetch"
101+
"cmd/go/internal/str"
102+
"context"
103+
"os"
104+
"path"
105+
"path/filepath"
106+
"strings"
107+
108+
"golang.org/x/mod/module"
109+
"golang.org/x/mod/semver"
63110
)
64111

65112
// Init initializes the FIPS settings.
@@ -71,6 +118,10 @@ func Init() {
71118
}
72119
initDone = true
73120
initVersion()
121+
initDir()
122+
if Snapshot() {
123+
fsys.Bind(Dir(), filepath.Join(cfg.GOROOT, "src/crypto/internal/fips"))
124+
}
74125
}
75126

76127
var initDone bool
@@ -120,5 +171,85 @@ func initVersion() {
120171
return
121172
}
122173

174+
// Otherwise version must exist in lib/fips140, either as
175+
// a .zip (a source snapshot like v1.2.0.zip)
176+
// or a .txt (a redirect like inprocess.txt, containing a version number).
177+
if strings.Contains(v, "/") || strings.Contains(v, `\`) || strings.Contains(v, "..") {
178+
base.Fatalf("go: malformed GOFIPS140 version %q", cfg.GOFIPS140)
179+
}
180+
if cfg.GOROOT == "" {
181+
base.Fatalf("go: missing GOROOT for GOFIPS140")
182+
}
183+
184+
file := filepath.Join(cfg.GOROOT, "lib", "fips140", v)
185+
if data, err := os.ReadFile(file + ".txt"); err == nil {
186+
v = strings.TrimSpace(string(data))
187+
file = filepath.Join(cfg.GOROOT, "lib", "fips140", v)
188+
if _, err := os.Stat(file + ".zip"); err != nil {
189+
base.Fatalf("go: unknown GOFIPS140 version %q (from %q)", v, cfg.GOFIPS140)
190+
}
191+
}
192+
193+
if _, err := os.Stat(file + ".zip"); err == nil {
194+
// Found version. Add a build tag.
195+
cfg.BuildContext.BuildTags = append(cfg.BuildContext.BuildTags, "fips140"+semver.MajorMinor(v))
196+
version = v
197+
return
198+
}
199+
123200
base.Fatalf("go: unknown GOFIPS140 version %q", v)
124201
}
202+
203+
// Dir reports the directory containing the crypto/internal/fips source code.
204+
// If Snapshot() is false, Dir returns GOROOT/src/crypto/internal/fips.
205+
// Otherwise Dir ensures that the snapshot has been unpacked into the
206+
// module cache and then returns the directory in the module cache
207+
// corresponding to the crypto/internal/fips directory.
208+
func Dir() string {
209+
checkInit()
210+
return dir
211+
}
212+
213+
var dir string
214+
215+
func initDir() {
216+
v := version
217+
if v == "latest" || v == "off" {
218+
dir = filepath.Join(cfg.GOROOT, "src/crypto/internal/fips")
219+
return
220+
}
221+
222+
mod := module.Version{Path: "golang.org/fips140", Version: v}
223+
file := filepath.Join(cfg.GOROOT, "lib/fips140", v+".zip")
224+
zdir, err := modfetch.Unzip(context.Background(), mod, file)
225+
if err != nil {
226+
base.Fatalf("go: unpacking GOFIPS140=%v: %v", v, err)
227+
}
228+
dir = filepath.Join(zdir, "fips140")
229+
return
230+
}
231+
232+
// ResolveImport resolves the import path imp.
233+
// If it is of the form crypto/internal/fips/foo
234+
// (not crypto/internal/fips/v1.2.3/foo)
235+
// and we are using a snapshot, then LookupImport
236+
// rewrites the path to crypto/internal/fips/v1.2.3/foo
237+
// and returns that path and its location in the unpacked
238+
// FIPS snapshot.
239+
func ResolveImport(imp string) (newPath, dir string, ok bool) {
240+
checkInit()
241+
const fips = "crypto/internal/fips"
242+
if !Snapshot() || !str.HasPathPrefix(imp, fips) {
243+
return "", "", false
244+
}
245+
fipsv := path.Join(fips, version)
246+
var sub string
247+
if str.HasPathPrefix(imp, fipsv) {
248+
sub = "." + imp[len(fipsv):]
249+
} else {
250+
sub = "." + imp[len(fips):]
251+
}
252+
newPath = path.Join(fips, version, sub)
253+
dir = filepath.Join(Dir(), version, sub)
254+
return newPath, dir, true
255+
}

src/cmd/go/internal/fips/mkzip.go

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
//go:build ignore
6+
7+
// Mkzip creates a FIPS snapshot zip file.
8+
// See GOROOT/lib/fips140/README.md and GOROOT/lib/fips140/Makefile
9+
// for more details about when and why to use this.
10+
//
11+
// Usage:
12+
//
13+
// cd GOROOT/lib/fips140
14+
// go run ../../src/cmd/go/internal/fips/mkzip.go [-b branch] v1.2.3
15+
//
16+
// Mkzip creates a zip file named for the version on the command line
17+
// using the sources in the named branch (default origin/master,
18+
// to avoid accidentally including local commits).
19+
package main
20+
21+
import (
22+
"archive/zip"
23+
"bytes"
24+
"flag"
25+
"fmt"
26+
"io"
27+
"log"
28+
"os"
29+
"path/filepath"
30+
"regexp"
31+
"strings"
32+
33+
"golang.org/x/mod/module"
34+
modzip "golang.org/x/mod/zip"
35+
)
36+
37+
var flagBranch = flag.String("b", "origin/master", "branch to use")
38+
39+
func usage() {
40+
fmt.Fprintf(os.Stderr, "usage: go run mkzip.go [-b branch] vX.Y.Z\n")
41+
os.Exit(2)
42+
}
43+
44+
func main() {
45+
log.SetFlags(0)
46+
log.SetPrefix("mkzip: ")
47+
flag.Usage = usage
48+
flag.Parse()
49+
if flag.NArg() != 1 {
50+
usage()
51+
}
52+
53+
// Must run in the lib/fips140 directory, where the snapshots live.
54+
wd, err := os.Getwd()
55+
if err != nil {
56+
log.Fatal(err)
57+
}
58+
if !strings.HasSuffix(filepath.ToSlash(wd), "lib/fips140") {
59+
log.Fatalf("must be run in lib/fips140 directory")
60+
}
61+
62+
// Must have valid version, and must not overwrite existing file.
63+
version := flag.Arg(0)
64+
if !regexp.MustCompile(`^v\d+\.\d+\.\d+$`).MatchString(version) {
65+
log.Fatalf("invalid version %q; must be vX.Y.Z", version)
66+
}
67+
if _, err := os.Stat(version + ".zip"); err == nil {
68+
log.Fatalf("%s.zip already exists", version)
69+
}
70+
71+
// Make standard module zip file in memory.
72+
// The module path "golang.org/fips140" needs to be a valid module name,
73+
// and it is the path where the zip file will be unpacked in the module cache.
74+
// The path must begin with a domain name to satisfy the module validation rules,
75+
// but otherwise the path is not used. The cmd/go code using these zips
76+
// knows that the zip contains crypto/internal/fips.
77+
goroot := "../.."
78+
var zbuf bytes.Buffer
79+
err = modzip.CreateFromVCS(&zbuf,
80+
module.Version{Path: "golang.org/fips140", Version: version},
81+
goroot, *flagBranch, "src/crypto/internal/fips")
82+
if err != nil {
83+
log.Fatal(err)
84+
}
85+
86+
// Write new zip file with longer paths: fips140/v1.2.3/foo.go instead of foo.go.
87+
// That way we can bind the fips140 directory onto the
88+
// GOROOT/src/crypto/internal/fips directory and get a
89+
// crypto/internal/fips/v1.2.3 with the snapshot code
90+
// and an otherwise empty crypto/internal/fips directory.
91+
zr, err := zip.NewReader(bytes.NewReader(zbuf.Bytes()), int64(zbuf.Len()))
92+
if err != nil {
93+
log.Fatal(err)
94+
}
95+
96+
var zbuf2 bytes.Buffer
97+
zw := zip.NewWriter(&zbuf2)
98+
for _, f := range zr.File {
99+
// golang.org/[email protected]/dir/file.go ->
100+
// golang.org/[email protected]/fips140/v1.2.3/dir/file.go
101+
if f.Name != "golang.org/fips140@"+version+"/LICENSE" {
102+
f.Name = "golang.org/fips140@" + version + "/fips140/" + version +
103+
strings.TrimPrefix(f.Name, "golang.org/fips140@"+version)
104+
}
105+
wf, err := zw.CreateRaw(&f.FileHeader)
106+
if err != nil {
107+
log.Fatal(err)
108+
}
109+
rf, err := f.OpenRaw()
110+
if err != nil {
111+
log.Fatal(err)
112+
}
113+
if _, err := io.Copy(wf, rf); err != nil {
114+
log.Fatal(err)
115+
}
116+
}
117+
if err := zw.Close(); err != nil {
118+
log.Fatal(err)
119+
}
120+
121+
err = os.WriteFile(version+".zip", zbuf2.Bytes(), 0666)
122+
if err != nil {
123+
log.Fatal(err)
124+
}
125+
126+
log.Printf("wrote %s.zip", version)
127+
}

src/cmd/go/internal/load/pkg.go

+34-2
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ func (p *Package) copyBuild(opts PackageOpts, pp *build.Package) {
407407
p.BinaryOnly = pp.BinaryOnly
408408

409409
// TODO? Target
410-
p.Goroot = pp.Goroot
410+
p.Goroot = pp.Goroot || fips.Snapshot() && str.HasFilePathPrefix(p.Dir, fips.Dir())
411411
p.Standard = p.Goroot && p.ImportPath != "" && search.IsStandardImportPath(p.ImportPath)
412412
p.GoFiles = pp.GoFiles
413413
p.CgoFiles = pp.CgoFiles
@@ -885,7 +885,10 @@ func loadPackageData(ctx context.Context, path, parentPath, parentDir, parentRoo
885885
}
886886
r := resolvedImportCache.Do(importKey, func() resolvedImport {
887887
var r resolvedImport
888-
if cfg.ModulesEnabled {
888+
if newPath, dir, ok := fips.ResolveImport(path); ok {
889+
r.path = newPath
890+
r.dir = dir
891+
} else if cfg.ModulesEnabled {
889892
r.dir, r.path, r.err = modload.Lookup(parentPath, parentIsStd, path)
890893
} else if build.IsLocalImport(path) {
891894
r.dir = filepath.Join(parentDir, path)
@@ -1516,6 +1519,34 @@ func disallowInternal(ctx context.Context, srcDir string, importer *Package, imp
15161519
i-- // rewind over slash in ".../internal"
15171520
}
15181521

1522+
// FIPS-140 snapshots are special, because they comes from a non-GOROOT
1523+
// directory, so the usual directory rules don't work apply, or rather they
1524+
// apply differently depending on whether we are using a snapshot or the
1525+
// in-tree copy of the code. We apply a consistent rule here:
1526+
// crypto/internal/fips can only see crypto/internal, never top-of-tree internal.
1527+
// Similarly, crypto/... can see crypto/internal/fips even though the usual rules
1528+
// would not allow it in snapshot mode.
1529+
if str.HasPathPrefix(importerPath, "crypto") && str.HasPathPrefix(p.ImportPath, "crypto/internal/fips") {
1530+
return nil // crypto can use crypto/internal/fips
1531+
}
1532+
if str.HasPathPrefix(importerPath, "crypto/internal/fips") {
1533+
if str.HasPathPrefix(p.ImportPath, "crypto/internal") {
1534+
return nil // crypto/internal/fips can use crypto/internal
1535+
}
1536+
// TODO: Delete this switch once the usages are removed.
1537+
switch p.ImportPath {
1538+
case "internal/abi",
1539+
"internal/testenv",
1540+
"internal/cpu",
1541+
"internal/goarch",
1542+
"internal/asan",
1543+
"internal/byteorder",
1544+
"internal/godebug":
1545+
return nil
1546+
}
1547+
goto Error
1548+
}
1549+
15191550
if p.Module == nil {
15201551
parent := p.Dir[:i+len(p.Dir)-len(p.ImportPath)]
15211552

@@ -1546,6 +1577,7 @@ func disallowInternal(ctx context.Context, srcDir string, importer *Package, imp
15461577
}
15471578
}
15481579

1580+
Error:
15491581
// Internal is present, and srcDir is outside parent's tree. Not allowed.
15501582
perr := &PackageError{
15511583
alwaysPrintStack: true,

src/cmd/go/internal/modload/load.go

+4
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ import (
115115

116116
"cmd/go/internal/base"
117117
"cmd/go/internal/cfg"
118+
"cmd/go/internal/fips"
118119
"cmd/go/internal/fsys"
119120
"cmd/go/internal/gover"
120121
"cmd/go/internal/imports"
@@ -1957,6 +1958,9 @@ func (ld *loader) pkgTest(ctx context.Context, pkg *loadPkg, testFlags loadPkgFl
19571958
// stdVendor returns the canonical import path for the package with the given
19581959
// path when imported from the standard-library package at parentPath.
19591960
func (ld *loader) stdVendor(parentPath, path string) string {
1961+
if p, _, ok := fips.ResolveImport(path); ok {
1962+
return p
1963+
}
19601964
if search.IsStandardImportPath(path) {
19611965
return path
19621966
}

0 commit comments

Comments
 (0)