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