util/fipstools: basic support for KTS-IFC
Some notable limitations:
* Only the KTS-OAEP-basic scheme is supported.
* Only the rsakpg1-basic keygen mode is supported.
* Only the AFT test type is supported (no partialVal function support).
See the NIST KTS-IFC ACVP spec for more information:
https://pages.nist.gov/ACVP/draft-hammett-acvp-kas-ifc.html
Change-Id: I0f3760dbb20f537e443b52e209db2a9578bbb4dd
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/75510
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 ffd47e5..95160e9 100644
--- a/util/fipstools/acvp/ACVP.md
+++ b/util/fipstools/acvp/ACVP.md
@@ -144,6 +144,8 @@
| SLH-DSA-XX/sigVer | Public key, message, signature | Single-byte validity flag |
| SSHKDF/<HASH>/client | K, H, SessionID, cipher algorithm | client IV key, client encryption key, client integrity key |
| SSHKDF/<HASH>/server | K, H, SessionID, cipher algorithm | server IV key, server encryption key, server integrity key |
+| KTS-IFC/<HASH>/initiator | output length bytes, serverN bytes, serverE bytes | generated ciphertext (iutC), derived keying material (dkm) |
+| KTS-IFC/<HASH>/responder | iutN bytes, iutE bytes, iutP bytes, iutQ bytes, iutD bytes, ciphertext (serverC) bytes | derived keying material (dkm) |
¹ 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/kts.go b/util/fipstools/acvp/acvptool/subprocess/kts.go
new file mode 100644
index 0000000..ffbe095
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/subprocess/kts.go
@@ -0,0 +1,219 @@
+// Copyright 2025 The BoringSSL Authors
+//
+// 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/hex"
+ "encoding/json"
+ "fmt"
+)
+
+// The following structures reflect the JSON of ACVP KTS-IFC tests. See
+// https://pages.nist.gov/ACVP/draft-hammett-acvp-kas-ifc.html#name-test-vectors
+
+type ktsVectorSet struct {
+ Groups []ktsTestGroup `json:"testGroups"`
+ Revision string `json:"revision"`
+}
+
+type ktsTestGroup struct {
+ ID uint64 `json:"tgId"`
+ Type string `json:"testType"`
+ Scheme string `json:"scheme"`
+ Role string `json:"kasRole"`
+ Modulo uint64 `json:"modulo"`
+ KeyGen string `json:"keyGenerationMethod"`
+ KTSConf ktsConfig `json:"ktsConfiguration"`
+ OutputBits uint64 `json:"l"`
+ Tests []ktsTest `json:"tests"`
+}
+
+type ktsConfig struct {
+ HashAlg string `json:"hashAlg"`
+ AssociatedDataPattern string `json:"associatedDataPattern"`
+ Encoding string `json:"encoding"`
+}
+
+type ktsTest struct {
+ ID uint64 `json:"tcId"`
+
+ ServerN string `json:"serverN,omitempty"`
+ ServerE string `json:"serverE,omitempty"`
+ ServerC string `json:"serverC,omitempty"`
+
+ IutN string `json:"iutN,omitempty"`
+ IutE string `json:"iutE,omitempty"`
+ IutP string `json:"iutP,omitempty"`
+ IutQ string `json:"iutQ,omitempty"`
+ IutD string `json:"iutD,omitempty"`
+}
+
+type ktsTestGroupResponse struct {
+ ID uint64 `json:"tgId"`
+ Tests []ktsTestResponse `json:"tests"`
+}
+
+type ktsTestResponse struct {
+ ID uint64 `json:"tcId"`
+ IutC string `json:"iutC,omitempty"` // initiator role only
+ Dkm string `json:"dkm,omitempty"`
+}
+
+type kts struct {
+ hashAlgs map[string]bool // the supported hash algorithm primitives
+}
+
+func (k *kts) Process(vectorSet []byte, m Transactable) (any, error) {
+ var parsed ktsVectorSet
+ if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+ return nil, err
+ }
+
+ if parsed.Revision != "Sp800-56Br2" {
+ return nil, fmt.Errorf("unsupported revision %q", parsed.Revision)
+ }
+
+ var ret []ktsTestGroupResponse
+ for _, group := range parsed.Groups {
+ group := group
+ response := ktsTestGroupResponse{
+ ID: group.ID,
+ }
+
+ if group.Type != "AFT" {
+ return nil, fmt.Errorf("unsupported test type %q in test group %d", group.Type, group.ID)
+ }
+
+ if group.Scheme != "KTS-OAEP-basic" {
+ return nil, fmt.Errorf("unsupported scheme %q in test group %d", group.Scheme, group.ID)
+ }
+
+ if group.KeyGen != "rsakpg1-basic" {
+ return nil, fmt.Errorf("unsupported key generation method %q in test group %d - only fixed public exponent (rsakpg1-basic) is supported", group.KeyGen, group.ID)
+ }
+
+ if group.OutputBits%8 != 0 {
+ return nil, fmt.Errorf("%d bit L in test group %d: fractional bytes not supported", group.OutputBits, group.ID)
+ }
+
+ if _, ok := k.hashAlgs[group.KTSConf.HashAlg]; !ok {
+ return nil, fmt.Errorf("test group %d specifies unsupported hash alg %q", group.ID, group.KTSConf.HashAlg)
+ }
+
+ var testResponses []ktsTestResponse
+ for _, test := range group.Tests {
+ test := test
+
+ var err error
+ switch group.Role {
+ case "initiator":
+ err = k.processInitiator(m, &testResponses, group.KTSConf.HashAlg, group.OutputBits, test)
+ case "responder":
+ err = k.processResponder(m, &testResponses, group.KTSConf.HashAlg, test)
+ default:
+ err = fmt.Errorf("unknown role %q", group.Role)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ m.Barrier(func() {
+ response.Tests = testResponses
+ ret = append(ret, response)
+ })
+ }
+
+ if err := m.Flush(); err != nil {
+ return nil, err
+ }
+
+ return ret, nil
+}
+
+func (k *kts) processInitiator(m Transactable, responses *[]ktsTestResponse, hashAlg string, outputBits uint64, test ktsTest) error {
+ outputBytes := uint32le(uint32(outputBits / 8))
+
+ nBytes, err := hex.DecodeString(test.ServerN)
+ if err != nil {
+ return fmt.Errorf("invalid ServerN: %v", err)
+ }
+ eBytes, err := hex.DecodeString(test.ServerE)
+ if err != nil {
+ return fmt.Errorf("invalid ServerE: %v", err)
+ }
+
+ cmd := fmt.Sprintf("KTS-IFC/%s/initiator", hashAlg)
+ args := [][]byte{outputBytes, nBytes, eBytes}
+
+ m.TransactAsync(cmd, 2, args, func(result [][]byte) error {
+ *responses = append(*responses,
+ ktsTestResponse{
+ ID: test.ID,
+ IutC: hex.EncodeToString(result[0]),
+ Dkm: hex.EncodeToString(result[1]),
+ })
+ return nil
+ })
+
+ return nil
+}
+
+func (k *kts) processResponder(m Transactable, responses *[]ktsTestResponse, hashAlg string, test ktsTest) error {
+ nBytes, err := hex.DecodeString(test.IutN)
+ if err != nil {
+ return fmt.Errorf("invalid IutN: %v", err)
+ }
+
+ eBytes, err := hex.DecodeString(test.IutE)
+ if err != nil {
+ return fmt.Errorf("invalid IutE: %v", err)
+ }
+
+ pBytes, err := hex.DecodeString(test.IutP)
+ if err != nil {
+ return fmt.Errorf("invalid IutP: %v", err)
+ }
+
+ qBytes, err := hex.DecodeString(test.IutQ)
+ if err != nil {
+ return fmt.Errorf("invalid IutQ: %v", err)
+ }
+
+ dBytes, err := hex.DecodeString(test.IutD)
+ if err != nil {
+ return fmt.Errorf("invalid IutD: %v", err)
+ }
+
+ cBytes, err := hex.DecodeString(test.ServerC)
+ if err != nil {
+ return fmt.Errorf("invalid ServerC: %v", err)
+ }
+
+ cmd := fmt.Sprintf("KTS-IFC/%s/responder", hashAlg)
+ args := [][]byte{nBytes, eBytes, pBytes, qBytes, dBytes, cBytes}
+
+ m.TransactAsync(cmd, 1, args, func(result [][]byte) error {
+ *responses = append(*responses,
+ ktsTestResponse{
+ ID: test.ID,
+ Dkm: hex.EncodeToString(result[0]),
+ })
+ return nil
+ })
+
+ return nil
+}
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
index 13f9e17..feae455 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -148,6 +148,7 @@
"ML-KEM": &mlkem{},
"SLH-DSA": &slhdsa{},
"kdf-components": &ssh{},
+ "KTS-IFC": &kts{map[string]bool{"SHA-1": true, "SHA2-224": true, "SHA2-256": true, "SHA2-384": true, "SHA2-512": true, "SHA2-512/224": true, "SHA2-512/256": true, "SHA3-224": true, "SHA3-256": true, "SHA3-384": true, "SHA3-512": true}},
}
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}