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:
Neal Patel committed 2026-10-02 11:04:39 -07:00
1 parent ac7ada9af9
commit 819eab448b
7 files changed
+350 -60

No files matched your search

+6 -3
View File
@@ -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=
+37 -23
View File
@@ -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 {
+16 -5
View File
@@ -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")
}
+4 -9
View File
@@ -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.
+125 -20
View File
@@ -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")
+101
View File
@@ -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)
}
}
+61
View File
@@ -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