utils/fipstools: add PBKDF ACVP support

This commit extends the acvptool subprocess package to support the
PBKDF test vectors and expected responses defined by
draft-celi-acvp-pbkdf:

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

Change-Id: I1e9ea6035227543502720ff74c03d21d1f512f85
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/72027
Commit-Queue: Adam Langley <agl@google.com>
Reviewed-by: 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 3e649bd..2b16f24 100644
--- a/util/fipstools/acvp/ACVP.md
+++ b/util/fipstools/acvp/ACVP.md
@@ -110,6 +110,7 @@
 | SHA2-512/224/MCT     | Initial seed¹             | Digest  |
 | SHA2-512/256/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 |
 
 ¹ 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/pbkdf.go b/util/fipstools/acvp/acvptool/subprocess/pbkdf.go
new file mode 100644
index 0000000..6f3819c
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/subprocess/pbkdf.go
@@ -0,0 +1,119 @@
+// 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/hex"
+	"encoding/json"
+	"fmt"
+)
+
+// The following structures reflect the JSON of ACVP PBKDF tests. See
+// https://pages.nist.gov/ACVP/draft-celi-acvp-pbkdf.html#name-test-vectors
+
+type pbkdfTestVectorSet struct {
+	Groups []pbkdfTestGroup `json:"testGroups"`
+	Mode   string           `json:"mode"`
+}
+
+type pbkdfTestGroup struct {
+	ID       uint64 `json:"tgId"`
+	Type     string `json:"testType"`
+	HmacAlgo string `json:"hmacAlg"`
+	Tests    []struct {
+		ID             uint64 `json:"tcId"`
+		KeyLen         uint32 `json:"keyLen,omitempty"`
+		Salt           string `json:"salt,omitempty"`
+		Password       string `json:"password,omitempty"`
+		IterationCount uint32 `json:"iterationCount,omitempty"`
+	} `json:"tests"`
+}
+
+type pbkdfTestGroupResponse struct {
+	ID    uint64              `json:"tgId"`
+	Tests []pbkdfTestResponse `json:"tests"`
+}
+
+type pbkdfTestResponse struct {
+	ID         uint64 `json:"tcId"`
+	DerivedKey string `json:"derivedKey,omitempty"`
+}
+
+// pbkdf implements an ACVP algorithm by making requests to the
+// subprocess to generate PBKDF2 keys.
+type pbkdf struct{}
+
+func (p *pbkdf) Process(vectorSet []byte, m Transactable) (any, error) {
+	var parsed pbkdfTestVectorSet
+	if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+		return nil, err
+	}
+
+	var ret []pbkdfTestGroupResponse
+	// See
+	// https://pages.nist.gov/ACVP/draft-celi-acvp-pbkdf.html#name-test-vectors
+	// for details about the tests.
+	for _, group := range parsed.Groups {
+		group := group
+
+		// "There is only one test type: functional tests."
+		// https://pages.nist.gov/ACVP/draft-celi-acvp-pbkdf.html#section-6.1
+		if group.Type != "AFT" {
+			return nil, fmt.Errorf("test type %q in test group %d not supported", group.Type, group.ID)
+		}
+
+		response := pbkdfTestGroupResponse{
+			ID: group.ID,
+		}
+
+		for _, test := range group.Tests {
+			test := test
+
+			if test.KeyLen < 8 {
+				return nil, fmt.Errorf("key length must be at least 8 bits in test case %d/%d", group.ID, test.ID)
+			}
+			keyLen := uint32le(test.KeyLen)
+
+			salt, err := hex.DecodeString(test.Salt)
+			if err != nil {
+				return nil, fmt.Errorf("failed to decode hex salt in test case %d/%d: %s", group.ID, test.ID, err)
+			}
+
+			if test.IterationCount < 1 {
+				return nil, fmt.Errorf("iteration count must be at least 1 in test case %d/%d", group.ID, test.ID)
+			}
+			iterationCount := uint32le(test.IterationCount)
+
+			msg := [][]byte{[]byte(group.HmacAlgo), keyLen, salt, []byte(test.Password), iterationCount}
+			m.TransactAsync("PBKDF", 1, msg, func(result [][]byte) error {
+				response.Tests = append(response.Tests, pbkdfTestResponse{
+					ID:         test.ID,
+					DerivedKey: hex.EncodeToString(result[0]),
+				})
+				return nil
+			})
+		}
+
+		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 82a8e88..a3f183b 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -141,6 +141,7 @@
 		"RSA":               &rsa{},
 		"KAS-ECC-SSC":       &kas{},
 		"KAS-FFC-SSC":       &kasDH{},
+		"PBKDF":             &pbkdf{},
 	}
 	m.primitives["ECDSA"] = &ecdsa{"ECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives}
 	m.primitives["EDDSA"] = &ecdsa{"ECDSA", map[string]bool{"ED-25519": true}, nil}
diff --git a/util/fipstools/acvp/acvptool/test/expected/PBKDF.bz2 b/util/fipstools/acvp/acvptool/test/expected/PBKDF.bz2
new file mode 100644
index 0000000..ff295b4
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/expected/PBKDF.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/tests.json b/util/fipstools/acvp/acvptool/test/tests.json
index 6804b23..26eba30 100644
--- a/util/fipstools/acvp/acvptool/test/tests.json
+++ b/util/fipstools/acvp/acvptool/test/tests.json
@@ -32,5 +32,6 @@
 {"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/TLS12.bz2", "Out": "expected/TLS12.bz2"},
-{"Wrapper": "modulewrapper", "In": "vectors/TLS13.bz2", "Out": "expected/TLS13.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/PBKDF.bz2 b/util/fipstools/acvp/acvptool/test/vectors/PBKDF.bz2
new file mode 100644
index 0000000..d34c2eb
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/vectors/PBKDF.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
index 4cf5069..c2147c2 100644
--- a/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
+++ b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
@@ -25,13 +25,17 @@
 	"crypto/hmac"
 	"crypto/rand"
 	"crypto/sha256"
+	"crypto/sha512"
 	"encoding/binary"
 	"errors"
 	"fmt"
+	"hash"
 	"io"
 	"os"
 
 	"golang.org/x/crypto/hkdf"
+	"golang.org/x/crypto/pbkdf2"
+	"golang.org/x/crypto/sha3"
 	"golang.org/x/crypto/xts"
 )
 
@@ -51,6 +55,7 @@
 	"hmacDRBG-pr/SHA2-256":     hmacDRBGPredictionResistance,
 	"AES-CBC-CS3/encrypt":      ctsEncrypt,
 	"AES-CBC-CS3/decrypt":      ctsDecrypt,
+	"PBKDF":                    pbkdf,
 }
 
 func flush(args [][]byte) error {
@@ -167,6 +172,43 @@
 		  128,
 		  256
 		]
+	}, {
+		"algorithm": "PBKDF",
+		"revision":"1.0",
+		"capabilities": [{
+			"iterationCount":[{
+				"min":1,
+				"max":10000,
+				"increment":1
+			}],
+			"keyLen": [{
+				"min":112,
+				"max":4096,
+				"increment":8
+			}],
+			"passwordLen":[{
+				"min":8,
+				"max":64,
+				"increment":1
+			}],
+			"saltLen":[{
+				"min":128,
+				"max":512,
+				"increment":8
+			}],
+			"hmacAlg":[
+				"SHA2-224",
+				"SHA2-256",
+				"SHA2-384",
+				"SHA2-512",
+				"SHA2-512/224",
+				"SHA2-512/256",
+				"SHA3-224",
+				"SHA3-256",
+				"SHA3-384",
+				"SHA3-512"
+			]
+		}]
 	}
 ]`)); err != nil {
 		return err
@@ -472,6 +514,46 @@
 	return reply(doCTSDecrypt(key, ciphertext, iv))
 }
 
+func pbkdf(args [][]byte) error {
+	if len(args) != 5 {
+		return fmt.Errorf("pbkdf received %d args, wanted 5", len(args))
+	}
+
+	hmacName := args[0]
+	var h func() hash.Hash
+	switch string(hmacName) {
+	case "SHA2-224":
+		h = sha256.New224
+	case "SHA2-256":
+		h = sha256.New
+	case "SHA2-384":
+		h = sha512.New384
+	case "SHA2-512":
+		h = sha512.New
+	case "SHA2-512/224":
+		h = sha512.New512_224
+	case "SHA2-512/256":
+		h = sha512.New512_256
+	case "SHA3-224":
+		h = sha3.New224
+	case "SHA3-256":
+		h = sha3.New256
+	case "SHA3-384":
+		h = sha3.New384
+	case "SHA3-512":
+		h = sha3.New512
+	default:
+		return fmt.Errorf("pbkdf unknown HMAC algorithm: %q", hmacName)
+	}
+	keyLen := binary.LittleEndian.Uint32(args[1]) / 8
+	salt, password := args[2], args[3]
+	iterationCount := binary.LittleEndian.Uint32(args[4])
+
+	derivedKey := pbkdf2.Key(password, salt, int(iterationCount), int(keyLen), h)
+
+	return reply(derivedKey)
+}
+
 const (
 	maxArgs       = 9
 	maxArgLength  = 1 << 20