// Copyright 2025 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 !fips140v1.0

package fipstest

import (
	"bytes"
	"crypto/internal/cryptotest"
	entropy "crypto/internal/entropy/v1.0.0"
	"crypto/internal/fips140/drbg"
	"crypto/rand"
	"crypto/sha256"
	"crypto/sha512"
	"encoding/hex"
	"flag"
	"fmt"
	"internal/testenv"
	"io/fs"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"
)

var flagEntropySamples = flag.String("entropy-samples", "", "store entropy samples with the provided `suffix`")
var flagNISTSP80090B = flag.Bool("nist-sp800-90b", false, "run NIST SP 800-90B tests (requires docker)")

func TestEntropySamples(t *testing.T) {
	cryptotest.MustSupportFIPS140(t)
	now := time.Now().UTC()

	seqSampleCount := 1_000_000
	if *flagEntropySamples != "" {
		// The lab requested 300 million samples for a new heuristic procedure.
		seqSampleCount = 300_000_000
	}
	seqSamples := make([]uint8, seqSampleCount)
	samplesOrTryAgain(t, seqSamples)
	seqSamplesName := fmt.Sprintf("entropy_samples_sequential_%s_%s_%s_%s_%s.bin", entropy.Version(),
		runtime.GOOS, runtime.GOARCH, *flagEntropySamples, now.Format("20060102T150405Z"))
	if *flagEntropySamples != "" {
		if err := os.WriteFile(seqSamplesName, seqSamples, 0644); err != nil {
			t.Fatalf("failed to write samples to %q: %v", seqSamplesName, err)
		}
		t.Logf("wrote %s", seqSamplesName)
	}

	var restartSamples [1000][1000]uint8
	for i := range restartSamples {
		var samples [1024]uint8
		samplesOrTryAgain(t, samples[:])
		copy(restartSamples[i][:], samples[:])
	}
	restartSamplesName := fmt.Sprintf("entropy_samples_restart_%s_%s_%s_%s_%s.bin", entropy.Version(),
		runtime.GOOS, runtime.GOARCH, *flagEntropySamples, now.Format("20060102T150405Z"))
	if *flagEntropySamples != "" {
		f, err := os.Create(restartSamplesName)
		if err != nil {
			t.Fatalf("failed to create %q: %v", restartSamplesName, err)
		}
		for i := range restartSamples {
			if _, err := f.Write(restartSamples[i][:]); err != nil {
				t.Fatalf("failed to write samples to %q: %v", restartSamplesName, err)
			}
		}
		if err := f.Close(); err != nil {
			t.Fatalf("failed to close %q: %v", restartSamplesName, err)
		}
		t.Logf("wrote %s", restartSamplesName)
	}

	if *flagNISTSP80090B {
		if *flagEntropySamples == "" {
			t.Fatalf("-nist-sp800-90b requires -entropy-samples to be set too")
		}

		// Check if the nist-sp800-90b docker image is already present,
		// and build it otherwise.
		if err := testenv.Command(t,
			"docker", "image", "inspect", "nist-sp800-90b",
		).Run(); err != nil {
			t.Logf("building nist-sp800-90b docker image")
			dockerfile := filepath.Join(t.TempDir(), "Dockerfile.SP800-90B_EntropyAssessment")
			if err := os.WriteFile(dockerfile, []byte(NISTSP80090BDockerfile), 0644); err != nil {
				t.Fatalf("failed to write Dockerfile: %v", err)
			}
			out, err := testenv.Command(t,
				"docker", "build", "-t", "nist-sp800-90b", "-f", dockerfile, "/var/empty",
			).CombinedOutput()
			if err != nil {
				t.Fatalf("failed to build nist-sp800-90b docker image: %v\n%s", err, out)
			}
		}

		pwd, err := os.Getwd()
		if err != nil {
			t.Fatalf("failed to get current working directory: %v", err)
		}
		t.Logf("running ea_non_iid analysis")
		out, err := testenv.Command(t,
			"docker", "run", "--rm", "-v", fmt.Sprintf("%s:%s", pwd, pwd), "-w", pwd,
			"nist-sp800-90b", "ea_non_iid", seqSamplesName, "8",
		).CombinedOutput()
		if err != nil {
			t.Fatalf("ea_non_iid failed: %v\n%s", err, out)
		}
		t.Logf("\n%s", out)

		H_I := string(out)
		H_I = strings.TrimSpace(H_I[strings.LastIndexByte(H_I, ' ')+1:])
		t.Logf("running ea_restart analysis with H_I = %s", H_I)
		out, err = testenv.Command(t,
			"docker", "run", "--rm", "-v", fmt.Sprintf("%s:%s", pwd, pwd), "-w", pwd,
			"nist-sp800-90b", "ea_restart", restartSamplesName, "8", H_I,
		).CombinedOutput()
		if err != nil {
			t.Fatalf("ea_restart failed: %v\n%s", err, out)
		}
		t.Logf("\n%s", out)
	}
}

var NISTSP80090BDockerfile = `
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y build-essential git \
    libbz2-dev libdivsufsort-dev libjsoncpp-dev libgmp-dev libmpfr-dev libssl-dev \
    && rm -rf /var/lib/apt/lists/*
RUN git clone --depth 1 https://github.com/usnistgov/SP800-90B_EntropyAssessment.git
RUN cd SP800-90B_EntropyAssessment && git checkout 8924f158c97e7b805e0f95247403ad4c44b9cd6f
WORKDIR ./SP800-90B_EntropyAssessment/cpp/
RUN make all
RUN cd selftest && ./selftest
RUN cp ea_non_iid ea_restart /usr/local/bin/
`

var memory entropy.ScratchBuffer

// samplesOrTryAgain calls entropy.Samples up to 10 times until it succeeds.
// Samples has a non-negligible chance of failing the health tests, as required
// by SP 800-90B.
func samplesOrTryAgain(t *testing.T, samples []uint8) {
	t.Helper()
	for range 10 {
		if err := entropy.Samples(samples, &memory); err != nil {
			t.Logf("entropy.Samples() failed: %v", err)
			continue
		}
		return
	}
	t.Fatal("entropy.Samples() failed 10 times in a row")
}

func TestEntropySHA384(t *testing.T) {
	var input [1024]uint8
	for i := range input {
		input[i] = uint8(i)
	}
	want := sha512.Sum384(input[:])
	got := entropy.SHA384(&input)
	if got != want {
		t.Errorf("SHA384() = %x, want %x", got, want)
	}

	for l := range 1024*3 + 1 {
		input := make([]byte, l)
		rand.Read(input)
		want := sha512.Sum384(input)
		got := entropy.TestingOnlySHA384(input)
		if got != want {
			t.Errorf("TestingOnlySHA384(%d bytes) = %x, want %x", l, got, want)
		}
	}
}

func TestEntropyRepetitionCountTest(t *testing.T) {
	good := bytes.Repeat(append(bytes.Repeat([]uint8{42}, 40), 1), 100)
	if err := entropy.RepetitionCountTest(good); err != nil {
		t.Errorf("RepetitionCountTest(good) = %v, want nil", err)
	}

	bad := bytes.Repeat([]uint8{0}, 40)
	bad = append(bad, bytes.Repeat([]uint8{1}, 40)...)
	bad = append(bad, bytes.Repeat([]uint8{42}, 41)...)
	bad = append(bad, bytes.Repeat([]uint8{2}, 40)...)
	if err := entropy.RepetitionCountTest(bad); err == nil {
		t.Error("RepetitionCountTest(bad) = nil, want error")
	}

	bad = bytes.Repeat([]uint8{42}, 41)
	if err := entropy.RepetitionCountTest(bad); err == nil {
		t.Error("RepetitionCountTest(bad) = nil, want error")
	}
}

func TestEntropyAdaptiveProportionTest(t *testing.T) {
	good := bytes.Repeat([]uint8{0}, 409)
	good = append(good, bytes.Repeat([]uint8{1}, 512-409)...)
	good = append(good, bytes.Repeat([]uint8{0}, 409)...)
	if err := entropy.AdaptiveProportionTest(good); err != nil {
		t.Errorf("AdaptiveProportionTest(good) = %v, want nil", err)
	}

	// These fall out of the window.
	bad := bytes.Repeat([]uint8{1}, 100)
	bad = append(bad, bytes.Repeat([]uint8{1, 2, 3, 4, 5, 6}, 100)...)
	// These are in the window.
	bad = append(bad, bytes.Repeat([]uint8{42}, 410)...)
	if err := entropy.AdaptiveProportionTest(bad[:len(bad)-1]); err != nil {
		t.Errorf("AdaptiveProportionTest(bad[:len(bad)-1]) = %v, want nil", err)
	}
	if err := entropy.AdaptiveProportionTest(bad); err == nil {
		t.Error("AdaptiveProportionTest(bad) = nil, want error")
	}
}

func TestEntropyUnchanged(t *testing.T) {
	testenv.MustHaveSource(t)

	h := sha256.New()
	root := os.DirFS("../entropy/v1.0.0")
	if err := fs.WalkDir(root, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		data, err := fs.ReadFile(root, path)
		if err != nil {
			return err
		}
		t.Logf("Hashing %s (%d bytes)", path, len(data))
		fmt.Fprintf(h, "%s %d\n", path, len(data))
		h.Write(data)
		return nil
	}); err != nil {
		t.Fatalf("WalkDir: %v", err)
	}

	// The crypto/internal/entropy/v1.0.0 package is certified as a FIPS 140-3
	// entropy source through the Entropy Source Validation program,
	// independently of the FIPS 140-3 module. It must not change even across
	// FIPS 140-3 module versions, in order to reuse the ESV certificate.
	exp := "2541273241ae8aafe55026328354ed3799df1e2fb308b2097833203a42911b53"
	if got := hex.EncodeToString(h.Sum(nil)); got != exp {
		t.Errorf("hash of crypto/internal/entropy/v1.0.0 = %s, want %s", got, exp)
	}
}

func TestEntropyRace(t *testing.T) {
	// Check that concurrent calls to Seed don't trigger the race detector.
	for range 16 {
		go func() {
			_, _ = entropy.Seed(&memory)
		}()
	}
	// Same, with the higher-level DRBG.
	for range 16 {
		go func() {
			var b [64]byte
			drbg.Read(b[:])
		}()
	}
}

var sink byte

func BenchmarkEntropySeed(b *testing.B) {
	for b.Loop() {
		seed, err := entropy.Seed(&memory)
		if err != nil {
			b.Fatalf("entropy.Seed() failed: %v", err)
		}
		sink ^= seed[0]
	}
}
