SLH-DSA: add ACVP support
Change-Id: I41638aa7a4d00415eda593fe277fed6f768170de
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/73928
Reviewed-by: Bob Beck <bbe@google.com>
Commit-Queue: Adam Langley <agl@google.com>
diff --git a/util/fipstools/acvp/ACVP.md b/util/fipstools/acvp/ACVP.md
index 1004c84..79cf9c8 100644
--- a/util/fipstools/acvp/ACVP.md
+++ b/util/fipstools/acvp/ACVP.md
@@ -136,6 +136,9 @@
| ML-KEM-XX/keyGen | Seed | Public key, private key |
| ML-KEM-XX/encap | Public key, entropy | Ciphertext, shared secret |
| ML-KEM-XX/decap | Private key, ciphertext | Shared secret |
+| SLH-DSA-XX/keyGen | Seed | Private key, public key |
+| SLH-DSA-XX/sigGen | Private key, message, entropy or empty | Signature |
+| SLH-DSA-XX/sigVer | Public key, message, signature | Single-byte validity flag |
¹ The iterated tests would result in excessive numbers of round trips if the module wrapper handled only basic operations. Thus some ACVP logic is pushed down for these tests so that the inner loop can be handled locally. Either read the NIST documentation ([block-ciphers](https://pages.nist.gov/ACVP/draft-celi-acvp-symmetric.html#name-monte-carlo-tests-for-block) [hashes](https://pages.nist.gov/ACVP/draft-celi-acvp-sha.html#name-monte-carlo-tests-for-sha-1)) to understand the iteration count and return values or, probably more fruitfully, see how these functions are handled in the `modulewrapper` directory.
diff --git a/util/fipstools/acvp/acvptool/subprocess/slhdsa.go b/util/fipstools/acvp/acvptool/subprocess/slhdsa.go
new file mode 100644
index 0000000..0bae1aa
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/subprocess/slhdsa.go
@@ -0,0 +1,305 @@
+package subprocess
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "strings"
+)
+
+// Common top-level structure to parse mode
+type slhdsaTestVectorSet struct {
+ Algorithm string `json:"algorithm"`
+ Mode string `json:"mode"`
+ Revision string `json:"revision"`
+}
+
+type slhdsaKeyGenTestVectorSet struct {
+ Algorithm string `json:"algorithm"`
+ Mode string `json:"mode"`
+ Revision string `json:"revision"`
+ Groups []slhdsaKeyGenTestGroup `json:"testGroups"`
+}
+
+type slhdsaKeyGenTestGroup struct {
+ ID uint64 `json:"tgId"`
+ TestType string `json:"testType"`
+ ParameterSet string `json:"parameterSet"`
+ Tests []slhdsaKeyGenTest `json:"tests"`
+}
+
+type slhdsaKeyGenTest struct {
+ ID uint64 `json:"tcId"`
+ SKSeed string `json:"skSeed"`
+ SKPrf string `json:"skPrf"`
+ PKSeed string `json:"pkSeed"`
+}
+
+type slhdsaKeyGenTestGroupResponse struct {
+ ID uint64 `json:"tgId"`
+ Tests []slhdsaKeyGenTestResponse `json:"tests"`
+}
+
+type slhdsaKeyGenTestResponse struct {
+ ID uint64 `json:"tcId"`
+ PublicKey string `json:"pk"`
+ PrivateKey string `json:"sk"`
+}
+
+type slhdsaSigGenTestVectorSet struct {
+ Algorithm string `json:"algorithm"`
+ Mode string `json:"mode"`
+ Revision string `json:"revision"`
+ Groups []slhdsaSigGenTestGroup `json:"testGroups"`
+}
+
+type slhdsaSigGenTestGroup struct {
+ ID uint64 `json:"tgId"`
+ TestType string `json:"testType"`
+ ParameterSet string `json:"parameterSet"`
+ Deterministic bool `json:"deterministic"`
+ Tests []slhdsaSigGenTest `json:"tests"`
+}
+
+type slhdsaSigGenTest struct {
+ ID uint64 `json:"tcId"`
+ Message string `json:"message"`
+ PrivateKey string `json:"sk"`
+ AdditionalRandomness string `json:"additionalRandomness,omitempty"`
+}
+
+type slhdsaSigGenTestGroupResponse struct {
+ ID uint64 `json:"tgId"`
+ Tests []slhdsaSigGenTestResponse `json:"tests"`
+}
+
+type slhdsaSigGenTestResponse struct {
+ ID uint64 `json:"tcId"`
+ Signature string `json:"signature"`
+}
+
+type slhdsaSigVerTestVectorSet struct {
+ Algorithm string `json:"algorithm"`
+ Mode string `json:"mode"`
+ Revision string `json:"revision"`
+ Groups []slhdsaSigVerTestGroup `json:"testGroups"`
+}
+
+type slhdsaSigVerTestGroup struct {
+ ID uint64 `json:"tgId"`
+ TestType string `json:"testType"`
+ ParameterSet string `json:"parameterSet"`
+ Tests []slhdsaSigVerTest `json:"tests"`
+}
+
+type slhdsaSigVerTest struct {
+ ID uint64 `json:"tcId"`
+ Message string `json:"message"`
+ Signature string `json:"signature"`
+ PublicKey string `json:"pk"`
+}
+
+type slhdsaSigVerTestGroupResponse struct {
+ ID uint64 `json:"tgId"`
+ Tests []slhdsaSigVerTestResponse `json:"tests"`
+}
+
+type slhdsaSigVerTestResponse struct {
+ ID uint64 `json:"tcId"`
+ TestPassed bool `json:"testPassed"`
+}
+
+type slhdsa struct{}
+
+func (s *slhdsa) Process(vectorSet []byte, t Transactable) (any, error) {
+ var common slhdsaTestVectorSet
+ if err := json.Unmarshal(vectorSet, &common); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal vector set: %v", err)
+ }
+
+ switch common.Mode {
+ case "keyGen":
+ return s.processKeyGen(vectorSet, t)
+ case "sigGen":
+ return s.processSigGen(vectorSet, t)
+ case "sigVer":
+ return s.processSigVer(vectorSet, t)
+ default:
+ return nil, fmt.Errorf("unsupported SLH-DSA mode: %s", common.Mode)
+ }
+}
+
+func (s *slhdsa) processKeyGen(vectorSet []byte, t Transactable) (any, error) {
+ var parsed slhdsaKeyGenTestVectorSet
+ if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal keyGen vector set: %v", err)
+ }
+
+ var ret []slhdsaKeyGenTestGroupResponse
+
+ for _, group := range parsed.Groups {
+ response := slhdsaKeyGenTestGroupResponse{
+ ID: group.ID,
+ }
+
+ if !strings.HasPrefix(group.ParameterSet, "SLH-DSA-") {
+ return nil, fmt.Errorf("invalid parameter set: %s", group.ParameterSet)
+ }
+ cmdName := group.ParameterSet + "/keyGen"
+
+ for _, test := range group.Tests {
+ skSeed, err := hex.DecodeString(test.SKSeed)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode skSeed in test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+
+ skPrf, err := hex.DecodeString(test.SKPrf)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode skPrf in test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+
+ pkSeed, err := hex.DecodeString(test.PKSeed)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode pkSeed in test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+
+ var seed []byte
+ seed = append(seed, skSeed...)
+ seed = append(seed, skPrf...)
+ seed = append(seed, pkSeed...)
+
+ result, err := t.Transact(cmdName, 2, seed)
+ if err != nil {
+ return nil, fmt.Errorf("key generation failed for test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+
+ response.Tests = append(response.Tests, slhdsaKeyGenTestResponse{
+ ID: test.ID,
+ PrivateKey: hex.EncodeToString(result[0]),
+ PublicKey: hex.EncodeToString(result[1]),
+ })
+ }
+
+ ret = append(ret, response)
+ }
+
+ return ret, nil
+}
+
+func (s *slhdsa) processSigGen(vectorSet []byte, t Transactable) (any, error) {
+ var parsed slhdsaSigGenTestVectorSet
+ if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal sigGen vector set: %v", err)
+ }
+
+ var ret []slhdsaSigGenTestGroupResponse
+
+ for _, group := range parsed.Groups {
+ response := slhdsaSigGenTestGroupResponse{
+ ID: group.ID,
+ }
+
+ if !strings.HasPrefix(group.ParameterSet, "SLH-DSA-") {
+ return nil, fmt.Errorf("invalid parameter set: %s", group.ParameterSet)
+ }
+ cmdName := group.ParameterSet + "/sigGen"
+
+ for _, test := range group.Tests {
+ sk, err := hex.DecodeString(test.PrivateKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode private key in test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+
+ msg, err := hex.DecodeString(test.Message)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode message in test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+
+ var randomness []byte
+ if !group.Deterministic {
+ randomness, err = hex.DecodeString(test.AdditionalRandomness)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode randomness in test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+ }
+
+ result, err := t.Transact(cmdName, 1, sk, msg, randomness)
+ if err != nil {
+ return nil, fmt.Errorf("signature generation failed for test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+
+ response.Tests = append(response.Tests, slhdsaSigGenTestResponse{
+ ID: test.ID,
+ Signature: hex.EncodeToString(result[0]),
+ })
+ }
+
+ ret = append(ret, response)
+ }
+
+ return ret, nil
+}
+
+func (s *slhdsa) processSigVer(vectorSet []byte, t Transactable) (any, error) {
+ var parsed slhdsaSigVerTestVectorSet
+ if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal sigVer vector set: %v", err)
+ }
+
+ var ret []slhdsaSigVerTestGroupResponse
+
+ for _, group := range parsed.Groups {
+ response := slhdsaSigVerTestGroupResponse{
+ ID: group.ID,
+ }
+
+ if !strings.HasPrefix(group.ParameterSet, "SLH-DSA-") {
+ return nil, fmt.Errorf("invalid parameter set: %s", group.ParameterSet)
+ }
+ cmdName := group.ParameterSet + "/sigVer"
+
+ for _, test := range group.Tests {
+ pk, err := hex.DecodeString(test.PublicKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode public key in test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+
+ msg, err := hex.DecodeString(test.Message)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode message in test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+
+ sig, err := hex.DecodeString(test.Signature)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode signature in test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+
+ result, err := t.Transact(cmdName, 1, pk, msg, sig)
+ if err != nil {
+ return nil, fmt.Errorf("signature verification failed for test case %d/%d: %s",
+ group.ID, test.ID, err)
+ }
+
+ testPassed := result[0][0] != 0
+ response.Tests = append(response.Tests, slhdsaSigVerTestResponse{
+ ID: test.ID,
+ TestPassed: testPassed,
+ })
+ }
+
+ ret = append(ret, response)
+ }
+
+ return ret, nil
+}
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
index 0913220..d68e5d3 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -146,6 +146,7 @@
"PBKDF": &pbkdf{},
"ML-DSA": &mldsa{},
"ML-KEM": &mlkem{},
+ "SLH-DSA": &slhdsa{},
}
m.primitives["ECDSA"] = &ecdsa{"ECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives}
m.primitives["DetECDSA"] = &ecdsa{"DetECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives}
diff --git a/util/fipstools/acvp/acvptool/test/expected/SLH-DSA.bz2 b/util/fipstools/acvp/acvptool/test/expected/SLH-DSA.bz2
new file mode 100644
index 0000000..0f49ac6
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/expected/SLH-DSA.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/tests.json b/util/fipstools/acvp/acvptool/test/tests.json
index 736e1d9..17dd7d8 100644
--- a/util/fipstools/acvp/acvptool/test/tests.json
+++ b/util/fipstools/acvp/acvptool/test/tests.json
@@ -33,6 +33,7 @@
{"Wrapper": "modulewrapper", "In": "vectors/SHA2-256.bz2", "Out": "expected/SHA2-256.bz2"},
{"Wrapper": "modulewrapper", "In": "vectors/SHA2-384.bz2", "Out": "expected/SHA2-384.bz2"},
{"Wrapper": "modulewrapper", "In": "vectors/SHA2-512.bz2", "Out": "expected/SHA2-512.bz2"},
+{"Wrapper": "modulewrapper", "In": "vectors/SLH-DSA.bz2", "Out": "expected/SLH-DSA.bz2"},
{"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"},
diff --git a/util/fipstools/acvp/acvptool/test/vectors/SLH-DSA.bz2 b/util/fipstools/acvp/acvptool/test/vectors/SLH-DSA.bz2
new file mode 100644
index 0000000..7b87ae0
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/vectors/SLH-DSA.bz2
Binary files differ
diff --git a/util/fipstools/acvp/modulewrapper/modulewrapper.cc b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
index d5d0f95..7d4a0fc 100644
--- a/util/fipstools/acvp/modulewrapper/modulewrapper.cc
+++ b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
@@ -1004,6 +1004,60 @@
"encapsulation",
"decapsulation"
]
+ },
+ {
+ "algorithm": "SLH-DSA",
+ "mode": "keyGen",
+ "revision": "FIPS205",
+ "parameterSets": [
+ "SLH-DSA-SHA2-128s"
+ ]
+ },
+ {
+ "algorithm": "SLH-DSA",
+ "mode": "sigGen",
+ "revision": "FIPS205",
+ "deterministic": [
+ true,
+ false
+ ],
+ "capabilities": [
+ {
+ "parameterSets": [
+ "SLH-DSA-SHA2-128s"
+ ],
+ "messageLength": [
+ {
+ "min": 8,
+ "max": 65536,
+ "increment": 8
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "algorithm": "SLH-DSA",
+ "mode": "sigVer",
+ "revision": "FIPS205",
+ "deterministic": [
+ true,
+ false
+ ],
+ "capabilities": [
+ {
+ "parameterSets": [
+ "SLH-DSA-SHA2-128s"
+ ],
+ "messageLength": [
+ {
+ "min": 8,
+ "max": 65536,
+ "increment": 8
+ }
+ ]
+ }
+ ]
}
])";
return write_reply({Span<const uint8_t>(
@@ -2360,6 +2414,72 @@
return write_reply({shared_secret});
}
+static bool SLHDSAKeyGen(const Span<const uint8_t> args[],
+ ReplyCallback write_reply) {
+ const Span<const uint8_t> seed = args[0];
+
+ if (seed.size() != 3 * BCM_SLHDSA_SHA2_128S_N) {
+ LOG_ERROR("Bad seed size.\n");
+ return false;
+ }
+
+ uint8_t public_key[BCM_SLHDSA_SHA2_128S_PUBLIC_KEY_BYTES];
+ uint8_t private_key[BCM_SLHDSA_SHA2_128S_PRIVATE_KEY_BYTES];
+ BCM_slhdsa_sha2_128s_generate_key_from_seed(public_key, private_key,
+ seed.data());
+
+ return write_reply({private_key, public_key});
+}
+
+static bool SLHDSASigGen(const Span<const uint8_t> args[],
+ ReplyCallback write_reply) {
+ const Span<const uint8_t> private_key = args[0];
+ const Span<const uint8_t> msg = args[1];
+ const Span<const uint8_t> entropy_span = args[2];
+
+ if (private_key.size() != BCM_SLHDSA_SHA2_128S_PRIVATE_KEY_BYTES) {
+ LOG_ERROR("Bad private key size.\n");
+ return false;
+ }
+
+ uint8_t entropy[BCM_SLHDSA_SHA2_128S_N];
+ if (!entropy_span.empty()) {
+ if (entropy_span.size() != BCM_SLHDSA_SHA2_128S_N) {
+ LOG_ERROR("Bad entropy size.\n");
+ return false;
+ }
+ memcpy(entropy, entropy_span.data(), entropy_span.size());
+ } else {
+ memcpy(entropy, private_key.data() + 32, 16);
+ }
+
+ uint8_t signature[BCM_SLHDSA_SHA2_128S_SIGNATURE_BYTES];
+ BCM_slhdsa_sha2_128s_sign_internal(signature, private_key.data(), nullptr,
+ nullptr, 0, msg.data(), msg.size(),
+ entropy);
+
+ return write_reply({signature});
+}
+
+static bool SLHDSASigVer(const Span<const uint8_t> args[],
+ ReplyCallback write_reply) {
+ const Span<const uint8_t> public_key = args[0];
+ const Span<const uint8_t> msg = args[1];
+ const Span<const uint8_t> signature = args[2];
+
+ if (public_key.size() != BCM_SLHDSA_SHA2_128S_PUBLIC_KEY_BYTES) {
+ LOG_ERROR("Bad public key size.\n");
+ return false;
+ }
+
+ const int ok = bcm_success(BCM_slhdsa_sha2_128s_verify_internal(
+ signature.data(), signature.size(), public_key.data(), nullptr, nullptr,
+ 0, msg.data(), msg.size()));
+
+ const uint8_t ok_byte = ok ? 1 : 0;
+ return write_reply({Span<const uint8_t>(&ok_byte, 1)});
+}
+
static constexpr struct {
char name[kMaxNameLength + 1];
uint8_t num_expected_args;
@@ -2495,6 +2615,9 @@
{"ML-KEM-1024/decap", 2,
MLKEMDecap<BCM_mlkem1024_private_key, BCM_mlkem1024_parse_private_key,
BCM_mlkem1024_decap>},
+ {"SLH-DSA-SHA2-128s/keyGen", 1, SLHDSAKeyGen},
+ {"SLH-DSA-SHA2-128s/sigGen", 3, SLHDSASigGen},
+ {"SLH-DSA-SHA2-128s/sigVer", 3, SLHDSASigVer},
};
Handler FindHandler(Span<const Span<const uint8_t>> args) {