Source file src/crypto/internal/fips140test/entropy_test.go

     1  // Copyright 2025 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 !fips140v1.0
     6  
     7  package fipstest
     8  
     9  import (
    10  	"bytes"
    11  	"crypto/internal/cryptotest"
    12  	entropy "crypto/internal/entropy/v1.0.0"
    13  	"crypto/internal/fips140/drbg"
    14  	"crypto/rand"
    15  	"crypto/sha256"
    16  	"crypto/sha512"
    17  	"encoding/hex"
    18  	"flag"
    19  	"fmt"
    20  	"internal/testenv"
    21  	"io/fs"
    22  	"os"
    23  	"path/filepath"
    24  	"runtime"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  )
    29  
    30  var flagEntropySamples = flag.String("entropy-samples", "", "store entropy samples with the provided `suffix`")
    31  var flagNISTSP80090B = flag.Bool("nist-sp800-90b", false, "run NIST SP 800-90B tests (requires docker)")
    32  
    33  func TestEntropySamples(t *testing.T) {
    34  	cryptotest.MustSupportFIPS140(t)
    35  	now := time.Now().UTC()
    36  
    37  	seqSampleCount := 1_000_000
    38  	if *flagEntropySamples != "" {
    39  		// The lab requested 300 million samples for a new heuristic procedure.
    40  		seqSampleCount = 300_000_000
    41  	}
    42  	seqSamples := make([]uint8, seqSampleCount)
    43  	samplesOrTryAgain(t, seqSamples)
    44  	seqSamplesName := fmt.Sprintf("entropy_samples_sequential_%s_%s_%s_%s_%s.bin", entropy.Version(),
    45  		runtime.GOOS, runtime.GOARCH, *flagEntropySamples, now.Format("20060102T150405Z"))
    46  	if *flagEntropySamples != "" {
    47  		if err := os.WriteFile(seqSamplesName, seqSamples, 0644); err != nil {
    48  			t.Fatalf("failed to write samples to %q: %v", seqSamplesName, err)
    49  		}
    50  		t.Logf("wrote %s", seqSamplesName)
    51  	}
    52  
    53  	var restartSamples [1000][1000]uint8
    54  	for i := range restartSamples {
    55  		var samples [1024]uint8
    56  		samplesOrTryAgain(t, samples[:])
    57  		copy(restartSamples[i][:], samples[:])
    58  	}
    59  	restartSamplesName := fmt.Sprintf("entropy_samples_restart_%s_%s_%s_%s_%s.bin", entropy.Version(),
    60  		runtime.GOOS, runtime.GOARCH, *flagEntropySamples, now.Format("20060102T150405Z"))
    61  	if *flagEntropySamples != "" {
    62  		f, err := os.Create(restartSamplesName)
    63  		if err != nil {
    64  			t.Fatalf("failed to create %q: %v", restartSamplesName, err)
    65  		}
    66  		for i := range restartSamples {
    67  			if _, err := f.Write(restartSamples[i][:]); err != nil {
    68  				t.Fatalf("failed to write samples to %q: %v", restartSamplesName, err)
    69  			}
    70  		}
    71  		if err := f.Close(); err != nil {
    72  			t.Fatalf("failed to close %q: %v", restartSamplesName, err)
    73  		}
    74  		t.Logf("wrote %s", restartSamplesName)
    75  	}
    76  
    77  	if *flagNISTSP80090B {
    78  		if *flagEntropySamples == "" {
    79  			t.Fatalf("-nist-sp800-90b requires -entropy-samples to be set too")
    80  		}
    81  
    82  		// Check if the nist-sp800-90b docker image is already present,
    83  		// and build it otherwise.
    84  		if err := testenv.Command(t,
    85  			"docker", "image", "inspect", "nist-sp800-90b",
    86  		).Run(); err != nil {
    87  			t.Logf("building nist-sp800-90b docker image")
    88  			dockerfile := filepath.Join(t.TempDir(), "Dockerfile.SP800-90B_EntropyAssessment")
    89  			if err := os.WriteFile(dockerfile, []byte(NISTSP80090BDockerfile), 0644); err != nil {
    90  				t.Fatalf("failed to write Dockerfile: %v", err)
    91  			}
    92  			out, err := testenv.Command(t,
    93  				"docker", "build", "-t", "nist-sp800-90b", "-f", dockerfile, "/var/empty",
    94  			).CombinedOutput()
    95  			if err != nil {
    96  				t.Fatalf("failed to build nist-sp800-90b docker image: %v\n%s", err, out)
    97  			}
    98  		}
    99  
   100  		pwd, err := os.Getwd()
   101  		if err != nil {
   102  			t.Fatalf("failed to get current working directory: %v", err)
   103  		}
   104  		t.Logf("running ea_non_iid analysis")
   105  		out, err := testenv.Command(t,
   106  			"docker", "run", "--rm", "-v", fmt.Sprintf("%s:%s", pwd, pwd), "-w", pwd,
   107  			"nist-sp800-90b", "ea_non_iid", seqSamplesName, "8",
   108  		).CombinedOutput()
   109  		if err != nil {
   110  			t.Fatalf("ea_non_iid failed: %v\n%s", err, out)
   111  		}
   112  		t.Logf("\n%s", out)
   113  
   114  		H_I := string(out)
   115  		H_I = strings.TrimSpace(H_I[strings.LastIndexByte(H_I, ' ')+1:])
   116  		t.Logf("running ea_restart analysis with H_I = %s", H_I)
   117  		out, err = testenv.Command(t,
   118  			"docker", "run", "--rm", "-v", fmt.Sprintf("%s:%s", pwd, pwd), "-w", pwd,
   119  			"nist-sp800-90b", "ea_restart", restartSamplesName, "8", H_I,
   120  		).CombinedOutput()
   121  		if err != nil {
   122  			t.Fatalf("ea_restart failed: %v\n%s", err, out)
   123  		}
   124  		t.Logf("\n%s", out)
   125  	}
   126  }
   127  
   128  var NISTSP80090BDockerfile = `
   129  FROM ubuntu:24.04
   130  RUN apt-get update && apt-get install -y build-essential git \
   131      libbz2-dev libdivsufsort-dev libjsoncpp-dev libgmp-dev libmpfr-dev libssl-dev \
   132      && rm -rf /var/lib/apt/lists/*
   133  RUN git clone --depth 1 https://github.com/usnistgov/SP800-90B_EntropyAssessment.git
   134  RUN cd SP800-90B_EntropyAssessment && git checkout 8924f158c97e7b805e0f95247403ad4c44b9cd6f
   135  WORKDIR ./SP800-90B_EntropyAssessment/cpp/
   136  RUN make all
   137  RUN cd selftest && ./selftest
   138  RUN cp ea_non_iid ea_restart /usr/local/bin/
   139  `
   140  
   141  var memory entropy.ScratchBuffer
   142  
   143  // samplesOrTryAgain calls entropy.Samples up to 10 times until it succeeds.
   144  // Samples has a non-negligible chance of failing the health tests, as required
   145  // by SP 800-90B.
   146  func samplesOrTryAgain(t *testing.T, samples []uint8) {
   147  	t.Helper()
   148  	for range 10 {
   149  		if err := entropy.Samples(samples, &memory); err != nil {
   150  			t.Logf("entropy.Samples() failed: %v", err)
   151  			continue
   152  		}
   153  		return
   154  	}
   155  	t.Fatal("entropy.Samples() failed 10 times in a row")
   156  }
   157  
   158  func TestEntropySHA384(t *testing.T) {
   159  	var input [1024]uint8
   160  	for i := range input {
   161  		input[i] = uint8(i)
   162  	}
   163  	want := sha512.Sum384(input[:])
   164  	got := entropy.SHA384(&input)
   165  	if got != want {
   166  		t.Errorf("SHA384() = %x, want %x", got, want)
   167  	}
   168  
   169  	for l := range 1024*3 + 1 {
   170  		input := make([]byte, l)
   171  		rand.Read(input)
   172  		want := sha512.Sum384(input)
   173  		got := entropy.TestingOnlySHA384(input)
   174  		if got != want {
   175  			t.Errorf("TestingOnlySHA384(%d bytes) = %x, want %x", l, got, want)
   176  		}
   177  	}
   178  }
   179  
   180  func TestEntropyRepetitionCountTest(t *testing.T) {
   181  	good := bytes.Repeat(append(bytes.Repeat([]uint8{42}, 40), 1), 100)
   182  	if err := entropy.RepetitionCountTest(good); err != nil {
   183  		t.Errorf("RepetitionCountTest(good) = %v, want nil", err)
   184  	}
   185  
   186  	bad := bytes.Repeat([]uint8{0}, 40)
   187  	bad = append(bad, bytes.Repeat([]uint8{1}, 40)...)
   188  	bad = append(bad, bytes.Repeat([]uint8{42}, 41)...)
   189  	bad = append(bad, bytes.Repeat([]uint8{2}, 40)...)
   190  	if err := entropy.RepetitionCountTest(bad); err == nil {
   191  		t.Error("RepetitionCountTest(bad) = nil, want error")
   192  	}
   193  
   194  	bad = bytes.Repeat([]uint8{42}, 41)
   195  	if err := entropy.RepetitionCountTest(bad); err == nil {
   196  		t.Error("RepetitionCountTest(bad) = nil, want error")
   197  	}
   198  }
   199  
   200  func TestEntropyAdaptiveProportionTest(t *testing.T) {
   201  	good := bytes.Repeat([]uint8{0}, 409)
   202  	good = append(good, bytes.Repeat([]uint8{1}, 512-409)...)
   203  	good = append(good, bytes.Repeat([]uint8{0}, 409)...)
   204  	if err := entropy.AdaptiveProportionTest(good); err != nil {
   205  		t.Errorf("AdaptiveProportionTest(good) = %v, want nil", err)
   206  	}
   207  
   208  	// These fall out of the window.
   209  	bad := bytes.Repeat([]uint8{1}, 100)
   210  	bad = append(bad, bytes.Repeat([]uint8{1, 2, 3, 4, 5, 6}, 100)...)
   211  	// These are in the window.
   212  	bad = append(bad, bytes.Repeat([]uint8{42}, 410)...)
   213  	if err := entropy.AdaptiveProportionTest(bad[:len(bad)-1]); err != nil {
   214  		t.Errorf("AdaptiveProportionTest(bad[:len(bad)-1]) = %v, want nil", err)
   215  	}
   216  	if err := entropy.AdaptiveProportionTest(bad); err == nil {
   217  		t.Error("AdaptiveProportionTest(bad) = nil, want error")
   218  	}
   219  }
   220  
   221  func TestEntropyUnchanged(t *testing.T) {
   222  	testenv.MustHaveSource(t)
   223  
   224  	h := sha256.New()
   225  	root := os.DirFS("../entropy/v1.0.0")
   226  	if err := fs.WalkDir(root, ".", func(path string, d fs.DirEntry, err error) error {
   227  		if err != nil {
   228  			return err
   229  		}
   230  		if d.IsDir() {
   231  			return nil
   232  		}
   233  		data, err := fs.ReadFile(root, path)
   234  		if err != nil {
   235  			return err
   236  		}
   237  		t.Logf("Hashing %s (%d bytes)", path, len(data))
   238  		fmt.Fprintf(h, "%s %d\n", path, len(data))
   239  		h.Write(data)
   240  		return nil
   241  	}); err != nil {
   242  		t.Fatalf("WalkDir: %v", err)
   243  	}
   244  
   245  	// The crypto/internal/entropy/v1.0.0 package is certified as a FIPS 140-3
   246  	// entropy source through the Entropy Source Validation program,
   247  	// independently of the FIPS 140-3 module. It must not change even across
   248  	// FIPS 140-3 module versions, in order to reuse the ESV certificate.
   249  	exp := "2541273241ae8aafe55026328354ed3799df1e2fb308b2097833203a42911b53"
   250  	if got := hex.EncodeToString(h.Sum(nil)); got != exp {
   251  		t.Errorf("hash of crypto/internal/entropy/v1.0.0 = %s, want %s", got, exp)
   252  	}
   253  }
   254  
   255  func TestEntropyRace(t *testing.T) {
   256  	// Check that concurrent calls to Seed don't trigger the race detector.
   257  	for range 16 {
   258  		go func() {
   259  			_, _ = entropy.Seed(&memory)
   260  		}()
   261  	}
   262  	// Same, with the higher-level DRBG.
   263  	for range 16 {
   264  		go func() {
   265  			var b [64]byte
   266  			drbg.Read(b[:])
   267  		}()
   268  	}
   269  }
   270  
   271  var sink byte
   272  
   273  func BenchmarkEntropySeed(b *testing.B) {
   274  	for b.Loop() {
   275  		seed, err := entropy.Seed(&memory)
   276  		if err != nil {
   277  			b.Fatalf("entropy.Seed() failed: %v", err)
   278  		}
   279  		sink ^= seed[0]
   280  	}
   281  }
   282  

View as plain text