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/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