acvp: add SP800-108 KDF support.
Based on a change from Dan Janni.
Change-Id: Ibe00e61cb43819ecad7c1376f8c013aca3667037
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/41964
Commit-Queue: Adam Langley <agl@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/util/fipstools/acvp/acvptool/subprocess/kdf.go b/util/fipstools/acvp/acvptool/subprocess/kdf.go
new file mode 100644
index 0000000..e8e16a0
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/subprocess/kdf.go
@@ -0,0 +1,132 @@
+// Copyright (c) 2020, 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 (
+ "bytes"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+)
+
+// The following structures reflect the JSON of ACVP KDF tests. See
+// https://usnistgov.github.io/ACVP/artifacts/acvp_sub_kdf108.html#rfc.section.3
+
+type kdfTestVectorSet struct {
+ Groups []kdfTestGroup `json:"testGroups"`
+}
+
+type kdfTestGroup struct {
+ ID uint64 `json:"tgId"`
+ // KDFMode can take the values "counter", "feedback", or
+ // "double pipeline iteration".
+ KDFMode string `json:"kdfMode"`
+ MACMode string `json:"macMode"`
+ CounterLocation string `json:"counterLocation"`
+ OutputBits uint32 `json:"keyOutLength"`
+ CounterBits uint32 `json:"counterLength"`
+ ZeroIV bool `json:"zeroLengthIv"`
+
+ Tests []struct {
+ ID uint64 `json:"tcId"`
+ Key string `json:"keyIn"`
+ Deferred bool `json:"deferred"`
+ }
+}
+
+type kdfTestGroupResponse struct {
+ ID uint64 `json:"tgId"`
+ Tests []kdfTestResponse `json:"tests"`
+}
+
+type kdfTestResponse struct {
+ ID uint64 `json:"tcId"`
+ KeyIn string `json:"keyIn,omitempty"`
+ FixedData string `json:"fixedData"`
+ KeyOut string `json:"keyOut"`
+}
+
+type kdfPrimitive struct{}
+
+func (k *kdfPrimitive) Process(vectorSet []byte, m Transactable) (interface{}, error) {
+ var parsed kdfTestVectorSet
+ if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+ return nil, err
+ }
+
+ var respGroups []kdfTestGroupResponse
+ for _, group := range parsed.Groups {
+ groupResp := kdfTestGroupResponse{ID: group.ID}
+
+ if group.OutputBits%8 != 0 {
+ return nil, fmt.Errorf("%d bit key in test group %d: fractional bytes not supported", group.OutputBits, group.ID)
+ }
+
+ if group.KDFMode != "counter" {
+ // feedback mode would need the IV to be handled.
+ // double-pipeline mode is not useful.
+ return nil, fmt.Errorf("KDF mode %q not supported", group.KDFMode)
+ }
+
+ switch group.CounterLocation {
+ case "after fixed data", "before fixed data":
+ break
+ default:
+ return nil, fmt.Errorf("Label location %q not supported", group.CounterLocation)
+ }
+
+ counterBits := uint32le(group.CounterBits)
+ outputBytes := uint32le(group.OutputBits / 8)
+
+ for _, test := range group.Tests {
+ testResp := kdfTestResponse{ID: test.ID}
+
+ var key []byte
+ if test.Deferred {
+ if len(test.Key) != 0 {
+ return nil, fmt.Errorf("key provided in deferred test case %d/%d", group.ID, test.ID)
+ }
+ } else {
+ var err error
+ if key, err = hex.DecodeString(test.Key); err != nil {
+ return nil, fmt.Errorf("failed to decode Key in test case %d/%d: %v", group.ID, test.ID, err)
+ }
+ }
+
+ // Make the call to the crypto module.
+ resp, err := m.Transact("KDF-counter", 3, outputBytes, []byte(group.MACMode), []byte(group.CounterLocation), key, counterBits)
+ if err != nil {
+ return nil, fmt.Errorf("wrapper KDF operation failed: %s", err)
+ }
+
+ // Parse results.
+ testResp.ID = test.ID
+ if test.Deferred {
+ testResp.KeyIn = hex.EncodeToString(resp[0])
+ }
+ testResp.FixedData = hex.EncodeToString(resp[1])
+ testResp.KeyOut = hex.EncodeToString(resp[2])
+
+ if !test.Deferred && !bytes.Equal(resp[0], key) {
+ return nil, fmt.Errorf("wrapper returned a different key for non-deferred KDF operation")
+ }
+
+ groupResp.Tests = append(groupResp.Tests, testResp)
+ }
+ respGroups = append(respGroups, groupResp)
+ }
+
+ return respGroups, nil
+}
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
index d22f3d5..b9a82ee 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -85,6 +85,7 @@
"HMAC-SHA2-512": &hmacPrimitive{"HMAC-SHA2-512", 64},
"ctrDRBG": &drbg{"ctrDRBG", map[string]bool{"AES-128": true, "AES-192": true, "AES-256": true}},
"hmacDRBG": &drbg{"hmacDRBG", map[string]bool{"SHA-1": true, "SHA2-224": true, "SHA2-256": true, "SHA2-384": true, "SHA2-512": true}},
+ "KDF": &kdfPrimitive{},
}
m.primitives["ECDSA"] = &ecdsa{"ECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives}
@@ -198,3 +199,9 @@
type primitive interface {
Process(vectorSet []byte, t Transactable) (interface{}, error)
}
+
+func uint32le(n uint32) []byte {
+ var ret [4]byte
+ binary.LittleEndian.PutUint32(ret[:], n)
+ return ret[:]
+}
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess_test.go b/util/fipstools/acvp/acvptool/subprocess/subprocess_test.go
index eb70f9f..09214c4 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess_test.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess_test.go
@@ -96,6 +96,73 @@
}]
}`)
+var validKDFJSON = []byte(`{
+ "vsId": 1564,
+ "algorithm": "counterMode",
+ "revision": "1.0",
+ "testGroups": [{
+ "tgId": 1,
+ "kdfMode": "counter",
+ "macMode": "CMAC-AES128",
+ "counterLocation": "after fixed data",
+ "keyOutLength": 1024,
+ "counterLength": 8,
+ "tests": [{
+ "tcId": 1,
+ "keyIn": "5DA38931E8D9174BC3279C8942D2DB82",
+ "deferred": false
+ },
+ {
+ "tcId": 2,
+ "keyIn": "58F5426A40E3D5D2C94F0F97EB30C739",
+ "deferred": false
+ }
+ ]
+ }]
+}`)
+
+var callsKDF = []fakeTransactCall{
+ fakeTransactCall{cmd: "KDF-counter", expectedNumResults: 3, args: [][]byte{
+ uint32le(128), // outputBytes
+ []byte("CMAC-AES128"), // macMode
+ []byte("after fixed data"), // counterLocation
+ fromHex("5DA38931E8D9174BC3279C8942D2DB82"), // keyIn
+ uint32le(8), // counterLength
+ }},
+ fakeTransactCall{cmd: "KDF-counter", expectedNumResults: 3, args: [][]byte{
+ uint32le(128), // outputBytes
+ []byte("CMAC-AES128"), // macMode
+ []byte("after fixed data"), // counterLocation
+ fromHex("58F5426A40E3D5D2C94F0F97EB30C739"), // keyIn
+ uint32le(8), // counterLength
+ }},
+}
+
+var invalidKDFJSON = []byte(`{
+ "vsId": 1564,
+ "algorithm": "counterMode",
+ "revision": "1.0",
+ "testGroups": [{
+ "tgId": 1,
+ "kdfMode": "counter",
+ "macMode": "CMAC-AES128",
+ "counterLocation": "after fixed data",
+ "keyOutLength": 1024,
+ "counterLength": 8,
+ "tests": [{
+ "tcId": 1,
+ "keyIn": "5DA38931E8D9174BC3279C8942D2DB82",
+ "deferred": false
+ },
+ {
+ "tcId": abc,
+ "keyIn": "58F5426A40E3D5D2C94F0F97EB30C739",
+ "deferred": false
+ }
+ ]
+ }]
+}`)
+
var validACVPAESECB = []byte(`{
"vsId" : 181726,
"algorithm" : "ACVP-AES-ECB",
@@ -293,6 +360,25 @@
expectedCalls: callsSHA2_256,
},
{
+ algo: "kdf",
+ p: &kdfPrimitive{},
+ validJSON: validKDFJSON,
+ invalidJSON: invalidKDFJSON,
+ expectedCalls: callsKDF,
+ results: []fakeTransactResult{
+ {bytes: [][]byte{
+ fromHex("5DA38931E8D9174BC3279C8942D2DB82"),
+ []byte("data1"),
+ []byte("keyOut1"),
+ }},
+ {bytes: [][]byte{
+ fromHex("58F5426A40E3D5D2C94F0F97EB30C739"),
+ []byte("data2"),
+ []byte("keyOut2"),
+ }},
+ },
+ },
+ {
algo: "ACVP-AES-ECB",
p: &blockCipher{"AES", 16, false},
validJSON: validACVPAESECB,
diff --git a/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
new file mode 100644
index 0000000..284b5d3
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
@@ -0,0 +1,201 @@
+// testmodulewrapper is a modulewrapper binary that works with acvptool and
+// implements the primitives that BoringSSL's modulewrapper doesn't, so that
+// we have something that can exercise all the code in avcptool.
+
+package main
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+)
+
+var handlers = map[string]func([][]byte) error{
+ "getConfig": getConfig,
+ "KDF-counter": kdfCounter,
+}
+
+func getConfig(args [][]byte) error {
+ if len(args) != 0 {
+ return fmt.Errorf("getConfig received %d args", len(args))
+ }
+
+ return reply([]byte(`[
+ {
+ "algorithm": "KDF",
+ "revision": "1.0",
+ "capabilities": [{
+ "kdfMode": "counter",
+ "macMode": [
+ "HMAC-SHA2-256"
+ ],
+ "supportedLengths": [{
+ "min": 8,
+ "max": 4096,
+ "increment": 8
+ }],
+ "fixedDataOrder": [
+ "before fixed data"
+ ],
+ "counterLength": [
+ 32
+ ]
+ }]
+ }
+]`))
+}
+
+func kdfCounter(args [][]byte) error {
+ if len(args) != 5 {
+ return fmt.Errorf("KDF received %d args", len(args))
+ }
+
+ outputBytes32, prf, counterLocation, key, counterBits32 := args[0], args[1], args[2], args[3], args[4]
+ outputBytes := binary.LittleEndian.Uint32(outputBytes32)
+ counterBits := binary.LittleEndian.Uint32(counterBits32)
+
+ if !bytes.Equal(prf, []byte("HMAC-SHA2-256")) {
+ return fmt.Errorf("KDF received unsupported PRF %q", string(prf))
+ }
+ if !bytes.Equal(counterLocation, []byte("before fixed data")) {
+ return fmt.Errorf("KDF received unsupported counter location %q", counterLocation)
+ }
+ if counterBits != 32 {
+ return fmt.Errorf("KDF received unsupported counter length %d", counterBits)
+ }
+
+ if len(key) == 0 {
+ key = make([]byte, 32)
+ rand.Reader.Read(key)
+ }
+
+ // See https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-108.pdf section 5.1
+ if outputBytes+31 < outputBytes {
+ return fmt.Errorf("KDF received excessive output length %d", outputBytes)
+ }
+
+ n := (outputBytes + 31) / 32
+ result := make([]byte, 0, 32*n)
+ mac := hmac.New(sha256.New, key)
+ var input [4 + 8]byte
+ var digest []byte
+ rand.Reader.Read(input[4:])
+ for i := uint32(1); i <= n; i++ {
+ mac.Reset()
+ binary.BigEndian.PutUint32(input[:4], i)
+ mac.Write(input[:])
+ digest = mac.Sum(digest[:0])
+ result = append(result, digest...)
+ }
+
+ return reply(key, input[4:], result[:outputBytes])
+}
+
+func reply(responses ...[]byte) error {
+ if len(responses) > maxArgs {
+ return fmt.Errorf("%d responses is too many", len(responses))
+ }
+
+ var lengths [4 * (1 + maxArgs)]byte
+ binary.LittleEndian.PutUint32(lengths[:4], uint32(len(responses)))
+ for i, response := range responses {
+ binary.LittleEndian.PutUint32(lengths[4*(i+1):4*(i+2)], uint32(len(response)))
+ }
+
+ lengthsLength := (1 + len(responses)) * 4
+ if n, err := os.Stdout.Write(lengths[:lengthsLength]); n != lengthsLength || err != nil {
+ return fmt.Errorf("write failed: %s", err)
+ }
+
+ for _, response := range responses {
+ if n, err := os.Stdout.Write(response); n != len(response) || err != nil {
+ return fmt.Errorf("write failed: %s", err)
+ }
+ }
+
+ return nil
+}
+
+const (
+ maxArgs = 8
+ maxArgLength = 1 << 20
+ maxNameLength = 30
+)
+
+func main() {
+ if err := do(); err != nil {
+ fmt.Fprintf(os.Stderr, "%s.\n", err)
+ os.Exit(1)
+ }
+}
+
+func do() error {
+ var nums [4 * (1 + maxArgs)]byte
+ var argLengths [maxArgs]uint32
+ var args [maxArgs][]byte
+ var argsData []byte
+
+ for {
+ if _, err := io.ReadFull(os.Stdin, nums[:8]); err != nil {
+ return err
+ }
+
+ numArgs := binary.LittleEndian.Uint32(nums[:4])
+ if numArgs == 0 {
+ return errors.New("Invalid, zero-argument operation requested")
+ } else if numArgs > maxArgs {
+ return fmt.Errorf("Operation requested with %d args, but %d is the limit", numArgs, maxArgs)
+ }
+
+ if numArgs > 1 {
+ if _, err := io.ReadFull(os.Stdin, nums[8:4+4*numArgs]); err != nil {
+ return err
+ }
+ }
+
+ input := nums[4:]
+ var need uint64
+ for i := uint32(0); i < numArgs; i++ {
+ argLength := binary.LittleEndian.Uint32(input[:4])
+ if i == 0 && argLength > maxNameLength {
+ return fmt.Errorf("Operation with name of length %d exceeded limit of %d", argLength, maxNameLength)
+ } else if argLength > maxArgLength {
+ return fmt.Errorf("Operation with argument of length %d exceeded limit of %d", argLength, maxArgLength)
+ }
+ need += uint64(argLength)
+ argLengths[i] = argLength
+ input = input[4:]
+ }
+
+ if need > uint64(cap(argsData)) {
+ argsData = make([]byte, need)
+ } else {
+ argsData = argsData[:need]
+ }
+
+ if _, err := io.ReadFull(os.Stdin, argsData); err != nil {
+ return err
+ }
+
+ input = argsData
+ for i := uint32(0); i < numArgs; i++ {
+ args[i] = input[:argLengths[i]]
+ input = input[argLengths[i]:]
+ }
+
+ name := string(args[0])
+ if handler, ok := handlers[name]; !ok {
+ return fmt.Errorf("unknown operation %q", name)
+ } else {
+ if err := handler(args[1:numArgs]); err != nil {
+ return err
+ }
+ }
+ }
+}