Source file src/crypto/internal/fips/acvp_test.go

     1  // Copyright 2024 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  // A module wrapper adapting the Go FIPS module to the protocol used by the
     6  // BoringSSL project's `acvptool`.
     7  //
     8  // The `acvptool` "lowers" the NIST ACVP server JSON test vectors into a simpler
     9  // stdin/stdout protocol that can be implemented by a module shim. The tool
    10  // will fork this binary, request the supported configuration, and then provide
    11  // test cases over stdin, expecting results to be returned on stdout.
    12  //
    13  // See "Testing other FIPS modules"[0] from the BoringSSL ACVP.md documentation
    14  // for a more detailed description of the protocol used between the acvptool
    15  // and module wrappers.
    16  //
    17  // [0]:https://boringssl.googlesource.com/boringssl/+/refs/heads/master/util/fipstools/acvp/ACVP.md#testing-other-fips-modules
    18  package fips_test
    19  
    20  import (
    21  	"bufio"
    22  	"bytes"
    23  	"crypto/internal/fips"
    24  	"crypto/internal/fips/hmac"
    25  	"crypto/internal/fips/sha256"
    26  	"crypto/internal/fips/sha3"
    27  	"crypto/internal/fips/sha512"
    28  	_ "embed"
    29  	"encoding/binary"
    30  	"encoding/json"
    31  	"errors"
    32  	"fmt"
    33  	"internal/testenv"
    34  	"io"
    35  	"os"
    36  	"os/exec"
    37  	"path/filepath"
    38  	"strings"
    39  	"testing"
    40  )
    41  
    42  func TestMain(m *testing.M) {
    43  	if os.Getenv("ACVP_WRAPPER") == "1" {
    44  		wrapperMain()
    45  	} else {
    46  		os.Exit(m.Run())
    47  	}
    48  }
    49  
    50  func wrapperMain() {
    51  	if err := processingLoop(bufio.NewReader(os.Stdin), os.Stdout); err != nil {
    52  		fmt.Fprintf(os.Stderr, "processing error: %v\n", err)
    53  		os.Exit(1)
    54  	}
    55  }
    56  
    57  type request struct {
    58  	name string
    59  	args [][]byte
    60  }
    61  
    62  type commandHandler func([][]byte) ([][]byte, error)
    63  
    64  type command struct {
    65  	// requiredArgs enforces that an exact number of arguments are provided to the handler.
    66  	requiredArgs int
    67  	handler      commandHandler
    68  }
    69  
    70  var (
    71  	// SHA2 algorithm capabilities:
    72  	//   https://pages.nist.gov/ACVP/draft-celi-acvp-sha.html#section-7.2
    73  	// HMAC algorithm capabilities:
    74  	//   https://pages.nist.gov/ACVP/draft-fussell-acvp-mac.html#section-7
    75  	//go:embed acvp_capabilities.json
    76  	capabilitiesJson []byte
    77  
    78  	// commands should reflect what config says we support. E.g. adding a command here will be a NOP
    79  	// unless the configuration/acvp_capabilities.json indicates the command's associated algorithm
    80  	// is supported.
    81  	commands = map[string]command{
    82  		"getConfig": cmdGetConfig(),
    83  
    84  		"SHA2-224":         cmdHashAft(sha256.New224()),
    85  		"SHA2-224/MCT":     cmdHashMct(sha256.New224()),
    86  		"SHA2-256":         cmdHashAft(sha256.New()),
    87  		"SHA2-256/MCT":     cmdHashMct(sha256.New()),
    88  		"SHA2-384":         cmdHashAft(sha512.New384()),
    89  		"SHA2-384/MCT":     cmdHashMct(sha512.New384()),
    90  		"SHA2-512":         cmdHashAft(sha512.New()),
    91  		"SHA2-512/MCT":     cmdHashMct(sha512.New()),
    92  		"SHA2-512/224":     cmdHashAft(sha512.New512_224()),
    93  		"SHA2-512/224/MCT": cmdHashMct(sha512.New512_224()),
    94  		"SHA2-512/256":     cmdHashAft(sha512.New512_256()),
    95  		"SHA2-512/256/MCT": cmdHashMct(sha512.New512_256()),
    96  
    97  		"SHA3-256":     cmdHashAft(sha3.New256()),
    98  		"SHA3-256/MCT": cmdSha3Mct(sha3.New256()),
    99  		"SHA3-224":     cmdHashAft(sha3.New224()),
   100  		"SHA3-224/MCT": cmdSha3Mct(sha3.New224()),
   101  		"SHA3-384":     cmdHashAft(sha3.New384()),
   102  		"SHA3-384/MCT": cmdSha3Mct(sha3.New384()),
   103  		"SHA3-512":     cmdHashAft(sha3.New512()),
   104  		"SHA3-512/MCT": cmdSha3Mct(sha3.New512()),
   105  
   106  		"HMAC-SHA2-224":     cmdHmacAft(func() fips.Hash { return sha256.New224() }),
   107  		"HMAC-SHA2-256":     cmdHmacAft(func() fips.Hash { return sha256.New() }),
   108  		"HMAC-SHA2-384":     cmdHmacAft(func() fips.Hash { return sha512.New384() }),
   109  		"HMAC-SHA2-512":     cmdHmacAft(func() fips.Hash { return sha512.New() }),
   110  		"HMAC-SHA2-512/224": cmdHmacAft(func() fips.Hash { return sha512.New512_224() }),
   111  		"HMAC-SHA2-512/256": cmdHmacAft(func() fips.Hash { return sha512.New512_256() }),
   112  		"HMAC-SHA3-224":     cmdHmacAft(func() fips.Hash { return sha3.New224() }),
   113  		"HMAC-SHA3-256":     cmdHmacAft(func() fips.Hash { return sha3.New256() }),
   114  		"HMAC-SHA3-384":     cmdHmacAft(func() fips.Hash { return sha3.New384() }),
   115  		"HMAC-SHA3-512":     cmdHmacAft(func() fips.Hash { return sha3.New512() }),
   116  	}
   117  )
   118  
   119  func processingLoop(reader io.Reader, writer io.Writer) error {
   120  	// Per ACVP.md:
   121  	//   The protocol is request–response: the subprocess only speaks in response to a request
   122  	//   and there is exactly one response for every request.
   123  	for {
   124  		req, err := readRequest(reader)
   125  		if errors.Is(err, io.EOF) {
   126  			break
   127  		} else if err != nil {
   128  			return fmt.Errorf("reading request: %w", err)
   129  		}
   130  
   131  		cmd, exists := commands[req.name]
   132  		if !exists {
   133  			return fmt.Errorf("unknown command: %q", req.name)
   134  		}
   135  
   136  		if gotArgs := len(req.args); gotArgs != cmd.requiredArgs {
   137  			return fmt.Errorf("command %q expected %d args, got %d", req.name, cmd.requiredArgs, gotArgs)
   138  		}
   139  
   140  		response, err := cmd.handler(req.args)
   141  		if err != nil {
   142  			return fmt.Errorf("command %q failed: %w", req.name, err)
   143  		}
   144  
   145  		if err = writeResponse(writer, response); err != nil {
   146  			return fmt.Errorf("command %q response failed: %w", req.name, err)
   147  		}
   148  	}
   149  
   150  	return nil
   151  }
   152  
   153  func readRequest(reader io.Reader) (*request, error) {
   154  	// Per ACVP.md:
   155  	//   Requests consist of one or more byte strings and responses consist
   156  	//   of zero or more byte strings. A request contains: the number of byte
   157  	//   strings, the length of each byte string, and the contents of each byte
   158  	//   string. All numbers are 32-bit little-endian and values are
   159  	//   concatenated in the order specified.
   160  	var numArgs uint32
   161  	if err := binary.Read(reader, binary.LittleEndian, &numArgs); err != nil {
   162  		return nil, err
   163  	}
   164  	if numArgs == 0 {
   165  		return nil, errors.New("invalid request: zero args")
   166  	}
   167  
   168  	args, err := readArgs(reader, numArgs)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	return &request{
   174  		name: string(args[0]),
   175  		args: args[1:],
   176  	}, nil
   177  }
   178  
   179  func readArgs(reader io.Reader, requiredArgs uint32) ([][]byte, error) {
   180  	argLengths := make([]uint32, requiredArgs)
   181  	args := make([][]byte, requiredArgs)
   182  
   183  	for i := range argLengths {
   184  		if err := binary.Read(reader, binary.LittleEndian, &argLengths[i]); err != nil {
   185  			return nil, fmt.Errorf("invalid request: failed to read %d-th arg len: %w", i, err)
   186  		}
   187  	}
   188  
   189  	for i, length := range argLengths {
   190  		buf := make([]byte, length)
   191  		if _, err := io.ReadFull(reader, buf); err != nil {
   192  			return nil, fmt.Errorf("invalid request: failed to read %d-th arg data: %w", i, err)
   193  		}
   194  		args[i] = buf
   195  	}
   196  
   197  	return args, nil
   198  }
   199  
   200  func writeResponse(writer io.Writer, args [][]byte) error {
   201  	// See `readRequest` for details on the base format. Per ACVP.md:
   202  	//   A response has the same format except that there may be zero byte strings
   203  	//   and the first byte string has no special meaning.
   204  	numArgs := uint32(len(args))
   205  	if err := binary.Write(writer, binary.LittleEndian, numArgs); err != nil {
   206  		return fmt.Errorf("writing arg count: %w", err)
   207  	}
   208  
   209  	for i, arg := range args {
   210  		if err := binary.Write(writer, binary.LittleEndian, uint32(len(arg))); err != nil {
   211  			return fmt.Errorf("writing %d-th arg length: %w", i, err)
   212  		}
   213  	}
   214  
   215  	for i, b := range args {
   216  		if _, err := writer.Write(b); err != nil {
   217  			return fmt.Errorf("writing %d-th arg data: %w", i, err)
   218  		}
   219  	}
   220  
   221  	return nil
   222  }
   223  
   224  // "All implementations must support the getConfig command
   225  // which takes no arguments and returns a single byte string
   226  // which is a JSON blob of ACVP algorithm configuration."
   227  func cmdGetConfig() command {
   228  	return command{
   229  		handler: func(args [][]byte) ([][]byte, error) {
   230  			return [][]byte{capabilitiesJson}, nil
   231  		},
   232  	}
   233  }
   234  
   235  // cmdHashAft returns a command handler for the specified hash
   236  // algorithm for algorithm functional test (AFT) test cases.
   237  //
   238  // This shape of command expects a message as the sole argument,
   239  // and writes the resulting digest as a response.
   240  //
   241  // See https://pages.nist.gov/ACVP/draft-celi-acvp-sha.html
   242  func cmdHashAft(h fips.Hash) command {
   243  	return command{
   244  		requiredArgs: 1, // Message to hash.
   245  		handler: func(args [][]byte) ([][]byte, error) {
   246  			h.Reset()
   247  			h.Write(args[0])
   248  			digest := make([]byte, 0, h.Size())
   249  			digest = h.Sum(digest)
   250  
   251  			return [][]byte{digest}, nil
   252  		},
   253  	}
   254  }
   255  
   256  // cmdHashMct returns a command handler for the specified hash
   257  // algorithm for monte carlo test (MCT) test cases.
   258  //
   259  // This shape of command expects a seed as the sole argument,
   260  // and writes the resulting digest as a response. It implements
   261  // the "standard" flavour of the MCT, not the "alternative".
   262  //
   263  // This algorithm was ported from `HashMCT` in BSSL's `modulewrapper.cc`
   264  // Note that it differs slightly from the upstream NIST MCT[0] algorithm
   265  // in that it does not perform the outer 100 iterations itself. See
   266  // footnote #1 in the ACVP.md docs[1], the acvptool handles this.
   267  //
   268  // [0]: https://pages.nist.gov/ACVP/draft-celi-acvp-sha.html#section-6.2
   269  // [1]: https://boringssl.googlesource.com/boringssl/+/refs/heads/master/util/fipstools/acvp/ACVP.md#testing-other-fips-modules
   270  func cmdHashMct(h fips.Hash) command {
   271  	return command{
   272  		requiredArgs: 1, // Seed message.
   273  		handler: func(args [][]byte) ([][]byte, error) {
   274  			hSize := h.Size()
   275  			seed := args[0]
   276  
   277  			if seedLen := len(seed); seedLen != hSize {
   278  				return nil, fmt.Errorf("invalid seed size: expected %d got %d", hSize, seedLen)
   279  			}
   280  
   281  			digest := make([]byte, 0, hSize)
   282  			buf := make([]byte, 0, 3*hSize)
   283  			buf = append(buf, seed...)
   284  			buf = append(buf, seed...)
   285  			buf = append(buf, seed...)
   286  
   287  			for i := 0; i < 1000; i++ {
   288  				h.Reset()
   289  				h.Write(buf)
   290  				digest = h.Sum(digest[:0])
   291  
   292  				copy(buf, buf[hSize:])
   293  				copy(buf[2*hSize:], digest)
   294  			}
   295  
   296  			return [][]byte{buf[hSize*2:]}, nil
   297  		},
   298  	}
   299  }
   300  
   301  // cmdSha3Mct returns a command handler for the specified hash
   302  // algorithm for SHA-3 monte carlo test (MCT) test cases.
   303  //
   304  // This shape of command expects a seed as the sole argument,
   305  // and writes the resulting digest as a response. It implements
   306  // the "standard" flavour of the MCT, not the "alternative".
   307  //
   308  // This algorithm was ported from the "standard" MCT algorithm
   309  // specified in  draft-celi-acvp-sha3[0]. Note this differs from
   310  // the SHA2-* family of MCT tests handled by cmdHashMct. However,
   311  // like that handler it does not perform the outer 100 iterations.
   312  //
   313  // [0]: https://pages.nist.gov/ACVP/draft-celi-acvp-sha3.html#section-6.2.1
   314  func cmdSha3Mct(h fips.Hash) command {
   315  	return command{
   316  		requiredArgs: 1, // Seed message.
   317  		handler: func(args [][]byte) ([][]byte, error) {
   318  			seed := args[0]
   319  			md := make([][]byte, 1001)
   320  			md[0] = seed
   321  
   322  			for i := 1; i <= 1000; i++ {
   323  				h.Reset()
   324  				h.Write(md[i-1])
   325  				md[i] = h.Sum(nil)
   326  			}
   327  
   328  			return [][]byte{md[1000]}, nil
   329  		},
   330  	}
   331  }
   332  
   333  func cmdHmacAft(h func() fips.Hash) command {
   334  	return command{
   335  		requiredArgs: 2, // Message and key
   336  		handler: func(args [][]byte) ([][]byte, error) {
   337  			msg := args[0]
   338  			key := args[1]
   339  			mac := hmac.New(h, key)
   340  			mac.Write(msg)
   341  			return [][]byte{mac.Sum(nil)}, nil
   342  		},
   343  	}
   344  }
   345  
   346  func TestACVP(t *testing.T) {
   347  	testenv.SkipIfShortAndSlow(t)
   348  	testenv.MustHaveExternalNetwork(t)
   349  	testenv.MustHaveGoRun(t)
   350  	testenv.MustHaveExec(t)
   351  
   352  	const (
   353  		bsslModule    = "boringssl.googlesource.com/boringssl.git"
   354  		bsslVersion   = "v0.0.0-20241009223352-905c3903fd42"
   355  		goAcvpModule  = "github.com/cpu/go-acvp"
   356  		goAcvpVersion = "v0.0.0-20241009200939-159f4c69a90d"
   357  	)
   358  
   359  	// In crypto/tls/bogo_shim_test.go the test is skipped if run on a builder with runtime.GOOS == "windows"
   360  	// due to flaky networking. It may be necessary to do the same here.
   361  
   362  	// Stat the acvp test config file so the test will be re-run if it changes, invalidating cached results
   363  	// from the old config.
   364  	if _, err := os.Stat("acvp_test.config.json"); err != nil {
   365  		t.Fatalf("failed to stat config file: %s", err)
   366  	}
   367  
   368  	// Create a temporary mod cache dir for the test module/tooling.
   369  	d := t.TempDir()
   370  	modcache := filepath.Join(d, "modcache")
   371  	if err := os.Mkdir(modcache, 0777); err != nil {
   372  		t.Fatal(err)
   373  	}
   374  	fmt.Printf("caching dependent modules in %q\n", modcache)
   375  	t.Setenv("GOMODCACHE", modcache)
   376  
   377  	// Fetch the BSSL module and use the JSON output to find the absolute path to the dir.
   378  	bsslDir := fetchModule(t, bsslModule, bsslVersion)
   379  
   380  	fmt.Println("building acvptool")
   381  
   382  	// Build the acvptool binary.
   383  	goTool := testenv.GoToolPath(t)
   384  	cmd := exec.Command(goTool,
   385  		"build",
   386  		"./util/fipstools/acvp/acvptool")
   387  	cmd.Dir = bsslDir
   388  	out := &strings.Builder{}
   389  	cmd.Stderr = out
   390  	if err := cmd.Run(); err != nil {
   391  		t.Fatalf("failed to build acvptool: %s\n%s", err, out.String())
   392  	}
   393  
   394  	// Similarly, fetch the ACVP data module that has vectors/expected answers.
   395  	dataDir := fetchModule(t, goAcvpModule, goAcvpVersion)
   396  
   397  	cwd, err := os.Getwd()
   398  	if err != nil {
   399  		t.Fatalf("failed to fetch cwd: %s", err)
   400  	}
   401  	configPath := filepath.Join(cwd, "acvp_test.config.json")
   402  	toolPath := filepath.Join(bsslDir, "acvptool")
   403  	fmt.Printf("running check_expected.go\ncwd: %q\ndata_dir: %q\nconfig: %q\ntool: %q\nmodule-wrapper: %q\n",
   404  		cwd, dataDir, configPath, toolPath, os.Args[0])
   405  
   406  	// Run the check_expected test driver using the acvptool we built, and this test binary as the
   407  	// module wrapper. The file paths in the config file are specified relative to the dataDir root
   408  	// so we run the command from that dir.
   409  	args := []string{
   410  		"run",
   411  		filepath.Join(bsslDir, "util/fipstools/acvp/acvptool/test/check_expected.go"),
   412  		"-tool",
   413  		toolPath,
   414  		// Note: module prefix must match Wrapper value in acvp_test.config.json.
   415  		"-module-wrappers", "go:" + os.Args[0],
   416  		"-tests", configPath,
   417  	}
   418  	cmd = exec.Command(goTool, args...)
   419  	cmd.Dir = dataDir
   420  	cmd.Env = []string{"ACVP_WRAPPER=1", "GOCACHE=" + modcache}
   421  	output, err := cmd.CombinedOutput()
   422  	if err != nil {
   423  		t.Fatalf("failed to run acvp tests: %s\n%s", err, string(output))
   424  	}
   425  	fmt.Println(string(output))
   426  }
   427  
   428  func fetchModule(t *testing.T, module, version string) string {
   429  	goTool := testenv.GoToolPath(t)
   430  	fmt.Printf("fetching %s@%s\n", module, version)
   431  
   432  	output, err := exec.Command(goTool, "mod", "download", "-json", "-modcacherw", module+"@"+version).CombinedOutput()
   433  	if err != nil {
   434  		t.Fatalf("failed to download %s@%s: %s\n%s\n", module, version, err, output)
   435  	}
   436  	var j struct {
   437  		Dir string
   438  	}
   439  	if err := json.Unmarshal(output, &j); err != nil {
   440  		t.Fatalf("failed to parse 'go mod download': %s\n%s\n", err, output)
   441  	}
   442  
   443  	return j.Dir
   444  }
   445  
   446  func TestTooFewArgs(t *testing.T) {
   447  	commands["test"] = command{
   448  		requiredArgs: 1,
   449  		handler: func(args [][]byte) ([][]byte, error) {
   450  			if gotArgs := len(args); gotArgs != 1 {
   451  				return nil, fmt.Errorf("expected 1 args, got %d", gotArgs)
   452  			}
   453  			return nil, nil
   454  		},
   455  	}
   456  
   457  	var output bytes.Buffer
   458  	err := processingLoop(mockRequest(t, "test", nil), &output)
   459  	if err == nil {
   460  		t.Fatalf("expected error, got nil")
   461  	}
   462  	expectedErr := "expected 1 args, got 0"
   463  	if !strings.Contains(err.Error(), expectedErr) {
   464  		t.Errorf("expected error to contain %q, got %v", expectedErr, err)
   465  	}
   466  }
   467  
   468  func TestTooManyArgs(t *testing.T) {
   469  	commands["test"] = command{
   470  		requiredArgs: 1,
   471  		handler: func(args [][]byte) ([][]byte, error) {
   472  			if gotArgs := len(args); gotArgs != 1 {
   473  				return nil, fmt.Errorf("expected 1 args, got %d", gotArgs)
   474  			}
   475  			return nil, nil
   476  		},
   477  	}
   478  
   479  	var output bytes.Buffer
   480  	err := processingLoop(mockRequest(
   481  		t, "test", [][]byte{[]byte("one"), []byte("two")}), &output)
   482  	if err == nil {
   483  		t.Fatalf("expected error, got nil")
   484  	}
   485  	expectedErr := "expected 1 args, got 2"
   486  	if !strings.Contains(err.Error(), expectedErr) {
   487  		t.Errorf("expected error to contain %q, got %v", expectedErr, err)
   488  	}
   489  }
   490  
   491  func TestGetConfig(t *testing.T) {
   492  	var output bytes.Buffer
   493  	err := processingLoop(mockRequest(t, "getConfig", nil), &output)
   494  	if err != nil {
   495  		t.Errorf("unexpected error: %v", err)
   496  	}
   497  
   498  	respArgs := readResponse(t, &output)
   499  	if len(respArgs) != 1 {
   500  		t.Fatalf("expected 1 response arg, got %d", len(respArgs))
   501  	}
   502  
   503  	if !bytes.Equal(respArgs[0], capabilitiesJson) {
   504  		t.Errorf("expected config %q, got %q", string(capabilitiesJson), string(respArgs[0]))
   505  	}
   506  }
   507  
   508  func TestSha2256(t *testing.T) {
   509  	testMessage := []byte("gophers eat grass")
   510  	expectedDigest := []byte{
   511  		188, 142, 10, 214, 48, 236, 72, 143, 70, 216, 223, 205, 219, 69, 53, 29,
   512  		205, 207, 162, 6, 14, 70, 113, 60, 251, 170, 201, 236, 119, 39, 141, 172,
   513  	}
   514  
   515  	var output bytes.Buffer
   516  	err := processingLoop(mockRequest(t, "SHA2-256", [][]byte{testMessage}), &output)
   517  	if err != nil {
   518  		t.Errorf("unexpected error: %v", err)
   519  	}
   520  
   521  	respArgs := readResponse(t, &output)
   522  	if len(respArgs) != 1 {
   523  		t.Fatalf("expected 1 response arg, got %d", len(respArgs))
   524  	}
   525  
   526  	if !bytes.Equal(respArgs[0], expectedDigest) {
   527  		t.Errorf("expected digest %v, got %v", expectedDigest, respArgs[0])
   528  	}
   529  }
   530  
   531  func mockRequest(t *testing.T, cmd string, args [][]byte) io.Reader {
   532  	t.Helper()
   533  
   534  	msgData := append([][]byte{[]byte(cmd)}, args...)
   535  
   536  	var buf bytes.Buffer
   537  	if err := writeResponse(&buf, msgData); err != nil {
   538  		t.Fatalf("writeResponse error: %v", err)
   539  	}
   540  
   541  	return &buf
   542  }
   543  
   544  func readResponse(t *testing.T, reader io.Reader) [][]byte {
   545  	var numArgs uint32
   546  	if err := binary.Read(reader, binary.LittleEndian, &numArgs); err != nil {
   547  		t.Fatalf("failed to read response args count: %v", err)
   548  	}
   549  
   550  	args, err := readArgs(reader, numArgs)
   551  	if err != nil {
   552  		t.Fatalf("failed to read %d response args: %v", numArgs, err)
   553  	}
   554  
   555  	return args
   556  }
   557  

View as plain text