cmd/go: bundle go.sum h1 hashes for golang.org/fips140
Instead of providing a carve-out for the golang.org/fips module, add the canonical go.sum h1 hash to its trusted fips140.sum file. When a modfetch would normally see this bundled module, use the trusted ziphash in fips140.sum to populate its entry in the GOMODCACHE. Fixes CVE-2026-94444 For golang/go#81833 Change-Id: Ib8ebf0837e56fb8825e4526a7114b61ac843fd0b Reviewed-on: https://go-review.googlesource.com/c/go/+/840685 Reviewed-by: Filippo Valsorda <filippo@golang.org> Reviewed-by: Nicholas Husin <nsh@golang.org> Reviewed-by: Michael Pratt <mpratt@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Neal Patel <nealpatel@google.com> Reviewed-by: Nicholas Husin <husin@google.com> Reviewed-by: Roland Shoemaker <roland@golang.org> Commit-Queue: Michael Pratt <mpratt@google.com>
This commit is contained in:
1 parent
ac7ada9af9
commit
819eab448b
7 files changed
+350
-60
No files matched your search
@@ -1,4 +1,7 @@
|
||||
# SHA256 checksums of snapshot zip files in this directory.
|
||||
# Checksums of snapshot zip files in this directory.
|
||||
# Each line is "NAME SHA256HEX H1HASH", where SHA256HEX is the SHA256
|
||||
# checksum of the zip file and H1HASH is the module zip hash of its
|
||||
# contents, as recorded in go.sum files and the module cache.
|
||||
# These checksums are included in the FIPS security policy
|
||||
# (validation instructions sent to the lab) and MUST NOT CHANGE.
|
||||
# That is, the zip files themselves must not change.
|
||||
@@ -9,5 +12,5 @@
|
||||
#
|
||||
# go test cmd/go/internal/fips140 -update
|
||||
#
|
||||
v1.0.0-c2097c7c.zip daf3614e0406f67ae6323c902db3f953a1effb199142362a039e7526dfb9368b
|
||||
v1.26.0.zip 9b28f847fdf1db4a36cb2b2f8ec09443c039383f085630a03ecfaddf6db7ea23
|
||||
v1.0.0-c2097c7c.zip daf3614e0406f67ae6323c902db3f953a1effb199142362a039e7526dfb9368b h1:YapsF69QWGwygaBqdI0rp7IfE5a6tAwepNAHONVcWqQ=
|
||||
v1.26.0.zip 9b28f847fdf1db4a36cb2b2f8ec09443c039383f085630a03ecfaddf6db7ea23 h1:dtoPX1ALGGp4rMLzyh6oqIkYRXnXxRkpPWu56l5DFpM=
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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.
|
||||
// Package fips140 implements support for the GOFIPS140 build setting.
|
||||
//
|
||||
// The GOFIPS140 build setting controls two aspects of the build:
|
||||
//
|
||||
@@ -230,48 +230,62 @@ func initDir() {
|
||||
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)
|
||||
}
|
||||
sumfile := filepath.Join(cfg.GOROOT, "lib/fips140/fips140.sum")
|
||||
_, ziphash, err := lookupZipSum(sumfile, filepath.Base(file))
|
||||
if err != nil {
|
||||
base.Fatalf("go: verifying GOFIPS140=%v: %v", v, err)
|
||||
}
|
||||
|
||||
zdir, err := modfetch.NewFetcher().Unzip(ctx, mod, file)
|
||||
// fips140.sum records both the SHA-256 hash of the zip file and its
|
||||
// go.sum-style module zip hash. Unzip uses the cached copy in the
|
||||
// module cache only if the cache records that module zip hash for it,
|
||||
// and otherwise discards the copy and unpacks the snapshot again.
|
||||
//
|
||||
// The FIPS 140-3 Security Policy requires checking the SHA-256 hash
|
||||
// of the zip file. Unzip calls verify exactly when it is about to
|
||||
// unpack the zip file, so the hash is checked once per unpacking
|
||||
// (whatever the reason for it) rather than on every go command.
|
||||
verify := func() error { return verifyZipSum(file, sumfile) }
|
||||
zdir, err := modfetch.NewFetcher().Unzip(ctx, mod, file, ziphash, verify)
|
||||
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 {
|
||||
// lookupZipSum returns the SHA-256 hash and module zip hash recorded for
|
||||
// name in sumfile, which is expected to be in the format of
|
||||
// GOROOT/lib/fips140/fips140.sum: "NAME SHA256HEX H1HASH" lines, with "#"
|
||||
// comments. H1HASH is the module zip hash in the format used by go.sum.
|
||||
func lookupZipSum(sumfile, name string) (sha256hex, ziphash string, err error) {
|
||||
sums, err := os.ReadFile(sumfile)
|
||||
if err != nil {
|
||||
return err
|
||||
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 {
|
||||
f := strings.Fields(line)
|
||||
if len(f) != 3 || f[0] != name {
|
||||
continue
|
||||
}
|
||||
if n == name {
|
||||
want = strings.TrimSpace(h)
|
||||
break
|
||||
if !strings.HasPrefix(f[2], "h1:") {
|
||||
return "", "", fmt.Errorf("malformed module zip hash %q for %s in %s", f[2], name, sumfile)
|
||||
}
|
||||
return f[1], f[2], nil
|
||||
}
|
||||
if want == "" {
|
||||
return fmt.Errorf("no SHA-256 hash for %s in %s", name, sumfile)
|
||||
return "", "", fmt.Errorf("no entry for %s in %s", name, sumfile)
|
||||
}
|
||||
|
||||
// verifyZipSum checks that the SHA-256 hash of zipfile matches the entry
|
||||
// for its base name in sumfile.
|
||||
func verifyZipSum(zipfile, sumfile string) error {
|
||||
name := filepath.Base(zipfile)
|
||||
want, _, err := lookupZipSum(sumfile, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Open(zipfile)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/mod/sumdb/dirhash"
|
||||
)
|
||||
|
||||
var update = flag.Bool("update", false, "update GOROOT/lib/fips140/fips140.sum")
|
||||
@@ -33,8 +35,8 @@ func TestSums(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
format := func(name string, sum [32]byte) string {
|
||||
return fmt.Sprintf("%s %x\n", name, sum[:])
|
||||
format := func(name string, sum [32]byte, ziphash string) string {
|
||||
return fmt.Sprintf("%s %x %s\n", name, sum[:], ziphash)
|
||||
}
|
||||
|
||||
want := make(map[string]string)
|
||||
@@ -43,8 +45,12 @@ func TestSums(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ziphash, err := dirhash.HashZip(zip, dirhash.DefaultHash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := filepath.Base(zip)
|
||||
want[name] = format(name, sha256.Sum256(data))
|
||||
want[name] = format(name, sha256.Sum256(data), ziphash)
|
||||
}
|
||||
|
||||
// Process diff, deleting or correcting stale lines.
|
||||
@@ -117,8 +123,13 @@ func TestVerifyZipSum(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
zeroSum = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
ziphash = "h1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
)
|
||||
|
||||
// Matching hash with comments and a second entry passes.
|
||||
write(fmt.Sprintf("# comment\n\nv1.2.3.zip %x\nother.zip 0000000000000000000000000000000000000000000000000000000000000000\n", sum[:]))
|
||||
write(fmt.Sprintf("# comment\n\nv1.2.3.zip %x %s\nother.zip %s h1:other=\n", sum[:], ziphash, zeroSum))
|
||||
if err := verifyZipSum(zipfile, sumfile); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -130,7 +141,7 @@ func TestVerifyZipSum(t *testing.T) {
|
||||
}
|
||||
|
||||
// Wrong hash fails.
|
||||
write("v1.2.3.zip 0000000000000000000000000000000000000000000000000000000000000000\n")
|
||||
write(fmt.Sprintf("v1.2.3.zip %s %s\n", zeroSum, ziphash))
|
||||
if err := verifyZipSum(zipfile, sumfile); err == nil {
|
||||
t.Errorf("expected error when hash does not match")
|
||||
}
|
||||
|
||||
@@ -115,15 +115,10 @@ func DownloadDir(ctx context.Context, m module.Version) (string, error) {
|
||||
return dir, err
|
||||
}
|
||||
|
||||
// Special case: ziphash is not required for the golang.org/fips140 module,
|
||||
// because it is unpacked from a file in GOROOT, not downloaded.
|
||||
// We've already checked that it's not a partial unpacking, so we're happy.
|
||||
if m.Path == "golang.org/fips140" {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// Check if a .ziphash file exists. It should be created before the
|
||||
// zip is extracted, but if it was deleted (by another program?), we need
|
||||
// Check if a .ziphash file exists. For downloaded modules it is created
|
||||
// before the zip is extracted; for GOFIPS140 snapshots, which Fetcher.Unzip
|
||||
// unpacks from GOROOT, it is created only after extraction completes.
|
||||
// Either way, if it is missing (deleted by another program?), we need
|
||||
// to re-calculate it. Note that checkMod will repopulate the ziphash
|
||||
// file if it doesn't exist, but if the module is excluded by checks
|
||||
// through GONOSUMDB or GOPRIVATE, that check and repopulation won't happen.
|
||||
|
||||
@@ -7,6 +7,15 @@ package modfetch
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"cmd/go/internal/base"
|
||||
"cmd/go/internal/cfg"
|
||||
"cmd/go/internal/fsys"
|
||||
"cmd/go/internal/gover"
|
||||
"cmd/go/internal/lockedfile"
|
||||
"cmd/go/internal/str"
|
||||
"cmd/go/internal/trace"
|
||||
"cmd/internal/par"
|
||||
"cmd/internal/robustio"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
@@ -20,22 +29,15 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"cmd/go/internal/base"
|
||||
"cmd/go/internal/cfg"
|
||||
"cmd/go/internal/fsys"
|
||||
"cmd/go/internal/gover"
|
||||
"cmd/go/internal/lockedfile"
|
||||
"cmd/go/internal/str"
|
||||
"cmd/go/internal/trace"
|
||||
"cmd/internal/par"
|
||||
"cmd/internal/robustio"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/mod/sumdb/dirhash"
|
||||
modzip "golang.org/x/mod/zip"
|
||||
)
|
||||
|
||||
var ErrToolchain = errors.New("internal error: invalid operation on toolchain module")
|
||||
var (
|
||||
ErrToolchain = errors.New("internal error: invalid operation on toolchain module")
|
||||
ErrFIPS140 = errors.New("golang.org/fips140 is bundled with the Go distribution and cannot be downloaded")
|
||||
)
|
||||
|
||||
// Download downloads the specific module version to the
|
||||
// local download cache and returns the name of the directory
|
||||
@@ -44,6 +46,9 @@ func (f *Fetcher) Download(ctx context.Context, mod module.Version) (dir string,
|
||||
if gover.IsToolchain(mod.Path) {
|
||||
return "", ErrToolchain
|
||||
}
|
||||
if mod.Path == "golang.org/fips140" {
|
||||
return "", ErrFIPS140
|
||||
}
|
||||
if err := checkCacheDir(ctx); err != nil {
|
||||
base.Fatal(err)
|
||||
}
|
||||
@@ -70,10 +75,29 @@ func (f *Fetcher) Download(ctx context.Context, mod module.Version) (dir string,
|
||||
})
|
||||
}
|
||||
|
||||
// Unzip is like Download but is given the explicit zip file to use,
|
||||
// rather than downloading it. This is used for the GOFIPS140 zip files,
|
||||
// which ship in the Go distribution itself.
|
||||
func (f *Fetcher) Unzip(ctx context.Context, mod module.Version, zipfile string) (dir string, err error) {
|
||||
// Unzip is like Download but for GOFIPS140 zip files which ship with
|
||||
// the Go distribution itself.
|
||||
//
|
||||
// Unzip performs a check like the go.sum check for downloaded modules:
|
||||
// A cached copy of mod is used only if the module cache records ziphash
|
||||
// as its module zip hash. As with go.sum, this rejects a cached copy
|
||||
// that was populated from some other source; it does not detect
|
||||
// modifications made to the module cache directory itself.
|
||||
//
|
||||
// Otherwise, any existing copy is discarded, zipfile is unpacked
|
||||
// again, and ziphash is stamped upon success.
|
||||
//
|
||||
// If verify is non-nil, it is called before zipfile is unpacked.
|
||||
// Unzip does not call verify when it returns a cached copy.
|
||||
//
|
||||
// If verify returns an error, the module cache is left untouched and
|
||||
// Unzip returns that error.
|
||||
//
|
||||
// ziphash must be non-empty.
|
||||
func (f *Fetcher) Unzip(ctx context.Context, mod module.Version, zipfile, ziphash string, verify func() error) (dir string, err error) {
|
||||
if ziphash == "" {
|
||||
return "", module.VersionError(mod, errors.New("internal error: Unzip called with empty module zip hash"))
|
||||
}
|
||||
if err := checkCacheDir(ctx); err != nil {
|
||||
base.Fatal(err)
|
||||
}
|
||||
@@ -85,15 +109,47 @@ func (f *Fetcher) Unzip(ctx context.Context, mod module.Version, zipfile string)
|
||||
dir, err = DownloadDir(ctx, mod)
|
||||
if err == nil {
|
||||
// The directory has already been completely extracted (no .partial file exists).
|
||||
return dir, nil
|
||||
ok, err := haveZipHash(ctx, mod, ziphash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ok {
|
||||
return dir, nil
|
||||
}
|
||||
} else if dir == "" || !errors.Is(err, fs.ErrNotExist) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return unzip(ctx, mod, zipfile)
|
||||
return unzip(ctx, mod, zipfile, ziphash, verify)
|
||||
})
|
||||
}
|
||||
|
||||
func haveZipHash(ctx context.Context, mod module.Version, ziphash string) (bool, error) {
|
||||
path, err := CachePath(ctx, mod, "ziphash")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
data, err := lockedfile.Read(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.TrimSpace(string(data)) == ziphash, nil
|
||||
}
|
||||
|
||||
func writeZipHash(ctx context.Context, mod module.Version, ziphash string) error {
|
||||
path, err := CachePath(ctx, mod, "ziphash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
return lockedfile.Write(path, strings.NewReader(ziphash), 0o666)
|
||||
}
|
||||
|
||||
func (f *Fetcher) download(ctx context.Context, mod module.Version) (dir string, err error) {
|
||||
ctx, span := trace.StartSpan(ctx, "modfetch.download "+mod.String())
|
||||
defer span.Done()
|
||||
@@ -114,10 +170,16 @@ func (f *Fetcher) download(ctx context.Context, mod module.Version) (dir string,
|
||||
return "", err
|
||||
}
|
||||
|
||||
return unzip(ctx, mod, zipfile)
|
||||
return unzip(ctx, mod, zipfile, "", nil)
|
||||
}
|
||||
|
||||
func unzip(ctx context.Context, mod module.Version, zipfile string) (dir string, err error) {
|
||||
// unzip extracts zipfile into the module cache directory for mod,
|
||||
// unless a complete copy is already there (and, if ziphash is non-empty,
|
||||
// the module cache records ziphash for it).
|
||||
//
|
||||
// If verify is non-nil, it is called once the decision to extract has
|
||||
// been made, before any existing copy is discarded.
|
||||
func unzip(ctx context.Context, mod module.Version, zipfile, ziphash string, verify func() error) (dir string, err error) {
|
||||
unlock, err := lockVersion(ctx, mod)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -130,10 +192,29 @@ func unzip(ctx context.Context, mod module.Version, zipfile string) (dir string,
|
||||
// Check whether the directory was populated while we were waiting on the lock.
|
||||
dir, dirErr := DownloadDir(ctx, mod)
|
||||
if dirErr == nil {
|
||||
return dir, nil
|
||||
if ziphash == "" {
|
||||
return dir, nil
|
||||
}
|
||||
ok, err := haveZipHash(ctx, mod, ziphash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ok {
|
||||
return dir, nil
|
||||
}
|
||||
dirErr = &DownloadDirPartialError{dir, errors.New("ziphash file does not match")}
|
||||
}
|
||||
_, dirExists := dirErr.(*DownloadDirPartialError)
|
||||
|
||||
// We are going to extract zipfile. Verify it first, while the module
|
||||
// cache is still untouched, so that a bad zip file never costs us a
|
||||
// cached copy and a good one is verified at most once per extraction.
|
||||
if verify != nil {
|
||||
if err := verify(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any remaining temporary directories created by old versions
|
||||
// (before 1.16), as well as partially extracted directories (indicated by
|
||||
// DownloadDirPartialError, usually because of a .partial file). This is only
|
||||
@@ -151,6 +232,19 @@ func unzip(ctx context.Context, mod module.Version, zipfile string) (dir string,
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
// Remove any recorded module zip hash before extracting, and record the
|
||||
// new one only after the .partial file is removed (below). That way a
|
||||
// .ziphash file for the module exists only beside a completely extracted
|
||||
// directory, no matter where an unpack is interrupted.
|
||||
if ziphash != "" {
|
||||
hashPath, err := CachePath(ctx, mod, "ziphash")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := robustio.RemoveAll(hashPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
partialPath, err := CachePath(ctx, mod, "partial")
|
||||
if err != nil {
|
||||
@@ -187,6 +281,13 @@ func unzip(ctx context.Context, mod module.Version, zipfile string) (dir string,
|
||||
if err := os.Remove(partialPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// The directory is complete: record the module zip hash of the zip
|
||||
// file it was extracted from. See the removal above.
|
||||
if ziphash != "" {
|
||||
if err := writeZipHash(ctx, mod, ziphash); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if !cfg.ModCacheRW {
|
||||
makeDirsReadOnly(dir)
|
||||
@@ -199,6 +300,10 @@ var downloadZipCache par.ErrCache[module.Version, string]
|
||||
// DownloadZip downloads the specific module version to the
|
||||
// local zip cache and returns the name of the zip file.
|
||||
func (f *Fetcher) DownloadZip(ctx context.Context, mod module.Version) (zipfile string, err error) {
|
||||
if mod.Path == "golang.org/fips140" {
|
||||
return "", ErrFIPS140
|
||||
}
|
||||
|
||||
// The par.Cache here avoids duplicate work.
|
||||
return downloadZipCache.Do(mod, func() (string, error) {
|
||||
zipfile, err := CachePath(ctx, mod, "zip")
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2026 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.
|
||||
|
||||
//go:build !js && !wasip1
|
||||
|
||||
package modfetch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/mod/sumdb/dirhash"
|
||||
modzip "golang.org/x/mod/zip"
|
||||
)
|
||||
|
||||
func TestUnzipVerify(t *testing.T) {
|
||||
var (
|
||||
ctx = t.Context()
|
||||
src = t.TempDir()
|
||||
mod = module.Version{Path: "example.com/unzip", Version: "v1.0.0"}
|
||||
)
|
||||
if err := os.WriteFile(filepath.Join(src, "go.mod"), []byte("module example.com/unzip\n"), 0o666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
zipfile := filepath.Join(t.TempDir(), "v1.0.0.zip")
|
||||
zf, err := os.Create(zipfile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := modzip.CreateFromDir(zf, mod, src); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := zf.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ziphash, err := dirhash.HashZip(zipfile, dirhash.DefaultHash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
unzip := func(verify func() error) (string, error) {
|
||||
dir, err := NewFetcher().Unzip(ctx, mod, zipfile, ziphash, verify)
|
||||
if dir != "" {
|
||||
t.Cleanup(func() { RemoveAll(dir) })
|
||||
}
|
||||
return dir, err
|
||||
}
|
||||
|
||||
var (
|
||||
errVerify = errors.New("verify failed")
|
||||
verifyFnFails = func() error { return errVerify }
|
||||
verifyFnPasses = func() error { return nil }
|
||||
)
|
||||
if _, err := unzip(verifyFnFails); !errors.Is(err, errVerify) {
|
||||
t.Fatalf("unzip with empty cache: got %v, want %v", err, errVerify)
|
||||
}
|
||||
if _, err := DownloadDir(ctx, mod); !errors.Is(err, fs.ErrNotExist) {
|
||||
t.Fatalf("DownloadDir after failed verify: got %v, want %v", err, fs.ErrNotExist)
|
||||
}
|
||||
|
||||
if _, err := unzip(verifyFnPasses); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := DownloadDir(ctx, mod); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ok, err := haveZipHash(ctx, mod, ziphash); err != nil || !ok {
|
||||
t.Fatalf("haveZipHash = (%v, %v), want (true, nil)", ok, err)
|
||||
}
|
||||
|
||||
if _, err := unzip(verifyFnFails); err != nil {
|
||||
t.Fatalf("unzip with cached copy: got %v, want nil", err)
|
||||
}
|
||||
|
||||
if err := writeZipHash(ctx, mod, "h1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := unzip(verifyFnFails); !errors.Is(err, errVerify) {
|
||||
t.Fatalf("unzip with mismatched ziphash: got %v, want %v", err, errVerify)
|
||||
}
|
||||
if _, err := DownloadDir(ctx, mod); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := unzip(verifyFnPasses); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := DownloadDir(ctx, mod); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ok, err := haveZipHash(ctx, mod, ziphash); err != nil || !ok {
|
||||
t.Errorf("haveZipHash after re-unpack: got (%v, %v), want (true, nil)", ok, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# The module cache copy of a FIPS snapshot is used only if the module
|
||||
# cache records the module zip hash from GOROOT/lib/fips140/fips140.sum
|
||||
# for it, the way downloaded modules are checked against go.sum.
|
||||
# Otherwise it is discarded and the snapshot is unpacked again.
|
||||
#
|
||||
# This detects a cache entry that was not unpacked from the bundled
|
||||
# snapshot (or whose hash record is missing). Like go.sum, it does not
|
||||
# protect the module cache from being modified in place: the replaced
|
||||
# source file below stands in for stale contents, not an attacker.
|
||||
|
||||
env snap=v1.26.0
|
||||
env GOFIPS140=$snap
|
||||
env GOMODCACHE=$WORK/modcache
|
||||
env GOFLAGS=-modcacherw
|
||||
|
||||
# Go+BoringCrypto conflicts with GOFIPS140.
|
||||
[GOEXPERIMENT:boringcrypto] skip
|
||||
|
||||
env ziphash=$GOMODCACHE/cache/download/golang.org/fips140/@v/$snap.ziphash
|
||||
env srcfile=$GOMODCACHE/golang.org/fips140@$snap/fips140/$snap/sha256/sha256.go
|
||||
|
||||
# unpacking the snapshot records its module zip hash in the module cache
|
||||
go list -f '{{.DefaultGODEBUG}}'
|
||||
stdout fips140=on
|
||||
exists $ziphash
|
||||
exists $srcfile
|
||||
grep '^h1:dtoPX1ALGGp4rMLzyh6oqIkYRXnXxRkpPWu56l5DFpM=$' $ziphash
|
||||
cp $ziphash good.ziphash
|
||||
|
||||
# a recorded hash that does not match fips140.sum
|
||||
# discards the cached copy and unpacks the snapshot again
|
||||
cp bad.ziphash $ziphash
|
||||
rm $srcfile
|
||||
cp stale/sha256.go $srcfile
|
||||
go list -f '{{.DefaultGODEBUG}}'
|
||||
stdout fips140=on
|
||||
exists $srcfile
|
||||
! grep stale $srcfile
|
||||
cmp $ziphash good.ziphash
|
||||
|
||||
# so does a missing hash
|
||||
rm $ziphash
|
||||
rm $srcfile
|
||||
cp stale/sha256.go $srcfile
|
||||
go list -f '{{.DefaultGODEBUG}}'
|
||||
stdout fips140=on
|
||||
exists $srcfile
|
||||
! grep stale $srcfile
|
||||
cmp $ziphash good.ziphash
|
||||
|
||||
-- go.mod --
|
||||
module m
|
||||
-- x.go --
|
||||
package main
|
||||
import _ "crypto/sha256"
|
||||
func main() {
|
||||
}
|
||||
-- bad.ziphash --
|
||||
h1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-- stale/sha256.go --
|
||||
package sha256 // stale
|
||||
Reference in new issue
Block a user