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 + } + } + } +}