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

View as plain text