From fda92cd640e36e2fd8ab455f4ca0c1a301c35011 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Tue, 29 Sep 2020 11:00:51 -0700 Subject: [PATCH] acvp: add AES-GCM support. Change-Id: I7636736752ac371fc8d86fbc6bf81ca797ac5092 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/43127 Reviewed-by: Adam Langley Reviewed-by: David Benjamin Commit-Queue: Adam Langley --- .../acvp/acvptool/subprocess/aead.go | 193 ++++++++++++++++++ .../acvp/acvptool/subprocess/subprocess.go | 1 + .../acvp/modulewrapper/modulewrapper.cc | 112 ++++++++++ 3 files changed, 306 insertions(+) create mode 100644 util/fipstools/acvp/acvptool/subprocess/aead.go diff --git a/util/fipstools/acvp/acvptool/subprocess/aead.go b/util/fipstools/acvp/acvptool/subprocess/aead.go new file mode 100644 index 000000000..0c38b8589 --- /dev/null +++ b/util/fipstools/acvp/acvptool/subprocess/aead.go @@ -0,0 +1,193 @@ +// 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 ( + "encoding/hex" + "encoding/json" + "fmt" +) + +// aead implements an ACVP algorithm by making requests to the subprocess +// to encrypt and decrypt with an AEAD. +type aead struct { + algo string +} + +type aeadVectorSet struct { + Groups []aeadTestGroup `json:"testGroups"` +} + +type aeadTestGroup struct { + ID uint64 `json:"tgId"` + Type string `json:"testType"` + Direction string `json:"direction"` + KeyBits int `json:"keyLen"` + TagBits int `json:"tagLen"` + Tests []struct { + ID uint64 `json:"tcId"` + PlaintextHex string `json:"pt"` + CiphertextHex string `json:"ct"` + IVHex string `json:"iv"` + KeyHex string `json:"key"` + AADHex string `json:"aad"` + TagHex string `json:"tag"` + } `json:"tests"` +} + +type aeadTestGroupResponse struct { + ID uint64 `json:"tgId"` + Tests []aeadTestResponse `json:"tests"` +} + +type aeadTestResponse struct { + ID uint64 `json:"tcId"` + CiphertextHex *string `json:"ct,omitempty"` + TagHex string `json:"tag,omitempty"` + PlaintextHex *string `json:"pt,omitempty"` + Passed *bool `json:"testPassed,omitempty"` +} + +func (a *aead) Process(vectorSet []byte, m Transactable) (interface{}, error) { + var parsed aeadVectorSet + if err := json.Unmarshal(vectorSet, &parsed); err != nil { + return nil, err + } + + var ret []aeadTestGroupResponse + // See draft-celi-acvp-symmetric.html#table-6. (NIST no longer publish HTML + // versions of the ACVP documents. You can find fragments in + // https://github.com/usnistgov/ACVP.) + for _, group := range parsed.Groups { + response := aeadTestGroupResponse{ + ID: group.ID, + } + + var encrypt bool + switch group.Direction { + case "encrypt": + encrypt = true + case "decrypt": + encrypt = false + default: + return nil, fmt.Errorf("test group %d has unknown direction %q", group.ID, group.Direction) + } + + op := a.algo + "/seal" + if !encrypt { + op = a.algo + "/open" + } + + if group.KeyBits%8 != 0 || group.KeyBits < 0 { + return nil, fmt.Errorf("test group %d contains non-byte-multiple key length %d", group.ID, group.KeyBits) + } + keyBytes := group.KeyBits / 8 + + if group.TagBits%8 != 0 || group.TagBits < 0 { + return nil, fmt.Errorf("test group %d contains non-byte-multiple tag length %d", group.ID, group.TagBits) + } + tagBytes := group.TagBits / 8 + + for _, test := range group.Tests { + if len(test.KeyHex) != keyBytes*2 { + return nil, fmt.Errorf("test case %d/%d contains key %q of length %d, but expected %d-bit key", group.ID, test.ID, test.KeyHex, len(test.KeyHex), group.KeyBits) + } + + key, err := hex.DecodeString(test.KeyHex) + if err != nil { + return nil, fmt.Errorf("failed to decode key in test case %d/%d: %s", group.ID, test.ID, err) + } + + nonce, err := hex.DecodeString(test.IVHex) + if err != nil { + return nil, fmt.Errorf("failed to decode nonce in test case %d/%d: %s", group.ID, test.ID, err) + } + + aad, err := hex.DecodeString(test.AADHex) + if err != nil { + return nil, fmt.Errorf("failed to decode aad in test case %d/%d: %s", group.ID, test.ID, err) + } + + var tag []byte + if !encrypt { + if tag, err = hex.DecodeString(test.TagHex); err != nil { + return nil, fmt.Errorf("failed to decode tag in test case %d/%d: %s", group.ID, test.ID, err) + } + if len(tag) != tagBytes { + return nil, fmt.Errorf("tag in test case %d/%d is %d bytes long, but should be %d", group.ID, test.ID, len(tag), tagBytes) + } + } else if len(test.TagHex) != 0 { + return nil, fmt.Errorf("test case %d/%d has unexpected tag input", group.ID, test.ID) + } + + var inputHex, otherHex string + if encrypt { + inputHex, otherHex = test.PlaintextHex, test.CiphertextHex + } else { + inputHex, otherHex = test.CiphertextHex, test.PlaintextHex + } + + if len(otherHex) != 0 { + return nil, fmt.Errorf("test case %d/%d has unexpected plain/ciphertext input", group.ID, test.ID) + } + + input, err := hex.DecodeString(inputHex) + if err != nil { + return nil, fmt.Errorf("failed to decode hex in test case %d/%d: %s", group.ID, test.ID, err) + } + + testResp := aeadTestResponse{ID: test.ID} + + if encrypt { + result, err := m.Transact(op, 1, uint32le(uint32(tagBytes)), key, input, nonce, aad) + if err != nil { + return nil, err + } + + if len(result[0]) < tagBytes { + return nil, fmt.Errorf("ciphertext from subprocess for test case %d/%d is shorter than the tag (%d vs %d)", group.ID, test.ID, len(result[0]), tagBytes) + } + + ciphertext := result[0][:len(result[0])-tagBytes] + ciphertextHex := hex.EncodeToString(ciphertext) + tag := result[0][len(result[0])-tagBytes:] + + testResp.CiphertextHex = &ciphertextHex + testResp.TagHex = hex.EncodeToString(tag) + } else { + result, err := m.Transact(op, 2, uint32le(uint32(tagBytes)), key, append(input, tag...), nonce, aad) + if err != nil { + return nil, err + } + + if len(result[0]) != 1 || (result[0][0]&0xfe) != 0 { + return nil, fmt.Errorf("invalid AEAD status result from subprocess") + } + passed := result[0][0] == 1 + testResp.Passed = &passed + if passed { + plaintextHex := hex.EncodeToString(result[1]) + testResp.PlaintextHex = &plaintextHex + } + } + + response.Tests = append(response.Tests, testResp) + } + + ret = append(ret, response) + } + + return ret, nil +} diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go index af757d545..a9fbdfc5a 100644 --- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go +++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go @@ -79,6 +79,7 @@ func NewWithIO(cmd *exec.Cmd, in io.WriteCloser, out io.ReadCloser) *Subprocess "ACVP-AES-ECB": &blockCipher{"AES", 16, true, false}, "ACVP-AES-CBC": &blockCipher{"AES-CBC", 16, true, true}, "ACVP-AES-CTR": &blockCipher{"AES-CTR", 16, false, true}, + "ACVP-AES-GCM": &aead{"AES-GCM"}, "HMAC-SHA-1": &hmacPrimitive{"HMAC-SHA-1", 20}, "HMAC-SHA2-224": &hmacPrimitive{"HMAC-SHA2-224", 28}, "HMAC-SHA2-256": &hmacPrimitive{"HMAC-SHA2-256", 32}, diff --git a/util/fipstools/acvp/modulewrapper/modulewrapper.cc b/util/fipstools/acvp/modulewrapper/modulewrapper.cc index 043d37553..2e9598bc4 100644 --- a/util/fipstools/acvp/modulewrapper/modulewrapper.cc +++ b/util/fipstools/acvp/modulewrapper/modulewrapper.cc @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -188,6 +189,21 @@ static bool GetConfig(const Span args[]) { "direction": ["encrypt", "decrypt"], "keyLen": [128, 192, 256] }, + { + "algorithm": "ACVP-AES-GCM", + "revision": "1.0", + "direction": ["encrypt", "decrypt"], + "keyLen": [128, 192, 256], + "payloadLen": [{ + "min": 0, "max": 256, "increment": 8 + }], + "aadLen": [{ + "min": 0, "max": 256, "increment": 8 + }], + "tagLen": [128], + "ivLen": [96], + "ivGen": "external" + }, { "algorithm": "HMAC-SHA-1", "revision": "1.0", @@ -411,6 +427,100 @@ static bool AES_CTR(const Span args[]) { return WriteReply(STDOUT_FILENO, Span(out)); } +static bool AESGCMSetup(EVP_AEAD_CTX *ctx, Span tag_len_span, + Span key) { + uint32_t tag_len_32; + if (tag_len_span.size() != sizeof(tag_len_32)) { + fprintf(stderr, "Tag size value is %u bytes, not an uint32_t\n", + static_cast(tag_len_span.size())); + return false; + } + memcpy(&tag_len_32, tag_len_span.data(), sizeof(tag_len_32)); + + const EVP_AEAD *aead; + switch (key.size()) { + case 16: + aead = EVP_aead_aes_128_gcm(); + break; + case 24: + aead = EVP_aead_aes_192_gcm(); + break; + case 32: + aead = EVP_aead_aes_256_gcm(); + break; + default: + fprintf(stderr, "Bad AES-GCM key length %u\n", + static_cast(key.size())); + return false; + } + + if (!EVP_AEAD_CTX_init(ctx, aead, key.data(), key.size(), tag_len_32, + nullptr)) { + fprintf(stderr, "Failed to setup AES-GCM with tag length %u\n", + static_cast(tag_len_32)); + return false; + } + + return true; +} + +static bool AESGCMSeal(const Span args[]) { + Span tag_len_span = args[0]; + Span key = args[1]; + Span plaintext = args[2]; + Span nonce = args[3]; + Span ad = args[4]; + + bssl::ScopedEVP_AEAD_CTX ctx; + if (!AESGCMSetup(ctx.get(), tag_len_span, key)) { + return false; + } + + if (EVP_AEAD_MAX_OVERHEAD + plaintext.size() < EVP_AEAD_MAX_OVERHEAD) { + return false; + } + std::vector out(EVP_AEAD_MAX_OVERHEAD + plaintext.size()); + + size_t out_len; + if (!EVP_AEAD_CTX_seal(ctx.get(), out.data(), &out_len, out.size(), + nonce.data(), nonce.size(), plaintext.data(), + plaintext.size(), ad.data(), ad.size())) { + return false; + } + + out.resize(out_len); + return WriteReply(STDOUT_FILENO, Span(out)); +} + +static bool AESGCMOpen(const Span args[]) { + Span tag_len_span = args[0]; + Span key = args[1]; + Span ciphertext = args[2]; + Span nonce = args[3]; + Span ad = args[4]; + + bssl::ScopedEVP_AEAD_CTX ctx; + if (!AESGCMSetup(ctx.get(), tag_len_span, key)) { + return false; + } + + std::vector out(ciphertext.size()); + size_t out_len; + uint8_t success[1] = {0}; + + if (!EVP_AEAD_CTX_open(ctx.get(), out.data(), &out_len, out.size(), + nonce.data(), nonce.size(), ciphertext.data(), + ciphertext.size(), ad.data(), ad.size())) { + return WriteReply(STDOUT_FILENO, Span(success), + Span()); + } + + out.resize(out_len); + success[0] = 1; + return WriteReply(STDOUT_FILENO, Span(success), + Span(out)); +} + template static bool HMAC(const Span args[]) { const EVP_MD *const md = HashFunc(); @@ -658,6 +768,8 @@ static constexpr struct { {"AES-CBC/decrypt", 3, AES_CBC}, {"AES-CTR/encrypt", 3, AES_CTR}, {"AES-CTR/decrypt", 3, AES_CTR}, + {"AES-GCM/seal", 5, AESGCMSeal}, + {"AES-GCM/open", 5, AESGCMOpen}, {"HMAC-SHA-1", 2, HMAC}, {"HMAC-SHA2-224", 2, HMAC}, {"HMAC-SHA2-256", 2, HMAC},