utils/fipstools: add SHAKE-128/256 ACVP support

This commit extends the acvptool subprocess package to support the
SHAKE-128 and SHAKE-256 test vectors and expected responses defined by
draft-celi-acvp-sha3:

https://pages.nist.gov/ACVP/draft-celi-acvp-sha3.html

The AFT test type is a close match to the existing SHA2/SHA3 AFT test
type, but VOT (variable output test) and MCT both differ enough that it
feels most clear to treat each as separate commands with their own
subprocess primitive.

The ACVP.md documentation of available commands is also updated to
describe the pre-existing SHA3 commands.

Change-Id: Ib5e468331f0c37e8298ff8e6a2bfa9665d954e83
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/72467
Reviewed-by: Adam Langley <agl@google.com>
Reviewed-by: Bob Beck <bbe@google.com>
Commit-Queue: Bob Beck <bbe@google.com>
diff --git a/util/fipstools/acvp/ACVP.md b/util/fipstools/acvp/ACVP.md
index 1367dd7..b904951 100644
--- a/util/fipstools/acvp/ACVP.md
+++ b/util/fipstools/acvp/ACVP.md
@@ -106,6 +106,16 @@
 | SHA2-512             | Value to hash             | Digest  |
 | SHA2-512/224         | Value to hash             | Digest  |
 | SHA2-512/256         | Value to hash             | Digest  |
+| SHA3-224             | Value to hash             | Digest  |
+| SHA3-256             | Value to hash             | Digest  |
+| SHA3-384             | Value to hash             | Digest  |
+| SHA3-512             | Value to hash             | Digest  |
+| SHAKE-128            | Value to hash, output length bytes | Digest |
+| SHAKE-128/VOT        | Value to hash, output length bytes | Digest |
+| SHAKE-128/MCT        | Initial seed¹, min output bytes, max output bytes, output length bytes | Digest, output length bytes |
+| SHAKE-256            | Value to hash, output length bytes | Digest |
+| SHAKE-256/VOT        | Value to hash, output length bytes | Digest |
+| SHAKE-256/MCT        | Initial seed¹, min output bytes, max output bytes, output length bytes | Digest, output length bytes |
 | SHA-1/MCT            | Initial seed¹             | Digest  |
 | SHA2-224/MCT         | Initial seed¹             | Digest  |
 | SHA2-256/MCT         | Initial seed¹             | Digest  |
@@ -113,6 +123,10 @@
 | SHA2-512/MCT         | Initial seed¹             | Digest  |
 | SHA2-512/224/MCT     | Initial seed¹             | Digest  |
 | SHA2-512/256/MCT     | Initial seed¹             | Digest  |
+| SHA3-224/MCT         | Initial seed¹             | Digest  |
+| SHA3-256/MCT         | Initial seed¹             | Digest  |
+| SHA3-384/MCT         | Initial seed¹             | Digest  |
+| SHA3-512/MCT         | Initial seed¹             | Digest  |
 | TLSKDF/1.2/&lt;HASH&gt; | Number output bytes, secret, label, seed1, seed2 | Output |
 | PBKDF                | HMAC name, key length (bits), salt, password, iteration count | Derived key |
 
diff --git a/util/fipstools/acvp/acvptool/subprocess/shake.go b/util/fipstools/acvp/acvptool/subprocess/shake.go
new file mode 100644
index 0000000..513cabb
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/subprocess/shake.go
@@ -0,0 +1,169 @@
+// Copyright (c) 2024, Google Inc.
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+package subprocess
+
+import (
+	"encoding/binary"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+)
+
+// The following structures reflect the JSON of ACVP shake tests. See
+// https://pages.nist.gov/ACVP/draft-celi-acvp-sha3.html#name-test-vectors
+
+type shakeTestVectorSet struct {
+	Groups []shakeTestGroup `json:"testGroups"`
+}
+
+type shakeTestGroup struct {
+	ID            uint64 `json:"tgId"`
+	Type          string `json:"testType"`
+	MaxOutLenBits uint32 `json:"maxOutLen"`
+	MinOutLenBits uint32 `json:"minOutLen"`
+	Tests         []struct {
+		ID           uint64 `json:"tcId"`
+		BitLength    uint64 `json:"len"`
+		BitOutLength uint32 `json:"outLen"`
+		MsgHex       string `json:"msg"`
+	} `json:"tests"`
+}
+
+type shakeTestGroupResponse struct {
+	ID    uint64              `json:"tgId"`
+	Tests []shakeTestResponse `json:"tests"`
+}
+
+type shakeTestResponse struct {
+	ID         uint64           `json:"tcId"`
+	DigestHex  string           `json:"md,omitempty"`
+	MCTResults []shakeMCTResult `json:"resultsArray,omitempty"`
+}
+
+type shakeMCTResult struct {
+	DigestHex string `json:"md"`
+	OutputLen uint32 `json:"outLen,omitempty"`
+}
+
+// shake implements an ACVP algorithm by making requests to the
+// subprocess to hash strings.
+type shake struct {
+	// algo is the ACVP name for this algorithm and also the command name
+	// given to the subprocess to hash with this hash function.
+	algo string
+	// size is the number of bytes of digest that the hash produces for AFT tests.
+	size int
+}
+
+func (h *shake) Process(vectorSet []byte, m Transactable) (any, error) {
+	var parsed shakeTestVectorSet
+	if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+		return nil, err
+	}
+
+	var ret []shakeTestGroupResponse
+	// See
+	// https://pages.nist.gov/ACVP/draft-celi-acvp-sha3.html#name-test-types
+	// for details about the tests.
+	for _, group := range parsed.Groups {
+		group := group
+		response := shakeTestGroupResponse{
+			ID: group.ID,
+		}
+
+		for _, test := range group.Tests {
+			test := test
+
+			if uint64(len(test.MsgHex))*4 != test.BitLength {
+				return nil, fmt.Errorf("test case %d/%d contains hex message of length %d but specifies a bit length of %d", group.ID, test.ID, len(test.MsgHex), test.BitLength)
+			}
+			msg, err := hex.DecodeString(test.MsgHex)
+			if err != nil {
+				return nil, fmt.Errorf("failed to decode hex in test case %d/%d: %s", group.ID, test.ID, err)
+			}
+
+			if test.BitOutLength%8 != 0 {
+				return nil, fmt.Errorf("test case %d/%d has bit length %d - fractional bytes not supported", group.ID, test.ID, test.BitOutLength)
+			}
+
+			switch group.Type {
+			case "AFT":
+				// "AFTs all produce a single digest size, matching the security strength of the extendable output function."
+				if test.BitOutLength != uint32(h.size*8) {
+					return nil, fmt.Errorf("AFT test case %d/%d has bit length %d but expected %d", group.ID, test.ID, test.BitOutLength, h.size*8)
+				}
+
+				m.TransactAsync(h.algo, 1, [][]byte{msg, uint32le(test.BitOutLength / 8)}, func(result [][]byte) error {
+					response.Tests = append(response.Tests, shakeTestResponse{
+						ID:        test.ID,
+						DigestHex: hex.EncodeToString(result[0]),
+					})
+					return nil
+				})
+			case "VOT":
+				// "The VOTs SHALL produce varying digest sizes based on the capabilities of the IUT"
+				m.TransactAsync(h.algo+"/VOT", 1, [][]byte{msg, uint32le(test.BitOutLength / 8)}, func(result [][]byte) error {
+					response.Tests = append(response.Tests, shakeTestResponse{
+						ID:        test.ID,
+						DigestHex: hex.EncodeToString(result[0]),
+					})
+					return nil
+				})
+			case "MCT":
+				// https://pages.nist.gov/ACVP/draft-celi-acvp-sha3.html#name-shake-monte-carlo-test
+				testResponse := shakeTestResponse{ID: test.ID}
+
+				if group.MinOutLenBits%8 != 0 {
+					return nil, fmt.Errorf("MCT test group %d has min output length %d - fractional bytes not supported", group.ID, group.MinOutLenBits)
+				}
+				if group.MaxOutLenBits%8 != 0 {
+					return nil, fmt.Errorf("MCT test group %d has max output length %d - fractional bytes not supported", group.ID, group.MaxOutLenBits)
+				}
+
+				digest := msg
+				minOutLenBytes := uint32le(group.MinOutLenBits / 8)
+				maxOutLenBytes := uint32le(group.MaxOutLenBits / 8)
+				outputLenBytes := uint32le(group.MaxOutLenBits / 8)
+
+				for i := 0; i < 100; i++ {
+					args := [][]byte{digest, minOutLenBytes, maxOutLenBytes, outputLenBytes}
+					result, err := m.Transact(h.algo+"/MCT", 2, args...)
+					if err != nil {
+						panic(h.algo + " mct operation failed: " + err.Error())
+					}
+
+					digest = result[0]
+					outputLenBytes = uint32le(binary.LittleEndian.Uint32(result[1]))
+					mctResult := shakeMCTResult{DigestHex: hex.EncodeToString(digest), OutputLen: uint32(len(digest) * 8)}
+					testResponse.MCTResults = append(testResponse.MCTResults, mctResult)
+				}
+
+				response.Tests = append(response.Tests, testResponse)
+			default:
+				return nil, fmt.Errorf("test group %d has unknown type %q", group.ID, group.Type)
+			}
+		}
+
+		m.Barrier(func() {
+			ret = append(ret, response)
+		})
+	}
+
+	if err := m.Flush(); err != nil {
+		return nil, err
+	}
+
+	return ret, nil
+}
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
index f20707f..d28dba1 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -108,6 +108,8 @@
 		"SHA3-256":          &hashPrimitive{"SHA3-256", 32},
 		"SHA3-384":          &hashPrimitive{"SHA3-384", 48},
 		"SHA3-512":          &hashPrimitive{"SHA3-512", 64},
+		"SHAKE-128":         &shake{"SHAKE-128", 16},
+		"SHAKE-256":         &shake{"SHAKE-256", 32},
 		"ACVP-AES-ECB":      &blockCipher{"AES", 16, 2, true, false, iterateAES},
 		"ACVP-AES-CBC":      &blockCipher{"AES-CBC", 16, 2, true, true, iterateAESCBC},
 		"ACVP-AES-CBC-CS3":  &blockCipher{"AES-CBC-CS3", 16, 1, false, true, iterateAESCBC},
diff --git a/util/fipstools/acvp/acvptool/test/expected/SHAKE-128.bz2 b/util/fipstools/acvp/acvptool/test/expected/SHAKE-128.bz2
new file mode 100644
index 0000000..7b70df5
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/expected/SHAKE-128.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/expected/SHAKE-256.bz2 b/util/fipstools/acvp/acvptool/test/expected/SHAKE-256.bz2
new file mode 100644
index 0000000..bc82517
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/expected/SHAKE-256.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/tests.json b/util/fipstools/acvp/acvptool/test/tests.json
index d041981..d54f722 100644
--- a/util/fipstools/acvp/acvptool/test/tests.json
+++ b/util/fipstools/acvp/acvptool/test/tests.json
@@ -34,5 +34,7 @@
 {"Wrapper": "modulewrapper", "In": "vectors/TLS12.bz2", "Out": "expected/TLS12.bz2"},
 {"Wrapper": "modulewrapper", "In": "vectors/TLS13.bz2", "Out": "expected/TLS13.bz2"},
 {"Wrapper": "testmodulewrapper", "In": "vectors/PBKDF.bz2", "Out": "expected/PBKDF.bz2"},
-{"Wrapper": "testmodulewrapper", "In": "vectors/EDDSA.bz2", "Out": "expected/EDDSA.bz2"}
+{"Wrapper": "testmodulewrapper", "In": "vectors/EDDSA.bz2", "Out": "expected/EDDSA.bz2"},
+{"Wrapper": "testmodulewrapper", "In": "vectors/SHAKE-128.bz2", "Out": "expected/SHAKE-128.bz2"},
+{"Wrapper": "testmodulewrapper", "In": "vectors/SHAKE-256.bz2", "Out": "expected/SHAKE-256.bz2"}
 ]
diff --git a/util/fipstools/acvp/acvptool/test/vectors/SHAKE-128.bz2 b/util/fipstools/acvp/acvptool/test/vectors/SHAKE-128.bz2
new file mode 100644
index 0000000..9b21c31
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/vectors/SHAKE-128.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/vectors/SHAKE-256.bz2 b/util/fipstools/acvp/acvptool/test/vectors/SHAKE-256.bz2
new file mode 100644
index 0000000..3617de6
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/vectors/SHAKE-256.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
index f35138c..9125282 100644
--- a/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
+++ b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
@@ -64,6 +64,12 @@
 	"EDDSA/keyVer":             eddsaKeyVer,
 	"EDDSA/sigGen":             eddsaSigGen,
 	"EDDSA/sigVer":             eddsaSigVer,
+	"SHAKE-128":                shakeAftVot(sha3.NewShake128),
+	"SHAKE-128/VOT":            shakeAftVot(sha3.NewShake128),
+	"SHAKE-128/MCT":            shakeMct(sha3.NewShake128),
+	"SHAKE-256":                shakeAftVot(sha3.NewShake256),
+	"SHAKE-256/VOT":            shakeAftVot(sha3.NewShake256),
+	"SHAKE-256/MCT":            shakeMct(sha3.NewShake256),
 }
 
 func flush(args [][]byte) error {
@@ -229,6 +235,28 @@
 		"pure": true,
 		"preHash": true,
 		"curve": ["ED-25519"]}
+	}, {
+		"algorithm": "SHAKE-128",
+		"inBit": false,
+		"outBit": false,
+		"inEmpty": false,
+		"outputLen": [{
+			"min": 128,
+			"max": 4096,
+			"increment": 8
+		}],
+		"revision": "1.0"
+	}, {
+		"algorithm": "SHAKE-256",
+		"inBit": false,
+		"outBit": false,
+		"inEmpty": false,
+		"outputLen": [{
+			"min": 128,
+			"max": 4096,
+			"increment": 8
+		}],
+		"revision": "1.0"
 	}
 ]`)); err != nil {
 		return err
@@ -671,6 +699,72 @@
 	return reply([]byte{1})
 }
 
+func shakeAftVot(digestFn func() sha3.ShakeHash) func([][]byte) error {
+	return func(args [][]byte) error {
+		if len(args) != 2 {
+			return fmt.Errorf("shakeAftVot received %d args, wanted 2", len(args))
+		}
+
+		msg := args[0]
+		outLenBytes := binary.LittleEndian.Uint32(args[1])
+
+		h := digestFn()
+		h.Write(msg)
+		digest := make([]byte, outLenBytes)
+		h.Read(digest)
+
+		return reply(digest)
+	}
+}
+
+func shakeMct(digestFn func() sha3.ShakeHash) func([][]byte) error {
+	return func(args [][]byte) error {
+		if len(args) != 4 {
+			return fmt.Errorf("shakeMct received %d args, wanted 4", len(args))
+		}
+
+		md := args[0]
+		minOutBytes := binary.LittleEndian.Uint32(args[1])
+		maxOutBytes := binary.LittleEndian.Uint32(args[2])
+
+		outputLenBytes := binary.LittleEndian.Uint32(args[3])
+		if outputLenBytes < 2 {
+			return fmt.Errorf("invalid output length: %d", outputLenBytes)
+		}
+
+		if maxOutBytes < minOutBytes {
+			return fmt.Errorf("invalid maxOutBytes and minOutBytes: %d, %d", maxOutBytes, minOutBytes)
+		}
+
+		rangeBytes := maxOutBytes - minOutBytes + 1
+
+		for i := 0; i < 1000; i++ {
+			// "The MSG[i] input to SHAKE MUST always contain at least 128 bits. If this is not the case
+			// as the previous digest was too short, append empty bits to the rightmost side of the digest."
+			boundary := min(len(md), 16)
+			msg := make([]byte, 16)
+			copy(msg, md[:boundary])
+
+			//  MD[i] = SHAKE(MSG[i], OutputLen * 8)
+			h := digestFn()
+			h.Write(msg)
+			digest := make([]byte, outputLenBytes)
+			h.Read(digest)
+			md = digest
+
+			// RightmostOutputBits = 16 rightmost bits of MD[i] as an integer
+			// OutputLen = minOutBytes + (RightmostOutputBits % Range)
+			rightmostOutput := uint32(md[outputLenBytes-2])<<8 | uint32(md[outputLenBytes-1])
+			outputLenBytes = minOutBytes + (rightmostOutput % rangeBytes)
+		}
+
+		encodedOutputLenBytes := make([]byte, 4)
+		binary.LittleEndian.PutUint32(encodedOutputLenBytes, outputLenBytes)
+
+		return reply(md, encodedOutputLenBytes)
+	}
+}
+
 const (
 	maxArgs       = 9
 	maxArgLength  = 1 << 20