--- /dev/null
+[
+ {"algorithm":"SHA2-224","messageLength":[{"increment":8,"max":65528,"min":0}],"revision":"1.0"},
+ {"algorithm":"SHA2-256","messageLength":[{"increment":8,"max":65528,"min":0}],"revision":"1.0"},
+ {"algorithm":"SHA2-384","messageLength":[{"increment":8,"max":65528,"min":0}],"revision":"1.0"},
+ {"algorithm":"SHA2-512","messageLength":[{"increment":8,"max":65528,"min":0}],"revision":"1.0"},
+ {"algorithm":"SHA2-512/224","messageLength":[{"increment":8,"max":65528,"min":0}],"revision":"1.0"},
+ {"algorithm":"SHA2-512/256","messageLength":[{"increment":8,"max":65528,"min":0}],"revision":"1.0"},
+
+ {"algorithm":"SHA3-224","messageLength":[{"increment":8,"max":65528,"min":0}],"revision":"2.0"},
+ {"algorithm":"SHA3-256","messageLength":[{"increment":8,"max":65528,"min":0}],"revision":"2.0"},
+ {"algorithm":"SHA3-384","messageLength":[{"increment":8,"max":65528,"min":0}],"revision":"2.0"},
+ {"algorithm":"SHA3-512","messageLength":[{"increment":8,"max":65528,"min":0}],"revision":"2.0"},
+
+ {"algorithm":"HMAC-SHA2-224","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":224,"min":32}],"revision":"1.0"},
+ {"algorithm":"HMAC-SHA2-256","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":256,"min":32}],"revision":"1.0"},
+ {"algorithm":"HMAC-SHA2-384","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":384,"min":32}],"revision":"1.0"},
+ {"algorithm":"HMAC-SHA2-512","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":512,"min":32}],"revision":"1.0"},
+ {"algorithm":"HMAC-SHA2-512/224","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":224,"min":32}],"revision":"1.0"},
+ {"algorithm":"HMAC-SHA2-512/256","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":256,"min":32}],"revision":"1.0"},
+
+ {"algorithm":"HMAC-SHA3-224","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":224,"min":32}],"revision":"1.0"},
+ {"algorithm":"HMAC-SHA3-256","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":256,"min":32}],"revision":"1.0"},
+ {"algorithm":"HMAC-SHA3-384","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":384,"min":32}],"revision":"1.0"},
+ {"algorithm":"HMAC-SHA3-512","keyLen":[{"increment":8,"max":524288,"min":8}],"macLen":[{"increment":8,"max":512,"min":32}],"revision":"1.0"}
+]
\ No newline at end of file
--- /dev/null
+// 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.
+
+// A module wrapper adapting the Go FIPS module to the protocol used by the
+// BoringSSL project's `acvptool`.
+//
+// The `acvptool` "lowers" the NIST ACVP server JSON test vectors into a simpler
+// stdin/stdout protocol that can be implemented by a module shim. The tool
+// will fork this binary, request the supported configuration, and then provide
+// test cases over stdin, expecting results to be returned on stdout.
+//
+// See "Testing other FIPS modules"[0] from the BoringSSL ACVP.md documentation
+// for a more detailed description of the protocol used between the acvptool
+// and module wrappers.
+//
+// [0]:https://boringssl.googlesource.com/boringssl/+/refs/heads/master/util/fipstools/acvp/ACVP.md#testing-other-fips-modules
+package fips_test
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/internal/fips"
+ "crypto/internal/fips/hmac"
+ "crypto/internal/fips/sha256"
+ "crypto/internal/fips/sha3"
+ "crypto/internal/fips/sha512"
+ _ "embed"
+ "encoding/binary"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "internal/testenv"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestMain(m *testing.M) {
+ if os.Getenv("ACVP_WRAPPER") == "1" {
+ wrapperMain()
+ } else {
+ os.Exit(m.Run())
+ }
+}
+
+func wrapperMain() {
+ if err := processingLoop(bufio.NewReader(os.Stdin), os.Stdout); err != nil {
+ fmt.Fprintf(os.Stderr, "processing error: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+type request struct {
+ name string
+ args [][]byte
+}
+
+type commandHandler func([][]byte) ([][]byte, error)
+
+type command struct {
+ // requiredArgs enforces that an exact number of arguments are provided to the handler.
+ requiredArgs int
+ handler commandHandler
+}
+
+var (
+ // SHA2 algorithm capabilities:
+ // https://pages.nist.gov/ACVP/draft-celi-acvp-sha.html#section-7.2
+ // HMAC algorithm capabilities:
+ // https://pages.nist.gov/ACVP/draft-fussell-acvp-mac.html#section-7
+ //go:embed acvp_capabilities.json
+ capabilitiesJson []byte
+
+ // commands should reflect what config says we support. E.g. adding a command here will be a NOP
+ // unless the configuration/acvp_capabilities.json indicates the command's associated algorithm
+ // is supported.
+ commands = map[string]command{
+ "getConfig": cmdGetConfig(),
+
+ "SHA2-224": cmdHashAft(sha256.New224()),
+ "SHA2-224/MCT": cmdHashMct(sha256.New224()),
+ "SHA2-256": cmdHashAft(sha256.New()),
+ "SHA2-256/MCT": cmdHashMct(sha256.New()),
+ "SHA2-384": cmdHashAft(sha512.New384()),
+ "SHA2-384/MCT": cmdHashMct(sha512.New384()),
+ "SHA2-512": cmdHashAft(sha512.New()),
+ "SHA2-512/MCT": cmdHashMct(sha512.New()),
+ "SHA2-512/224": cmdHashAft(sha512.New512_224()),
+ "SHA2-512/224/MCT": cmdHashMct(sha512.New512_224()),
+ "SHA2-512/256": cmdHashAft(sha512.New512_256()),
+ "SHA2-512/256/MCT": cmdHashMct(sha512.New512_256()),
+
+ "SHA3-256": cmdHashAft(sha3.New256()),
+ "SHA3-256/MCT": cmdSha3Mct(sha3.New256()),
+ "SHA3-224": cmdHashAft(sha3.New224()),
+ "SHA3-224/MCT": cmdSha3Mct(sha3.New224()),
+ "SHA3-384": cmdHashAft(sha3.New384()),
+ "SHA3-384/MCT": cmdSha3Mct(sha3.New384()),
+ "SHA3-512": cmdHashAft(sha3.New512()),
+ "SHA3-512/MCT": cmdSha3Mct(sha3.New512()),
+
+ "HMAC-SHA2-224": cmdHmacAft(func() fips.Hash { return sha256.New224() }),
+ "HMAC-SHA2-256": cmdHmacAft(func() fips.Hash { return sha256.New() }),
+ "HMAC-SHA2-384": cmdHmacAft(func() fips.Hash { return sha512.New384() }),
+ "HMAC-SHA2-512": cmdHmacAft(func() fips.Hash { return sha512.New() }),
+ "HMAC-SHA2-512/224": cmdHmacAft(func() fips.Hash { return sha512.New512_224() }),
+ "HMAC-SHA2-512/256": cmdHmacAft(func() fips.Hash { return sha512.New512_256() }),
+ "HMAC-SHA3-224": cmdHmacAft(func() fips.Hash { return sha3.New224() }),
+ "HMAC-SHA3-256": cmdHmacAft(func() fips.Hash { return sha3.New256() }),
+ "HMAC-SHA3-384": cmdHmacAft(func() fips.Hash { return sha3.New384() }),
+ "HMAC-SHA3-512": cmdHmacAft(func() fips.Hash { return sha3.New512() }),
+ }
+)
+
+func processingLoop(reader io.Reader, writer io.Writer) error {
+ // Per ACVP.md:
+ // The protocol is request–response: the subprocess only speaks in response to a request
+ // and there is exactly one response for every request.
+ for {
+ req, err := readRequest(reader)
+ if errors.Is(err, io.EOF) {
+ break
+ } else if err != nil {
+ return fmt.Errorf("reading request: %w", err)
+ }
+
+ cmd, exists := commands[req.name]
+ if !exists {
+ return fmt.Errorf("unknown command: %q", req.name)
+ }
+
+ if gotArgs := len(req.args); gotArgs != cmd.requiredArgs {
+ return fmt.Errorf("command %q expected %d args, got %d", req.name, cmd.requiredArgs, gotArgs)
+ }
+
+ response, err := cmd.handler(req.args)
+ if err != nil {
+ return fmt.Errorf("command %q failed: %w", req.name, err)
+ }
+
+ if err = writeResponse(writer, response); err != nil {
+ return fmt.Errorf("command %q response failed: %w", req.name, err)
+ }
+ }
+
+ return nil
+}
+
+func readRequest(reader io.Reader) (*request, error) {
+ // Per ACVP.md:
+ // Requests consist of one or more byte strings and responses consist
+ // of zero or more byte strings. A request contains: the number of byte
+ // strings, the length of each byte string, and the contents of each byte
+ // string. All numbers are 32-bit little-endian and values are
+ // concatenated in the order specified.
+ var numArgs uint32
+ if err := binary.Read(reader, binary.LittleEndian, &numArgs); err != nil {
+ return nil, err
+ }
+ if numArgs == 0 {
+ return nil, errors.New("invalid request: zero args")
+ }
+
+ args, err := readArgs(reader, numArgs)
+ if err != nil {
+ return nil, err
+ }
+
+ return &request{
+ name: string(args[0]),
+ args: args[1:],
+ }, nil
+}
+
+func readArgs(reader io.Reader, requiredArgs uint32) ([][]byte, error) {
+ argLengths := make([]uint32, requiredArgs)
+ args := make([][]byte, requiredArgs)
+
+ for i := range argLengths {
+ if err := binary.Read(reader, binary.LittleEndian, &argLengths[i]); err != nil {
+ return nil, fmt.Errorf("invalid request: failed to read %d-th arg len: %w", i, err)
+ }
+ }
+
+ for i, length := range argLengths {
+ buf := make([]byte, length)
+ if _, err := io.ReadFull(reader, buf); err != nil {
+ return nil, fmt.Errorf("invalid request: failed to read %d-th arg data: %w", i, err)
+ }
+ args[i] = buf
+ }
+
+ return args, nil
+}
+
+func writeResponse(writer io.Writer, args [][]byte) error {
+ // See `readRequest` for details on the base format. Per ACVP.md:
+ // A response has the same format except that there may be zero byte strings
+ // and the first byte string has no special meaning.
+ numArgs := uint32(len(args))
+ if err := binary.Write(writer, binary.LittleEndian, numArgs); err != nil {
+ return fmt.Errorf("writing arg count: %w", err)
+ }
+
+ for i, arg := range args {
+ if err := binary.Write(writer, binary.LittleEndian, uint32(len(arg))); err != nil {
+ return fmt.Errorf("writing %d-th arg length: %w", i, err)
+ }
+ }
+
+ for i, b := range args {
+ if _, err := writer.Write(b); err != nil {
+ return fmt.Errorf("writing %d-th arg data: %w", i, err)
+ }
+ }
+
+ return nil
+}
+
+// "All implementations must support the getConfig command
+// which takes no arguments and returns a single byte string
+// which is a JSON blob of ACVP algorithm configuration."
+func cmdGetConfig() command {
+ return command{
+ handler: func(args [][]byte) ([][]byte, error) {
+ return [][]byte{capabilitiesJson}, nil
+ },
+ }
+}
+
+// cmdHashAft returns a command handler for the specified hash
+// algorithm for algorithm functional test (AFT) test cases.
+//
+// This shape of command expects a message as the sole argument,
+// and writes the resulting digest as a response.
+//
+// See https://pages.nist.gov/ACVP/draft-celi-acvp-sha.html
+func cmdHashAft(h fips.Hash) command {
+ return command{
+ requiredArgs: 1, // Message to hash.
+ handler: func(args [][]byte) ([][]byte, error) {
+ h.Reset()
+ h.Write(args[0])
+ digest := make([]byte, 0, h.Size())
+ digest = h.Sum(digest)
+
+ return [][]byte{digest}, nil
+ },
+ }
+}
+
+// cmdHashMct returns a command handler for the specified hash
+// algorithm for monte carlo test (MCT) test cases.
+//
+// This shape of command expects a seed as the sole argument,
+// and writes the resulting digest as a response. It implements
+// the "standard" flavour of the MCT, not the "alternative".
+//
+// This algorithm was ported from `HashMCT` in BSSL's `modulewrapper.cc`
+// Note that it differs slightly from the upstream NIST MCT[0] algorithm
+// in that it does not perform the outer 100 iterations itself. See
+// footnote #1 in the ACVP.md docs[1], the acvptool handles this.
+//
+// [0]: https://pages.nist.gov/ACVP/draft-celi-acvp-sha.html#section-6.2
+// [1]: https://boringssl.googlesource.com/boringssl/+/refs/heads/master/util/fipstools/acvp/ACVP.md#testing-other-fips-modules
+func cmdHashMct(h fips.Hash) command {
+ return command{
+ requiredArgs: 1, // Seed message.
+ handler: func(args [][]byte) ([][]byte, error) {
+ hSize := h.Size()
+ seed := args[0]
+
+ if seedLen := len(seed); seedLen != hSize {
+ return nil, fmt.Errorf("invalid seed size: expected %d got %d", hSize, seedLen)
+ }
+
+ digest := make([]byte, 0, hSize)
+ buf := make([]byte, 0, 3*hSize)
+ buf = append(buf, seed...)
+ buf = append(buf, seed...)
+ buf = append(buf, seed...)
+
+ for i := 0; i < 1000; i++ {
+ h.Reset()
+ h.Write(buf)
+ digest = h.Sum(digest[:0])
+
+ copy(buf, buf[hSize:])
+ copy(buf[2*hSize:], digest)
+ }
+
+ return [][]byte{buf[hSize*2:]}, nil
+ },
+ }
+}
+
+// cmdSha3Mct returns a command handler for the specified hash
+// algorithm for SHA-3 monte carlo test (MCT) test cases.
+//
+// This shape of command expects a seed as the sole argument,
+// and writes the resulting digest as a response. It implements
+// the "standard" flavour of the MCT, not the "alternative".
+//
+// This algorithm was ported from the "standard" MCT algorithm
+// specified in draft-celi-acvp-sha3[0]. Note this differs from
+// the SHA2-* family of MCT tests handled by cmdHashMct. However,
+// like that handler it does not perform the outer 100 iterations.
+//
+// [0]: https://pages.nist.gov/ACVP/draft-celi-acvp-sha3.html#section-6.2.1
+func cmdSha3Mct(h fips.Hash) command {
+ return command{
+ requiredArgs: 1, // Seed message.
+ handler: func(args [][]byte) ([][]byte, error) {
+ seed := args[0]
+ md := make([][]byte, 1001)
+ md[0] = seed
+
+ for i := 1; i <= 1000; i++ {
+ h.Reset()
+ h.Write(md[i-1])
+ md[i] = h.Sum(nil)
+ }
+
+ return [][]byte{md[1000]}, nil
+ },
+ }
+}
+
+func cmdHmacAft(h func() fips.Hash) command {
+ return command{
+ requiredArgs: 2, // Message and key
+ handler: func(args [][]byte) ([][]byte, error) {
+ msg := args[0]
+ key := args[1]
+ mac := hmac.New(h, key)
+ mac.Write(msg)
+ return [][]byte{mac.Sum(nil)}, nil
+ },
+ }
+}
+
+func TestACVP(t *testing.T) {
+ testenv.SkipIfShortAndSlow(t)
+ testenv.MustHaveExternalNetwork(t)
+ testenv.MustHaveGoRun(t)
+ testenv.MustHaveExec(t)
+
+ const (
+ bsslModule = "boringssl.googlesource.com/boringssl.git"
+ bsslVersion = "v0.0.0-20241009223352-905c3903fd42"
+ goAcvpModule = "github.com/cpu/go-acvp"
+ goAcvpVersion = "v0.0.0-20241009200939-159f4c69a90d"
+ )
+
+ // In crypto/tls/bogo_shim_test.go the test is skipped if run on a builder with runtime.GOOS == "windows"
+ // due to flaky networking. It may be necessary to do the same here.
+
+ // Stat the acvp test config file so the test will be re-run if it changes, invalidating cached results
+ // from the old config.
+ if _, err := os.Stat("acvp_test.config.json"); err != nil {
+ t.Fatalf("failed to stat config file: %s", err)
+ }
+
+ // Create a temporary mod cache dir for the test module/tooling.
+ d := t.TempDir()
+ modcache := filepath.Join(d, "modcache")
+ if err := os.Mkdir(modcache, 0777); err != nil {
+ t.Fatal(err)
+ }
+ fmt.Printf("caching dependent modules in %q\n", modcache)
+ t.Setenv("GOMODCACHE", modcache)
+
+ // Fetch the BSSL module and use the JSON output to find the absolute path to the dir.
+ bsslDir := fetchModule(t, bsslModule, bsslVersion)
+
+ fmt.Println("building acvptool")
+
+ // Build the acvptool binary.
+ goTool := testenv.GoToolPath(t)
+ cmd := exec.Command(goTool,
+ "build",
+ "./util/fipstools/acvp/acvptool")
+ cmd.Dir = bsslDir
+ out := &strings.Builder{}
+ cmd.Stderr = out
+ if err := cmd.Run(); err != nil {
+ t.Fatalf("failed to build acvptool: %s\n%s", err, out.String())
+ }
+
+ // Similarly, fetch the ACVP data module that has vectors/expected answers.
+ dataDir := fetchModule(t, goAcvpModule, goAcvpVersion)
+
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("failed to fetch cwd: %s", err)
+ }
+ configPath := filepath.Join(cwd, "acvp_test.config.json")
+ toolPath := filepath.Join(bsslDir, "acvptool")
+ fmt.Printf("running check_expected.go\ncwd: %q\ndata_dir: %q\nconfig: %q\ntool: %q\nmodule-wrapper: %q\n",
+ cwd, dataDir, configPath, toolPath, os.Args[0])
+
+ // Run the check_expected test driver using the acvptool we built, and this test binary as the
+ // module wrapper. The file paths in the config file are specified relative to the dataDir root
+ // so we run the command from that dir.
+ args := []string{
+ "run",
+ filepath.Join(bsslDir, "util/fipstools/acvp/acvptool/test/check_expected.go"),
+ "-tool",
+ toolPath,
+ // Note: module prefix must match Wrapper value in acvp_test.config.json.
+ "-module-wrappers", "go:" + os.Args[0],
+ "-tests", configPath,
+ }
+ cmd = exec.Command(goTool, args...)
+ cmd.Dir = dataDir
+ cmd.Env = []string{"ACVP_WRAPPER=1", "GOCACHE=" + modcache}
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("failed to run acvp tests: %s\n%s", err, string(output))
+ }
+ fmt.Println(string(output))
+}
+
+func fetchModule(t *testing.T, module, version string) string {
+ goTool := testenv.GoToolPath(t)
+ fmt.Printf("fetching %s@%s\n", module, version)
+
+ output, err := exec.Command(goTool, "mod", "download", "-json", "-modcacherw", module+"@"+version).CombinedOutput()
+ if err != nil {
+ t.Fatalf("failed to download %s@%s: %s\n%s\n", module, version, err, output)
+ }
+ var j struct {
+ Dir string
+ }
+ if err := json.Unmarshal(output, &j); err != nil {
+ t.Fatalf("failed to parse 'go mod download': %s\n%s\n", err, output)
+ }
+
+ return j.Dir
+}
+
+func TestTooFewArgs(t *testing.T) {
+ commands["test"] = command{
+ requiredArgs: 1,
+ handler: func(args [][]byte) ([][]byte, error) {
+ if gotArgs := len(args); gotArgs != 1 {
+ return nil, fmt.Errorf("expected 1 args, got %d", gotArgs)
+ }
+ return nil, nil
+ },
+ }
+
+ var output bytes.Buffer
+ err := processingLoop(mockRequest(t, "test", nil), &output)
+ if err == nil {
+ t.Fatalf("expected error, got nil")
+ }
+ expectedErr := "expected 1 args, got 0"
+ if !strings.Contains(err.Error(), expectedErr) {
+ t.Errorf("expected error to contain %q, got %v", expectedErr, err)
+ }
+}
+
+func TestTooManyArgs(t *testing.T) {
+ commands["test"] = command{
+ requiredArgs: 1,
+ handler: func(args [][]byte) ([][]byte, error) {
+ if gotArgs := len(args); gotArgs != 1 {
+ return nil, fmt.Errorf("expected 1 args, got %d", gotArgs)
+ }
+ return nil, nil
+ },
+ }
+
+ var output bytes.Buffer
+ err := processingLoop(mockRequest(
+ t, "test", [][]byte{[]byte("one"), []byte("two")}), &output)
+ if err == nil {
+ t.Fatalf("expected error, got nil")
+ }
+ expectedErr := "expected 1 args, got 2"
+ if !strings.Contains(err.Error(), expectedErr) {
+ t.Errorf("expected error to contain %q, got %v", expectedErr, err)
+ }
+}
+
+func TestGetConfig(t *testing.T) {
+ var output bytes.Buffer
+ err := processingLoop(mockRequest(t, "getConfig", nil), &output)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+
+ respArgs := readResponse(t, &output)
+ if len(respArgs) != 1 {
+ t.Fatalf("expected 1 response arg, got %d", len(respArgs))
+ }
+
+ if !bytes.Equal(respArgs[0], capabilitiesJson) {
+ t.Errorf("expected config %q, got %q", string(capabilitiesJson), string(respArgs[0]))
+ }
+}
+
+func TestSha2256(t *testing.T) {
+ testMessage := []byte("gophers eat grass")
+ expectedDigest := []byte{
+ 188, 142, 10, 214, 48, 236, 72, 143, 70, 216, 223, 205, 219, 69, 53, 29,
+ 205, 207, 162, 6, 14, 70, 113, 60, 251, 170, 201, 236, 119, 39, 141, 172,
+ }
+
+ var output bytes.Buffer
+ err := processingLoop(mockRequest(t, "SHA2-256", [][]byte{testMessage}), &output)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+
+ respArgs := readResponse(t, &output)
+ if len(respArgs) != 1 {
+ t.Fatalf("expected 1 response arg, got %d", len(respArgs))
+ }
+
+ if !bytes.Equal(respArgs[0], expectedDigest) {
+ t.Errorf("expected digest %v, got %v", expectedDigest, respArgs[0])
+ }
+}
+
+func mockRequest(t *testing.T, cmd string, args [][]byte) io.Reader {
+ t.Helper()
+
+ msgData := append([][]byte{[]byte(cmd)}, args...)
+
+ var buf bytes.Buffer
+ if err := writeResponse(&buf, msgData); err != nil {
+ t.Fatalf("writeResponse error: %v", err)
+ }
+
+ return &buf
+}
+
+func readResponse(t *testing.T, reader io.Reader) [][]byte {
+ var numArgs uint32
+ if err := binary.Read(reader, binary.LittleEndian, &numArgs); err != nil {
+ t.Fatalf("failed to read response args count: %v", err)
+ }
+
+ args, err := readArgs(reader, numArgs)
+ if err != nil {
+ t.Fatalf("failed to read %d response args: %v", numArgs, err)
+ }
+
+ return args
+}