util/fipstools: cSHAKE ACVP algorithm support

Implements support for cSHAKE-128 and cSHAKE-256 ACVP testing (AFT and
MCT test types) based on the NIST specification:

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

The Go testmodulewrapper is updated to implement the new command
handlers.

Change-Id: I4bb459756273df10945cc814bd4a6a9d052641dd
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/75687
Reviewed-by: Bob Beck <bbe@google.com>
Commit-Queue: Bob Beck <bbe@google.com>
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/util/fipstools/acvp/ACVP.md b/util/fipstools/acvp/ACVP.md
index 95160e9..5fd2866 100644
--- a/util/fipstools/acvp/ACVP.md
+++ b/util/fipstools/acvp/ACVP.md
@@ -120,6 +120,8 @@
 | 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 |
+| cSHAKE-128           | Value to hash, output length bytes, function name bytes, customization bytes | Digest |
+| cSHAKE-128/MCT       | Initial seed¹, min output bytes, max output bytes, output length bytes, customization bytes | Digest, output length bytes, customization bytes |
 | SHA-1/MCT            | Initial seed¹             | Digest  |
 | SHA2-224/MCT         | Initial seed¹             | Digest  |
 | SHA2-256/MCT         | Initial seed¹             | Digest  |
diff --git a/util/fipstools/acvp/acvptool/subprocess/cshake.go b/util/fipstools/acvp/acvptool/subprocess/cshake.go
new file mode 100644
index 0000000..3c961b5
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/subprocess/cshake.go
@@ -0,0 +1,173 @@
+// Copyright (c) 2025, 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 XOF cSHAKE tests. See
+// https://pages.nist.gov/ACVP/draft-celi-acvp-xof.html#name-test-vectors
+
+type cShakeTestVectorSet struct {
+	Groups []cShakeTestGroup `json:"testGroups"`
+}
+
+type cShakeTestGroup struct {
+	ID                  uint64 `json:"tgId"`
+	Type                string `json:"testType"`
+	HexCustomization    bool   `json:"hexCustomization"`
+	MaxOutLenBits       uint32 `json:"maxOutLen"`
+	MinOutLenBits       uint32 `json:"minOutLen"`
+	OutLenIncrementBits uint32 `json:"outLenIncrement"`
+	Tests               []struct {
+		ID               uint64 `json:"tcId"`
+		MsgHex           string `json:"msg"`
+		BitLength        uint64 `json:"len"`
+		FunctionName     string `json:"functionName"`
+		Customization    string `json:"customization"`
+		CustomizationHex string `json:"customizationHex"`
+		OutLenBits       uint32 `json:"outLen"`
+	} `json:"tests"`
+}
+
+type cShakeTestGroupResponse struct {
+	ID    uint64               `json:"tgId"`
+	Tests []cShakeTestResponse `json:"tests"`
+}
+
+type cShakeTestResponse struct {
+	ID         uint64            `json:"tcId"`
+	DigestHex  string            `json:"md,omitempty"`
+	OutLenBits uint32            `json:"outLen,omitempty"`
+	MCTResults []cShakeMCTResult `json:"resultsArray,omitempty"`
+}
+
+type cShakeMCTResult struct {
+	DigestHex  string `json:"md"`
+	OutLenBits uint32 `json:"outLen,omitempty"`
+}
+
+type cShake struct {
+	algo string
+}
+
+func (h *cShake) Process(vectorSet []byte, m Transactable) (any, error) {
+	var parsed cShakeTestVectorSet
+	if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+		return nil, err
+	}
+
+	// See
+	// https://pages.nist.gov/ACVP/draft-celi-acvp-xof.html#name-test-types
+	// for details about the tests.
+	var ret []cShakeTestGroupResponse
+	for _, group := range parsed.Groups {
+		group := group
+		response := cShakeTestGroupResponse{
+			ID: group.ID,
+		}
+
+		if group.HexCustomization {
+			return nil, fmt.Errorf("test group %d has unsupported hex customization", group.ID)
+		}
+
+		for _, test := range group.Tests {
+			test := test
+
+			if test.CustomizationHex != "" {
+				return nil, fmt.Errorf("test case %d/%d has unsupported hex customization", group.ID, test.ID)
+			}
+
+			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.OutLenBits%8 != 0 {
+				return nil, fmt.Errorf("test case %d/%d has bit length %d - fractional bytes not supported", group.ID, test.ID, test.OutLenBits)
+			}
+
+			switch group.Type {
+			case "AFT":
+				args := [][]byte{msg, uint32le(test.OutLenBits / 8), []byte(test.FunctionName), []byte(test.Customization)}
+				m.TransactAsync(h.algo, 1, args, func(result [][]byte) error {
+					response.Tests = append(response.Tests, cShakeTestResponse{
+						ID:         test.ID,
+						DigestHex:  hex.EncodeToString(result[0]),
+						OutLenBits: test.OutLenBits,
+					})
+					return nil
+				})
+			case "MCT":
+				testResponse := cShakeTestResponse{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)
+				}
+				if group.OutLenIncrementBits%8 != 0 {
+					return nil, fmt.Errorf("MCT test group %d has output length increment %d - fractional bytes not supported", group.ID, group.OutLenIncrementBits)
+				}
+
+				minOutLenBytes := uint32le(group.MinOutLenBits / 8)
+				maxOutLenBytes := uint32le(group.MaxOutLenBits / 8)
+				outputLenBytes := uint32le(group.MaxOutLenBits / 8)
+				incrementBytes := uint32le(group.OutLenIncrementBits / 8)
+				var mctCustomization []byte
+
+				for i := 0; i < 100; i++ {
+					args := [][]byte{msg, minOutLenBytes, maxOutLenBytes, outputLenBytes, incrementBytes, mctCustomization}
+					result, err := m.Transact(h.algo+"/MCT", 3, args...)
+					if err != nil {
+						panic(h.algo + " mct operation failed: " + err.Error())
+					}
+
+					msg = result[0]
+					outputLenBytes = uint32le(binary.LittleEndian.Uint32(result[1]))
+					mctCustomization = result[2]
+
+					mctResult := cShakeMCTResult{
+						DigestHex:  hex.EncodeToString(msg),
+						OutLenBits: uint32(len(msg) * 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 feae455..4ab19d4 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -110,6 +110,8 @@
 		"SHA3-512":          &hashPrimitive{"SHA3-512", 64},
 		"SHAKE-128":         &shake{"SHAKE-128", 16},
 		"SHAKE-256":         &shake{"SHAKE-256", 32},
+		"cSHAKE-128":        &cShake{"cSHAKE-128"},
+		"cSHAKE-256":        &cShake{"cSHAKE-256"},
 		"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/cSHAKE-128.bz2 b/util/fipstools/acvp/acvptool/test/expected/cSHAKE-128.bz2
new file mode 100644
index 0000000..340b41a
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/expected/cSHAKE-128.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/expected/cSHAKE-256.bz2 b/util/fipstools/acvp/acvptool/test/expected/cSHAKE-256.bz2
new file mode 100644
index 0000000..2bf30e9
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/expected/cSHAKE-256.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/tests.json b/util/fipstools/acvp/acvptool/test/tests.json
index 17dd7d8..bc7cecb 100644
--- a/util/fipstools/acvp/acvptool/test/tests.json
+++ b/util/fipstools/acvp/acvptool/test/tests.json
@@ -39,5 +39,7 @@
 {"Wrapper": "testmodulewrapper", "In": "vectors/PBKDF.bz2", "Out": "expected/PBKDF.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"}
+{"Wrapper": "testmodulewrapper", "In": "vectors/SHAKE-256.bz2", "Out": "expected/SHAKE-256.bz2"},
+{"Wrapper": "testmodulewrapper", "In": "vectors/cSHAKE-128.bz2", "Out": "expected/cSHAKE-128.bz2"},
+{"Wrapper": "testmodulewrapper", "In": "vectors/cSHAKE-256.bz2", "Out": "expected/cSHAKE-256.bz2"}
 ]
diff --git a/util/fipstools/acvp/acvptool/test/vectors/cSHAKE-128.bz2 b/util/fipstools/acvp/acvptool/test/vectors/cSHAKE-128.bz2
new file mode 100644
index 0000000..ae74aae
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/vectors/cSHAKE-128.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/vectors/cSHAKE-256.bz2 b/util/fipstools/acvp/acvptool/test/vectors/cSHAKE-256.bz2
new file mode 100644
index 0000000..42600bd
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/vectors/cSHAKE-256.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
index 1546db6..8662605 100644
--- a/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
+++ b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
@@ -70,6 +70,10 @@
 	"SHAKE-256":                shakeAftVot(sha3.NewShake256),
 	"SHAKE-256/VOT":            shakeAftVot(sha3.NewShake256),
 	"SHAKE-256/MCT":            shakeMct(sha3.NewShake256),
+	"cSHAKE-128":               cShakeAft(sha3.NewCShake128),
+	"cSHAKE-128/MCT":           cShakeMct(sha3.NewCShake128),
+	"cSHAKE-256":               cShakeAft(sha3.NewCShake256),
+	"cSHAKE-256/MCT":           cShakeMct(sha3.NewCShake256),
 }
 
 func flush(args [][]byte) error {
@@ -257,6 +261,34 @@
 			"increment": 8
 		}],
 		"revision": "1.0"
+	}, {
+		"algorithm": "cSHAKE-128",
+		"hexCustomization": false,
+		"outputLen": [{
+			"min": 16,
+			"max": 65536,
+			"increment": 8
+		}],
+		"msgLen": [{
+			"min": 0,
+			"max": 65536,
+			"increment": 8
+		}],
+		"revision": "1.0"
+	}, {
+		"algorithm": "cSHAKE-256",
+		"hexCustomization": false,
+		"outputLen": [{
+			"min": 16,
+			"max": 65536,
+			"increment": 8
+		}],
+		"msgLen": [{
+			"min": 0,
+			"max": 65536,
+			"increment": 8
+		}],
+		"revision": "1.0"
 	}
 ]`)); err != nil {
 		return err
@@ -765,6 +797,91 @@
 	}
 }
 
+func cShakeAft(hFn func(N, S []byte) sha3.ShakeHash) func([][]byte) error {
+	return func(args [][]byte) error {
+		if len(args) != 4 {
+			return fmt.Errorf("cShakeAft received %d args, wanted 4", len(args))
+		}
+
+		msg := args[0]
+		outLenBytes := binary.LittleEndian.Uint32(args[1])
+		functionName := args[2]
+		customization := args[3]
+
+		h := hFn(functionName, customization)
+		h.Write(msg)
+		digest := make([]byte, outLenBytes)
+		h.Read(digest)
+
+		return reply(digest)
+	}
+}
+
+func cShakeMct(hFn func(N, S []byte) sha3.ShakeHash) func([][]byte) error {
+	return func(args [][]byte) error {
+		if len(args) != 6 {
+			return fmt.Errorf("cShakeMct received %d args, wanted 6", len(args))
+		}
+
+		message := args[0]
+		minOutLenBytes := binary.LittleEndian.Uint32(args[1])
+		maxOutLenBytes := binary.LittleEndian.Uint32(args[2])
+		outputLenBytes := binary.LittleEndian.Uint32(args[3])
+		incrementBytes := binary.LittleEndian.Uint32(args[4])
+		customization := args[5]
+
+		if outputLenBytes < 2 {
+			return fmt.Errorf("invalid output length: %d", outputLenBytes)
+		}
+
+		rangeBits := (maxOutLenBytes*8 - minOutLenBytes*8) + 1
+		if rangeBits == 0 {
+			return fmt.Errorf("invalid maxOutLenBytes and minOutLenBytes: %d, %d", maxOutLenBytes, minOutLenBytes)
+		}
+
+		// cSHAKE Monte Carlo test inner loop:
+		//   https://pages.nist.gov/ACVP/draft-celi-acvp-xof.html#section-6.2.1
+		for i := 0; i < 1000; i++ {
+			// InnerMsg = Left(Output[i-1] || ZeroBits(128), 128);
+			boundary := min(len(message), 16)
+			innerMsg := make([]byte, 16)
+			copy(innerMsg, message[:boundary])
+
+			// Output[i] = CSHAKE(InnerMsg, OutputLen, FunctionName, Customization);
+			h := hFn(nil, customization) // Note: function name fixed to "" for MCT.
+			h.Write(innerMsg)
+			digest := make([]byte, outputLenBytes)
+			h.Read(digest)
+			message = digest
+
+			// Rightmost_Output_bits = Right(Output[i], 16);
+			rightmostOutput := digest[outputLenBytes-2:]
+			// IMPORTANT: the specification says:
+			//   NOTE: For the "Rightmost_Output_bits % Range" operation, the Rightmost_Output_bits bit string
+			//   should be interpreted as a little endian-encoded number.
+			// This is **a lie**! It has to be interpreted as a big-endian number.
+			rightmostOutputBE := binary.BigEndian.Uint16(rightmostOutput)
+
+			// OutputLen = MinOutLen + (floor((Rightmost_Output_bits % Range) / OutLenIncrement) * OutLenIncrement);
+			incrementBits := incrementBytes * 8
+			outputLenBits := (minOutLenBytes * 8) + (((uint32)(rightmostOutputBE)%rangeBits)/incrementBits)*incrementBits
+			outputLenBytes = outputLenBits / 8
+
+			// Customization = BitsToString(InnerMsg || Rightmost_Output_bits);
+			msgWithBits := append(innerMsg, rightmostOutput...)
+			customization = make([]byte, len(msgWithBits))
+			for i, b := range msgWithBits {
+				customization[i] = (b % 26) + 65
+			}
+		}
+
+		encodedOutputLenBytes := make([]byte, 4)
+		binary.LittleEndian.PutUint32(encodedOutputLenBytes, outputLenBytes)
+
+		return reply(message, encodedOutputLenBytes, customization)
+	}
+}
+
 const (
 	maxArgs       = 9
 	maxArgLength  = 1 << 20