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