// Copyright 2024 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.

// Package fips implements support for the GOFIPS140 build setting.
//
// The GOFIPS140 build setting controls two aspects of the build:
//
//   - Whether binaries are built to default to running in FIPS-140 mode,
//     meaning whether they default to GODEBUG=fips140=on or =off.
//
//   - Which copy of the crypto/internal/fips140 source code to use.
//     The default is obviously GOROOT/src/crypto/internal/fips140,
//     but earlier snapshots that have differing levels of external
//     validation and certification are stored in GOROOT/lib/fips140
//     and can be substituted into the build instead.
//
// This package provides the logic needed by the rest of the go command
// to make those decisions and implement the resulting policy.
//
// [Init] must be called to initialize the FIPS logic. It may fail and
// call base.Fatalf.
//
// When GOFIPS140=off, [Enabled] returns false, and the build is
// unchanged from its usual behaviors.
//
// When GOFIPS140 is anything else, [Enabled] returns true, and the build
// sets the default GODEBUG to include fips140=on. This will make
// binaries change their behavior at runtime to confirm to various
// FIPS-140 details. [cmd/go/internal/load.defaultGODEBUG] calls
// [fips.Enabled] when preparing the default settings.
//
// For all builds, FIPS code and data is laid out in contiguous regions
// that are conceptually concatenated into a "fips object file" that the
// linker hashes and then binaries can re-hash at startup to detect
// corruption of those symbols. When [Enabled] is true, the link step
// passes -fipso={a.Objdir}/fips.o to the linker to save a copy of the
// fips.o file. Since the first build target always uses a.Objdir set to
// $WORK/b001, a build like
//
//	GOFIPS140=latest go build -work my/binary
//
// will leave fips.o behind in $WORK/b001
// (unless the build result is cached, of course).
//
// When GOFIPS140 is set to something besides off and latest, [Snapshot]
// returns true, indicating that the build should replace the latest copy
// of crypto/internal/fips140 with an earlier snapshot. The reason to do
// this is to use a copy that has been through additional lab validation
// (an "in-process" module) or NIST certification (a "certified" module).
// The snapshots are stored in GOROOT/lib/fips140 in module zip form.
// When a snapshot is being used, Init unpacks it into the module cache
// and then uses that directory as the source location.
//
// A FIPS snapshot like v1.2.3 is integrated into the build in two different ways.
//
// First, the snapshot's fips140 directory replaces crypto/internal/fips140
// using fsys.Bind. The effect is to appear to have deleted crypto/internal/fips140
// and everything below it, replacing it with the single subdirectory
// crypto/internal/fips140/v1.2.3, which now has the FIPS packages.
// This virtual file system replacement makes patterns like std and crypto...
// automatically see the snapshot packages instead of the original packages
// as they walk GOROOT/src/crypto/internal/fips140.
//
// Second, ResolveImport is called to resolve an import like crypto/internal/fips140/sha256.
// When snapshot v1.2.3 is being used, ResolveImport translates that path to
// crypto/internal/fips140/v1.2.3/sha256 and returns the actual source directory
// in the unpacked snapshot. Using the actual directory instead of the
// virtual directory GOROOT/src/crypto/internal/fips140/v1.2.3 makes sure
// that other tools using go list -json output can find the sources,
// as well as making sure builds have a real directory in which to run the
// assembler, compiler, and so on. The translation of the import path happens
// in the same code that handles mapping golang.org/x/mod to
// cmd/vendor/golang.org/x/mod when building commands.
//
// It is not strictly required to include v1.2.3 in the import path when using
// a snapshot - we could make things work without doing that - but including
// the v1.2.3 gives a different version of the code a different name, which is
// always a good general rule. In particular, it will mean that govulncheck need
// not have any special cases for crypto/internal/fips140 at all. The reports simply
// need to list the relevant symbols in a given Go version. (For example, if a bug
// is only in the in-tree copy but not the snapshots, it doesn't list the snapshot
// symbols; if it's in any snapshots, it has to list the specific snapshot symbols
// in addition to the “normal” symbol.)
package fips140

import (
	"context"
	"crypto/sha256"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"slices"
	"strings"

	"cmd/go/internal/base"
	"cmd/go/internal/cfg"
	"cmd/go/internal/fsys"
	"cmd/go/internal/modfetch"
	"cmd/go/internal/str"

	"golang.org/x/mod/module"
	"golang.org/x/mod/semver"
)

// Init initializes the FIPS settings.
// It must be called before using any other functions in this package.
// If initialization fails, Init calls base.Fatalf.
func Init() {
	if initDone {
		return
	}
	initDone = true
	initVersion()
	initDir()
	if Snapshot() {
		fsys.Bind(Dir(), filepath.Join(cfg.GOROOT, "src/crypto/internal/fips140"))
	}

	// ExperimentErr != nil if GOEXPERIMENT failed to parse. Typically
	// cmd/go main will exit in this case, but it is allowed during
	// toolchain selection, as the GOEXPERIMENT may be valid for the
	// selected toolchain version.
	if cfg.ExperimentErr == nil && cfg.Experiment.BoringCrypto && Enabled() {
		base.Fatalf("go: cannot use GOFIPS140 with GOEXPERIMENT=boringcrypto")
	}
	if slices.Contains(cfg.BuildContext.BuildTags, "purego") && Enabled() {
		base.Fatalf("go: cannot use GOFIPS140 with the purego build tag")
	}
}

var initDone bool

// checkInit panics if Init has not been called.
func checkInit() {
	if !initDone {
		panic("fips: not initialized")
	}
}

// Version reports the GOFIPS140 version in use,
// which is either "off", "latest", or a version like "v1.2.3".
// If GOFIPS140 is set to an alias like "inprocess" or "certified",
// Version returns the underlying version.
func Version() string {
	checkInit()
	return version
}

// Enabled reports whether FIPS mode is enabled at all.
// That is, it reports whether GOFIPS140 is set to something besides "off".
func Enabled() bool {
	checkInit()
	return version != "off"
}

// Snapshot reports whether FIPS mode is using a source snapshot
// rather than $GOROOT/src/crypto/internal/fips140.
// That is, it reports whether GOFIPS140 is set to something besides "latest" or "off".
func Snapshot() bool {
	checkInit()
	return version != "latest" && version != "off"
}

var version string

func initVersion() {
	// For off and latest, use the local source tree.
	v := cfg.GOFIPS140
	if v == "off" || v == "" {
		version = "off"
		return
	}
	if v == "latest" {
		version = "latest"
		return
	}

	// Otherwise version must exist in lib/fips140, either as
	// a .zip (a source snapshot like v1.2.0.zip)
	// or a .txt (a redirect like inprocess.txt, containing a version number).
	if strings.Contains(v, "/") || strings.Contains(v, `\`) || strings.Contains(v, "..") {
		base.Fatalf("go: malformed GOFIPS140 version %q", cfg.GOFIPS140)
	}
	if cfg.GOROOT == "" {
		base.Fatalf("go: missing GOROOT for GOFIPS140")
	}

	file := filepath.Join(cfg.GOROOT, "lib", "fips140", v)
	if data, err := os.ReadFile(file + ".txt"); err == nil {
		v = strings.TrimSpace(string(data))
		file = filepath.Join(cfg.GOROOT, "lib", "fips140", v)
		if _, err := os.Stat(file + ".zip"); err != nil {
			base.Fatalf("go: unknown GOFIPS140 version %q (from %q)", v, cfg.GOFIPS140)
		}
	}

	if _, err := os.Stat(file + ".zip"); err == nil {
		// Found version. Add a build tag.
		cfg.BuildContext.BuildTags = append(cfg.BuildContext.BuildTags, "fips140"+semver.MajorMinor(v))
		version = v
		return
	}

	base.Fatalf("go: unknown GOFIPS140 version %q", v)
}

// Dir reports the directory containing the crypto/internal/fips140 source code.
// If Snapshot() is false, Dir returns GOROOT/src/crypto/internal/fips140.
// Otherwise Dir ensures that the snapshot has been unpacked into the
// module cache and then returns the directory in the module cache
// corresponding to the crypto/internal/fips140 directory.
func Dir() string {
	checkInit()
	return dir
}

var dir string

func initDir() {
	v := version
	if v == "latest" || v == "off" {
		dir = filepath.Join(cfg.GOROOT, "src/crypto/internal/fips140")
		return
	}

	mod := module.Version{Path: "golang.org/fips140", Version: v}
	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)
		}
	}

	zdir, err := modfetch.NewFetcher().Unzip(ctx, mod, file)
	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 {
	sums, err := os.ReadFile(sumfile)
	if err != nil {
		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 {
			continue
		}
		if n == name {
			want = strings.TrimSpace(h)
			break
		}
	}
	if want == "" {
		return fmt.Errorf("no SHA-256 hash for %s in %s", name, sumfile)
	}
	f, err := os.Open(zipfile)
	if err != nil {
		return err
	}
	defer f.Close()
	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return err
	}
	if got := fmt.Sprintf("%x", h.Sum(nil)); got != want {
		return fmt.Errorf("SHA-256 hash of %s is %s, want %s (from %s)", name, got, want, sumfile)
	}
	return nil
}

// ResolveImport resolves the import path imp.
// If it is of the form crypto/internal/fips140/foo
// (not crypto/internal/fips140/v1.2.3/foo)
// and we are using a snapshot, then LookupImport
// rewrites the path to crypto/internal/fips140/v1.2.3/foo
// and returns that path and its location in the unpacked
// FIPS snapshot.
func ResolveImport(imp string) (newPath, dir string, ok bool) {
	checkInit()
	const fips = "crypto/internal/fips140"
	if !Snapshot() || !str.HasPathPrefix(imp, fips) {
		return "", "", false
	}
	fipsv := path.Join(fips, version)
	var sub string
	if str.HasPathPrefix(imp, fipsv) {
		sub = "." + imp[len(fipsv):]
	} else {
		sub = "." + imp[len(fips):]
	}
	newPath = path.Join(fips, version, sub)
	dir = filepath.Join(Dir(), version, sub)
	return newPath, dir, true
}
