acvptool: add support for ECDSA
Change-Id: I0c643de16d5215a20bb21e8523efccd5555098eb
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/38764
Reviewed-by: Gurleen Grewal <gurleengrewal@google.com>
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/util/fipstools/acvp/acvptool/subprocess/ecdsa.go b/util/fipstools/acvp/acvptool/subprocess/ecdsa.go
new file mode 100644
index 0000000..5b5b1d1
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/subprocess/ecdsa.go
@@ -0,0 +1,228 @@
+// Copyright (c) 2019, 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 hash tests. See
+// https://usnistgov.github.io/ACVP/artifacts/acvp_sub_ecdsa.html#test_vectors
+
+type ecdsaTestVectorSet struct {
+ Groups []ecdsaTestGroup `json:"testGroups"`
+ Mode string `json:"mode"`
+}
+
+type ecdsaTestGroup struct {
+ ID uint64 `json:"tgId"`
+ Curve string `json:"curve"`
+ SecretGenerationMode string `json:"secretGenerationMode,omitempty"`
+ HashAlgo string `json:"hashAlg,omitEmpty"`
+ ComponentTest bool `json:"componentTest"`
+ Tests []struct {
+ ID uint64 `json:"tcId"`
+ QxHex string `json:"qx,omitempty"`
+ QyHex string `json:"qy,omitempty"`
+ RHex string `json:"r,omitempty"`
+ SHex string `json:"s,omitempty"`
+ MsgHex string `json:"message,omitempty"`
+ } `json:"tests"`
+}
+
+type ecdsaTestGroupResponse struct {
+ ID uint64 `json:"tgId"`
+ Tests []ecdsaTestResponse `json:"tests"`
+ QxHex string `json:"qx,omitempty"`
+ QyHex string `json:"qy,omitempty"`
+}
+
+type ecdsaTestResponse struct {
+ ID uint64 `json:"tcId"`
+ DHex string `json:"d,omitempty"`
+ QxHex string `json:"qx,omitempty"`
+ QyHex string `json:"qy,omitempty"`
+ RHex string `json:"r,omitempty"`
+ SHex string `json:"s,omitempty"`
+ Passed *bool `json:"testPassed,omitempty"` // using pointer so value is not omitted when it is false
+}
+
+// ecdsa implements an ACVP algorithm by making requests to the
+// subprocess to generate and verify ECDSA keys and signatures.
+type ecdsa struct {
+ // algo is the ACVP name for this algorithm and also the command name
+ // given to the subprocess to hash with this hash function.
+ algo string
+ curves map[string]bool // supported curve names
+ m *Subprocess
+}
+
+func (e *ecdsa) Process(vectorSet []byte) (interface{}, error) {
+ var parsed ecdsaTestVectorSet
+ if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+ return nil, err
+ }
+
+ var ret []ecdsaTestGroupResponse
+ // See
+ // https://usnistgov.github.io/ACVP/artifacts/draft-celi-acvp-sha-00.html#rfc.section.3
+ // for details about the tests.
+ for _, group := range parsed.Groups {
+ if _, ok := e.curves[group.Curve]; !ok {
+ return nil, fmt.Errorf("curve %q in test group %d not supported", group.Curve, group.ID)
+ }
+
+ response := ecdsaTestGroupResponse{
+ ID: group.ID,
+ }
+ var sigGenPrivateKey []byte
+
+ for _, test := range group.Tests {
+ var testResp ecdsaTestResponse
+
+ switch parsed.Mode {
+ case "keyGen":
+ if group.SecretGenerationMode != "testing candidates" {
+ return nil, fmt.Errorf("invalid secret generation mode in test group %d: %q", group.ID, group.SecretGenerationMode)
+ }
+ result, err := e.m.transact(e.algo+"/"+"keyGen", 3, []byte(group.Curve))
+ if err != nil {
+ return nil, fmt.Errorf("key generation failed for test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ testResp.DHex = hex.EncodeToString(result[0])
+ testResp.QxHex = hex.EncodeToString(result[1])
+ testResp.QyHex = hex.EncodeToString(result[2])
+
+ case "keyVer":
+ qx, err := hex.DecodeString(test.QxHex)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode qx in test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ qy, err := hex.DecodeString(test.QyHex)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode qy in test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ result, err := e.m.transact(e.algo+"/"+"keyVer", 1, []byte(group.Curve), qx, qy)
+ if err != nil {
+ return nil, fmt.Errorf("key verification failed for test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ // result[0] should be a single byte: zero if false, one if true
+ switch {
+ case bytes.Equal(result[0], []byte{00}):
+ f := false
+ testResp.Passed = &f
+ case bytes.Equal(result[0], []byte{01}):
+ t := true
+ testResp.Passed = &t
+ default:
+ return nil, fmt.Errorf("key verification returned unexpected result: %q", result[0])
+ }
+
+ case "sigGen":
+ p := e.m.primitives[group.HashAlgo]
+ h, ok := p.(*hashPrimitive)
+ if !ok {
+ return nil, fmt.Errorf("unsupported hash algorithm %q in test group %d", group.HashAlgo, group.ID)
+ }
+
+ if len(sigGenPrivateKey) == 0 {
+ // Ask the subprocess to generate a key for this test group.
+ result, err := e.m.transact(e.algo+"/"+"keyGen", 3, []byte(group.Curve))
+ if err != nil {
+ return nil, fmt.Errorf("key generation failed for test case %d/%d: %s", group.ID, test.ID, err)
+ }
+
+ sigGenPrivateKey = result[0]
+ response.QxHex = hex.EncodeToString(result[1])
+ response.QyHex = hex.EncodeToString(result[2])
+ }
+
+ msg, err := hex.DecodeString(test.MsgHex)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode message hex in test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ op := e.algo + "/" + "sigGen"
+ if group.ComponentTest {
+ if len(msg) != h.size {
+ return nil, fmt.Errorf("test case %d/%d contains message %q of length %d, but expected length %d", group.ID, test.ID, test.MsgHex, len(msg), h.size)
+ }
+ op += "/componentTest"
+ }
+ result, err := e.m.transact(op, 2, []byte(group.Curve), sigGenPrivateKey, []byte(group.HashAlgo), msg)
+ if err != nil {
+ return nil, fmt.Errorf("signature generation failed for test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ testResp.RHex = hex.EncodeToString(result[0])
+ testResp.SHex = hex.EncodeToString(result[1])
+
+ case "sigVer":
+ p := e.m.primitives[group.HashAlgo]
+ _, ok := p.(*hashPrimitive)
+ if !ok {
+ return nil, fmt.Errorf("unsupported hash algorithm %q in test group %d", group.HashAlgo, group.ID)
+ }
+
+ msg, err := hex.DecodeString(test.MsgHex)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode message hex in test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ qx, err := hex.DecodeString(test.QxHex)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode qx in test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ qy, err := hex.DecodeString(test.QyHex)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode qy in test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ r, err := hex.DecodeString(test.RHex)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode R in test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ s, err := hex.DecodeString(test.SHex)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode S in test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ result, err := e.m.transact(e.algo+"/"+"sigVer", 1, []byte(group.Curve), []byte(group.HashAlgo), msg, qx, qy, r, s)
+ if err != nil {
+ return nil, fmt.Errorf("signature verification failed for test case %d/%d: %s", group.ID, test.ID, err)
+ }
+ // result[0] should be a single byte: zero if false, one if true
+ switch {
+ case bytes.Equal(result[0], []byte{00}):
+ f := false
+ testResp.Passed = &f
+ case bytes.Equal(result[0], []byte{01}):
+ t := true
+ testResp.Passed = &t
+ default:
+ return nil, fmt.Errorf("signature verification returned unexpected result: %q", result[0])
+ }
+
+ default:
+ return nil, fmt.Errorf("invalid mode %q in ECDSA vector set", parsed.Mode)
+ }
+
+ testResp.ID = test.ID
+ 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 460041c..6f3e6ad 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -77,6 +77,7 @@
"HMAC-SHA2-512": &hmacPrimitive{"HMAC-SHA2-512", 64, m},
"ctrDRBG": &drbg{"ctrDRBG", map[string]bool{"AES-128": true, "AES-192": true, "AES-256": true}, m},
"hmacDRBG": &drbg{"hmacDRBG", map[string]bool{"SHA-1": true, "SHA2-224": true, "SHA2-256": true, "SHA2-384": true, "SHA2-512": true}, m},
+ "ECDSA": &ecdsa{"ECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m},
}
return m
diff --git a/util/fipstools/acvp/modulewrapper/modulewrapper.cc b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
index f373b9e..109e3b0 100644
--- a/util/fipstools/acvp/modulewrapper/modulewrapper.cc
+++ b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
@@ -23,8 +23,13 @@
#include <cstdarg>
#include <openssl/aes.h>
+#include <openssl/bn.h>
#include <openssl/digest.h>
+#include <openssl/ec.h>
+#include <openssl/ec_key.h>
+#include <openssl/ecdsa.h>
#include <openssl/hmac.h>
+#include <openssl/obj.h>
#include <openssl/sha.h>
#include <openssl/span.h>
@@ -231,8 +236,71 @@
],
"returnedBitsLen": 2048
}]
+ },
+ {
+ "algorithm": "ECDSA",
+ "mode": "keyGen",
+ "revision": "1.0",
+ "curve": [
+ "P-224",
+ "P-256",
+ "P-384",
+ "P-521"
+ ],
+ "secretGenerationMode": [
+ "testing candidates"
+ ]
+ },
+ {
+ "algorithm": "ECDSA",
+ "mode": "keyVer",
+ "revision": "1.0",
+ "curve": [
+ "P-224",
+ "P-256",
+ "P-384",
+ "P-521"
+ ]
+ },
+ {
+ "algorithm": "ECDSA",
+ "mode": "sigGen",
+ "revision": "1.0",
+ "capabilities": [{
+ "curve": [
+ "P-224",
+ "P-256",
+ "P-384",
+ "P-521"
+ ],
+ "hashAlg": [
+ "SHA2-224",
+ "SHA2-256",
+ "SHA2-384",
+ "SHA2-512"
+ ]
+ }]
+ },
+ {
+ "algorithm": "ECDSA",
+ "mode": "sigVer",
+ "revision": "1.0",
+ "capabilities": [{
+ "curve": [
+ "P-224",
+ "P-256",
+ "P-384",
+ "P-521"
+ ],
+ "hashAlg": [
+ "SHA2-224",
+ "SHA2-256",
+ "SHA2-384",
+ "SHA2-512"
+ ]
+ }]
}
- ])";
+ ])";
return WriteReply(
STDOUT_FILENO,
Span<const uint8_t>(reinterpret_cast<const uint8_t *>(kConfig),
@@ -333,6 +401,171 @@
return WriteReply(STDOUT_FILENO, Span<const uint8_t>(out));
}
+static bool StringEq(Span<const uint8_t> a, const char *b) {
+ const size_t len = strlen(b);
+ return a.size() == len && memcmp(a.data(), b, len) == 0;
+}
+
+static bssl::UniquePtr<EC_KEY> ECKeyFromName(Span<const uint8_t> name) {
+ int nid;
+ if (StringEq(name, "P-224")) {
+ nid = NID_secp224r1;
+ } else if (StringEq(name, "P-256")) {
+ nid = NID_X9_62_prime256v1;
+ } else if (StringEq(name, "P-384")) {
+ nid = NID_secp384r1;
+ } else if (StringEq(name, "P-521")) {
+ nid = NID_secp521r1;
+ } else {
+ return nullptr;
+ }
+
+ return bssl::UniquePtr<EC_KEY>(EC_KEY_new_by_curve_name(nid));
+}
+
+static std::vector<uint8_t> BIGNUMBytes(const BIGNUM *bn) {
+ const size_t len = BN_num_bytes(bn);
+ std::vector<uint8_t> ret(len);
+ BN_bn2bin(bn, ret.data());
+ return ret;
+}
+
+static std::pair<std::vector<uint8_t>, std::vector<uint8_t>> GetPublicKeyBytes(
+ const EC_KEY *key) {
+ bssl::UniquePtr<BIGNUM> x(BN_new());
+ bssl::UniquePtr<BIGNUM> y(BN_new());
+ if (!EC_POINT_get_affine_coordinates_GFp(EC_KEY_get0_group(key),
+ EC_KEY_get0_public_key(key), x.get(),
+ y.get(), /*ctx=*/nullptr)) {
+ abort();
+ }
+
+ std::vector<uint8_t> x_bytes = BIGNUMBytes(x.get());
+ std::vector<uint8_t> y_bytes = BIGNUMBytes(y.get());
+
+ return std::make_pair(std::move(x_bytes), std::move(y_bytes));
+}
+
+static bool ECDSAKeyGen(const Span<const uint8_t> args[]) {
+ bssl::UniquePtr<EC_KEY> key = ECKeyFromName(args[0]);
+ if (!key || !EC_KEY_generate_key_fips(key.get())) {
+ return false;
+ }
+
+ const auto pub_key = GetPublicKeyBytes(key.get());
+ std::vector<uint8_t> d_bytes =
+ BIGNUMBytes(EC_KEY_get0_private_key(key.get()));
+
+ return WriteReply(STDOUT_FILENO, Span<const uint8_t>(d_bytes),
+ Span<const uint8_t>(pub_key.first),
+ Span<const uint8_t>(pub_key.second));
+}
+
+static bssl::UniquePtr<BIGNUM> BytesToBIGNUM(Span<const uint8_t> bytes) {
+ bssl::UniquePtr<BIGNUM> bn(BN_new());
+ BN_bin2bn(bytes.data(), bytes.size(), bn.get());
+ return bn;
+}
+
+static bool ECDSAKeyVer(const Span<const uint8_t> args[]) {
+ bssl::UniquePtr<EC_KEY> key = ECKeyFromName(args[0]);
+ if (!key) {
+ return false;
+ }
+
+ bssl::UniquePtr<BIGNUM> x(BytesToBIGNUM(args[1]));
+ bssl::UniquePtr<BIGNUM> y(BytesToBIGNUM(args[2]));
+
+ bssl::UniquePtr<EC_POINT> point(EC_POINT_new(EC_KEY_get0_group(key.get())));
+ uint8_t reply[1];
+ if (!EC_POINT_set_affine_coordinates_GFp(EC_KEY_get0_group(key.get()),
+ point.get(), x.get(), y.get(),
+ /*ctx=*/nullptr) ||
+ !EC_KEY_set_public_key(key.get(), point.get()) ||
+ !EC_KEY_check_fips(key.get())) {
+ reply[0] = 0;
+ } else {
+ reply[0] = 1;
+ }
+
+ return WriteReply(STDOUT_FILENO, Span<const uint8_t>(reply));
+}
+
+static const EVP_MD *HashFromName(Span<const uint8_t> name) {
+ if (StringEq(name, "SHA2-224")) {
+ return EVP_sha224();
+ } else if (StringEq(name, "SHA2-256")) {
+ return EVP_sha256();
+ } else if (StringEq(name, "SHA2-384")) {
+ return EVP_sha384();
+ } else if (StringEq(name, "SHA2-512")) {
+ return EVP_sha512();
+ } else {
+ return nullptr;
+ }
+}
+
+static bool ECDSASigGen(const Span<const uint8_t> args[]) {
+ bssl::UniquePtr<EC_KEY> key = ECKeyFromName(args[0]);
+ bssl::UniquePtr<BIGNUM> d = BytesToBIGNUM(args[1]);
+ const EVP_MD *hash = HashFromName(args[2]);
+ uint8_t digest[EVP_MAX_MD_SIZE];
+ unsigned digest_len;
+ if (!key || !hash ||
+ !EVP_Digest(args[3].data(), args[3].size(), digest, &digest_len, hash,
+ /*impl=*/nullptr) ||
+ !EC_KEY_set_private_key(key.get(), d.get())) {
+ return false;
+ }
+
+ bssl::UniquePtr<ECDSA_SIG> sig(ECDSA_do_sign(digest, digest_len, key.get()));
+ if (!sig) {
+ return false;
+ }
+
+ std::vector<uint8_t> r_bytes(BIGNUMBytes(sig->r));
+ std::vector<uint8_t> s_bytes(BIGNUMBytes(sig->s));
+
+ return WriteReply(STDOUT_FILENO, Span<const uint8_t>(r_bytes),
+ Span<const uint8_t>(s_bytes));
+}
+
+static bool ECDSASigVer(const Span<const uint8_t> args[]) {
+ bssl::UniquePtr<EC_KEY> key = ECKeyFromName(args[0]);
+ const EVP_MD *hash = HashFromName(args[1]);
+ auto msg = args[2];
+ bssl::UniquePtr<BIGNUM> x(BytesToBIGNUM(args[3]));
+ bssl::UniquePtr<BIGNUM> y(BytesToBIGNUM(args[4]));
+ bssl::UniquePtr<BIGNUM> r(BytesToBIGNUM(args[5]));
+ bssl::UniquePtr<BIGNUM> s(BytesToBIGNUM(args[6]));
+ ECDSA_SIG sig;
+ sig.r = r.get();
+ sig.s = s.get();
+
+ uint8_t digest[EVP_MAX_MD_SIZE];
+ unsigned digest_len;
+ if (!key || !hash ||
+ !EVP_Digest(msg.data(), msg.size(), digest, &digest_len, hash,
+ /*impl=*/nullptr)) {
+ return false;
+ }
+
+ bssl::UniquePtr<EC_POINT> point(EC_POINT_new(EC_KEY_get0_group(key.get())));
+ uint8_t reply[1];
+ if (!EC_POINT_set_affine_coordinates_GFp(EC_KEY_get0_group(key.get()),
+ point.get(), x.get(), y.get(),
+ /*ctx=*/nullptr) ||
+ !EC_KEY_set_public_key(key.get(), point.get()) ||
+ !EC_KEY_check_fips(key.get()) ||
+ !ECDSA_do_verify(digest, digest_len, &sig, key.get())) {
+ reply[0] = 0;
+ } else {
+ reply[0] = 1;
+ }
+
+ return WriteReply(STDOUT_FILENO, Span<const uint8_t>(reply));
+}
+
static constexpr struct {
const char name[kMaxNameLength + 1];
uint8_t expected_args;
@@ -354,6 +587,10 @@
{"HMAC-SHA2-384", 2, HMAC<EVP_sha384>},
{"HMAC-SHA2-512", 2, HMAC<EVP_sha512>},
{"ctrDRBG/AES-256", 6, DRBG},
+ {"ECDSA/keyGen", 1, ECDSAKeyGen},
+ {"ECDSA/keyVer", 3, ECDSAKeyVer},
+ {"ECDSA/sigGen", 4, ECDSASigGen},
+ {"ECDSA/sigVer", 7, ECDSASigVer},
};
int main() {