diff --git a/ext/openssl/ossl_pkey_ec.c b/ext/openssl/ossl_pkey_ec.c index deca4f43b..fd064e893 100644 --- a/ext/openssl/ossl_pkey_ec.c +++ b/ext/openssl/ossl_pkey_ec.c @@ -57,6 +57,7 @@ static ID id_i_group; static VALUE ec_group_new(const EC_GROUP *group); static VALUE ec_point_new(const EC_POINT *point, const EC_GROUP *group); +static VALUE ossl_ec_group_gen_ecdsa_params(VALUE self, VALUE k); /* * Creates a new EC_KEY on the EC group obj. arg can be an EC::Group or a String @@ -485,24 +486,54 @@ static VALUE ossl_ec_key_check_key(VALUE self) /* * call-seq: * key.dsa_sign_asn1(data) => String + * key.dsa_sign_asn1(data, k) => String + * key.dsa_sign_asn1(data, inverse_k, r) => String * - * See the OpenSSL documentation for ECDSA_sign() + * See the OpenSSL documentation for ECDSA_sign() and ECDSA_sign_ex() */ -static VALUE ossl_ec_key_dsa_sign_asn1(VALUE self, VALUE data) +static VALUE ossl_ec_key_dsa_sign_asn1(int argc, VALUE argv[], VALUE self) { EC_KEY *ec; + BIGNUM *invkBN, *rBN; unsigned int buf_len; - VALUE str; + VALUE str, group, params, r, inverse_k; GetEC(self, ec); - StringValue(data); + StringValue(argv[0]); if (EC_KEY_get0_private_key(ec) == NULL) ossl_raise(eECError, "Private EC key needed!"); str = rb_str_new(0, ECDSA_size(ec)); - if (ECDSA_sign(0, (unsigned char *) RSTRING_PTR(data), RSTRING_LENINT(data), (unsigned char *) RSTRING_PTR(str), &buf_len, ec) != 1) - ossl_raise(eECError, "ECDSA_sign"); + if (argc == 1) { + if (ECDSA_sign(0, (unsigned char *) RSTRING_PTR(argv[0]), RSTRING_LENINT(argv[0]), (unsigned char *) RSTRING_PTR(str), &buf_len, ec) != 1) + ossl_raise(eECError, "ECDSA_sign"); + } + else if (argc == 2 || argc == 3) { + if (argc == 2) { + group = ossl_ec_key_get_group(self); + params = ossl_ec_group_gen_ecdsa_params(group, argv[1]); + inverse_k = RARRAY_AREF(params, 0); + invkBN = GetBNPtr(inverse_k); + r = RARRAY_AREF(params, 1); + rBN = GetBNPtr(r); + } + else { + invkBN = GetBNPtr(argv[1]); + rBN = GetBNPtr(argv[2]); + } + + if (invkBN == NULL || rBN == NULL) { + rb_raise(eECError, "inverse_k and r must both be OpenSSL::BN"); + } + + if (ECDSA_sign_ex(0, (unsigned char *) RSTRING_PTR(argv[0]), RSTRING_LENINT(argv[0]), (unsigned char *) RSTRING_PTR(str), &buf_len, invkBN, rBN, ec) != 1) + ossl_raise(eECError, "ECDSA_sign"); + } + else { + rb_raise(eECError, "invalid arguments"); + } + rb_str_set_len(str, buf_len); return str; @@ -1521,6 +1552,97 @@ static VALUE ossl_ec_point_mul(int argc, VALUE *argv, VALUE self) return result; } + +/* + * Private method: creates inverse_k and r from k suitable for ECDSA + * + * call-seq: + * group.ecdsa_generate_signature_params(k) => [ inverse_k, r ] + * + * Params: + * k : a OpenSSL::BN in the range of 0 < k < order + * + * Return: two values in a decomposable array + * inverse_k : the multiplicative inverse of k + * r : the X value for the ephemeral point R + * + * This uses `k` as the ephemeral private key to generate point R. + * The return is an array with the multiplicative inverse of k known as + * `inverse_k` and the X value of point R known as `r`. These are used + * as parameters to the ECDSA signature operation where a random `k` is + * not desired such as rfc6979 + * + */ +static VALUE ossl_ec_group_gen_ecdsa_params(VALUE self, VALUE k) +{ + VALUE result, order, point_r, inverse_k, r, reduceR; + BIGNUM *kBN, *rBN, *invkBN, *orderBN, *reduceBN; + BN_CTX *bnCtx; + const EC_GROUP *ecGroup; + EC_POINT *pointR; + + if (NIL_P(k)) { + rb_raise(eECError, "value for k must not be nil"); + } + + kBN = GetBNPtr(k); + if (kBN == NULL) { + rb_raise(eECError, "value for k must be an OpenSSL::BN"); + } + + order = ossl_ec_group_get_order(self); + orderBN = GetBNPtr(order); + + if (BN_cmp(kBN, orderBN) == 1) { + rb_raise(eECError, "value for k must be less then group order"); + } + + GetECGroup(self, ecGroup); + point_r = ossl_ec_point_alloc(cEC_POINT); + point_r = ossl_ec_point_initialize(1, &self, point_r); + GetECPoint(point_r, pointR); + + if (pointR == NULL) { + rb_raise(eECError, "unable to allocate point R"); + } + + if (!EC_POINT_mul(ecGroup, pointR, kBN, NULL, NULL, NULL)) { + rb_raise(eECError, "unable to multiply generator by k to produce R"); + } + + r = ossl_bn_new(NULL); + rBN = GetBNPtr(r); + + if (!EC_POINT_get_affine_coordinates(ecGroup, pointR, rBN, NULL, NULL)) { + rb_raise(eECError, "unable to get coordinates for R"); + } + + reduceR = ossl_bn_new(NULL); + reduceBN = GetBNPtr(reduceR); + + bnCtx = BN_CTX_new(); + + if (!BN_nnmod(reduceBN, rBN, orderBN, bnCtx)) { + BN_CTX_free(bnCtx); + rb_raise(eECError, "unable to reduce r to order"); + } + + inverse_k = ossl_bn_new(NULL); + invkBN = GetBNPtr(inverse_k); + if (!BN_mod_inverse(invkBN, kBN, orderBN, bnCtx)) { + BN_CTX_free(bnCtx); + rb_raise(eECError, "unable to inverse k"); + } + + BN_CTX_free(bnCtx); + + result = rb_ary_new_capa(2); + rb_ary_store(result, 0, inverse_k); + rb_ary_store(result, 1, reduceR); + + return result; +} + void Init_ossl_ec(void) { #undef rb_intern @@ -1594,7 +1716,7 @@ void Init_ossl_ec(void) rb_define_alias(cEC, "generate_key", "generate_key!"); rb_define_method(cEC, "check_key", ossl_ec_key_check_key, 0); - rb_define_method(cEC, "dsa_sign_asn1", ossl_ec_key_dsa_sign_asn1, 1); + rb_define_method(cEC, "dsa_sign_asn1", ossl_ec_key_dsa_sign_asn1, -1); rb_define_method(cEC, "dsa_verify_asn1", ossl_ec_key_dsa_verify_asn1, 2); /* do_sign/do_verify */ @@ -1638,6 +1760,7 @@ void Init_ossl_ec(void) rb_define_method(cEC_GROUP, "to_pem", ossl_ec_group_to_pem, 0); rb_define_method(cEC_GROUP, "to_der", ossl_ec_group_to_der, 0); rb_define_method(cEC_GROUP, "to_text", ossl_ec_group_to_text, 0); + rb_define_private_method(cEC_GROUP, "ecdsa_generate_signature_params", ossl_ec_group_gen_ecdsa_params, 1); rb_define_alloc_func(cEC_POINT, ossl_ec_point_alloc); diff --git a/test/openssl/fixtures/ecdsa/prime192v1.yml b/test/openssl/fixtures/ecdsa/prime192v1.yml new file mode 100644 index 000000000..af0f6f8ff --- /dev/null +++ b/test/openssl/fixtures/ecdsa/prime192v1.yml @@ -0,0 +1,70 @@ +--- +curve: prime192v1 +parameters: + q: FFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831 +key: + private: 6FAB034934E4C0FC9AE67F5B5659A9D7D1FEFD187EE09FD4 + public: + x: AC2C77F529F91689FEA0EA5EFEC7F210D8EEA0B9E047ED56 + y: 3BC723E57670BD4887EBC732C523063D0A7C957BC97C1C43 +examples: + - message: "sample" + hash: SHA1 + nonce: 37D7CA00D2C7B0E5E412AC03BD44BA837FDD5B28CD3B0021 + signature: + r: 98C6BD12B23EAF5E2A2045132086BE3EB8EBD62ABF6698FF + s: 57A22B07DEA9530F8DE9471B1DC6624472E8E2844BC25B64 + - message: "sample" + hash: SHA224 + nonce: 4381526B3FC1E7128F202E194505592F01D5FF4C5AF015D8 + signature: + r: A1F00DAD97AEEC91C95585F36200C65F3C01812AA60378F5 + s: E07EC1304C7C6C9DEBBE980B9692668F81D4DE7922A0F97A + - message: "sample" + hash: SHA256 + nonce: 32B1B6D7D42A05CB449065727A84804FB1A3E34D8F261496 + signature: + r: 4B0B8CE98A92866A2820E20AA6B75B56382E0F9BFD5ECB55 + s: CCDB006926EA9565CBADC840829D8C384E06DE1F1E381B85 + - message: "sample" + hash: SHA384 + nonce: 4730005C4FCB01834C063A7B6760096DBE284B8252EF4311 + signature: + r: DA63BF0B9ABCF948FBB1E9167F136145F7A20426DCC287D5 + s: C3AA2C960972BD7A2003A57E1C4C77F0578F8AE95E31EC5E + - message: "sample" + hash: SHA512 + nonce: A2AC7AB055E4F20692D49209544C203A7D1F2C0BFBC75DB1 + signature: + r: 4D60C5AB1996BD848343B31C00850205E2EA6922DAC2E4B8 + s: 3F6E837448F027A1BF4B34E796E32A811CBB4050908D8F67 + - message: "test" + hash: SHA1 + nonce: D9CF9C3D3297D3260773A1DA7418DB5537AB8DD93DE7FA25 + signature: + r: 0F2141A0EBBC44D2E1AF90A50EBCFCE5E197B3B7D4DE036D + s: EB18BC9E1F3D7387500CB99CF5F7C157070A8961E38700B7 + - message: "test" + hash: SHA224 + nonce: F5DC805F76EF851800700CCE82E7B98D8911B7D510059FBE + signature: + r: 6945A1C1D1B2206B8145548F633BB61CEF04891BAF26ED34 + s: B7FB7FDFC339C0B9BD61A9F5A8EAF9BE58FC5CBA2CB15293 + - message: "test" + hash: SHA256 + nonce: 5C4CE89CF56D9E7C77C8585339B006B97B5F0680B4306C6C + signature: + r: 3A718BD8B4926C3B52EE6BBE67EF79B18CB6EB62B1AD97AE + s: 5662E6848A4A19B1F1AE2F72ACD4B8BBE50F1EAC65D9124F + - message: "test" + hash: SHA384 + nonce: 5AFEFB5D3393261B828DB6C91FBC68C230727B030C975693 + signature: + r: B234B60B4DB75A733E19280A7A6034BD6B1EE88AF5332367 + s: 7994090B2D59BB782BE57E74A44C9A1C700413F8ABEFE77A + - message: "test" + hash: SHA512 + nonce: 0758753A5254759C7CFBAD2E2D9B0792EEE44136C9480527 + signature: + r: FE4F4AE86A58B6507946715934FE2D8FF9D95B6B098FE739 + s: 74CF5605C98FBA0E1EF34D4B5A1577A7DCF59457CAE52290 \ No newline at end of file diff --git a/test/openssl/test_pkey_ec.rb b/test/openssl/test_pkey_ec.rb index 80ae9ffdf..69c8d0792 100644 --- a/test/openssl/test_pkey_ec.rb +++ b/test/openssl/test_pkey_ec.rb @@ -385,6 +385,84 @@ def test_ec_point_mul assert_raise(TypeError) { point.mul(nil) } end + def test_ecdsa_generate_from_k + group = OpenSSL::PKey::EC::Group.new 'prime192v1' + + k = OpenSSL::BN.new '0758753A5254759C7CFBAD2E2D9B0792EEE44136C9480527', 16 + expected_r = OpenSSL::BN.new 'FE4F4AE86A58B6507946715934FE2D8FF9D95B6B098FE739', 16 + + inverse_k, r = group.send(:ecdsa_generate_signature_params, k) + + assert_equal(expected_r, r) + assert_not_equal(inverse_k, k) + assert_equal(0.to_bn, group.order.mod_mul(inverse_k, group.order)) + end + + def test_ecdsa_deterministic_k + group = OpenSSL::PKey::EC::Group.new 'prime192v1' + + private_key = OpenSSL::BN.new '6FAB034934E4C0FC9AE67F5B5659A9D7D1FEFD187EE09FD4', 16 + public_key_encoded = OpenSSL::BN.new '04AC2C77F529F91689FEA0EA5EFEC7F210D8EEA0B9E047ED56' \ + '3BC723E57670BD4887EBC732C523063D0A7C957BC97C1C43', 16 + public_key = OpenSSL::PKey::EC::Point.new group, public_key_encoded + key = OpenSSL::PKey::EC.new group + key.private_key = private_key + key.public_key = public_key + + hash = OpenSSL::Digest.new('SHA512').digest('test') + + k = OpenSSL::BN.new '0758753A5254759C7CFBAD2E2D9B0792EEE44136C9480527', 16 + expected_r = OpenSSL::BN.new 'FE4F4AE86A58B6507946715934FE2D8FF9D95B6B098FE739', 16 + expected_s = OpenSSL::BN.new '74CF5605C98FBA0E1EF34D4B5A1577A7DCF59457CAE52290', 16 + + inverse_k, r = group.send(:ecdsa_generate_signature_params, k) + + result = key.dsa_sign_asn1(hash, inverse_k, r) + assert_equal(true, key.dsa_verify_asn1(hash, result)) + + k_only_result = key.dsa_sign_asn1(hash, k) + assert_equal(true, key.dsa_verify_asn1(hash, k_only_result)) + + parsed_signature = OpenSSL::ASN1.decode(result) + assert_equal(expected_r, parsed_signature.value[0].value) + assert_equal(expected_s, parsed_signature.value[1].value) + end + + def test_ecdsa_raw_sign + group = OpenSSL::PKey::EC::Group.new 'prime192v1' + + private_key = OpenSSL::BN.new '6FAB034934E4C0FC9AE67F5B5659A9D7D1FEFD187EE09FD4', 16 + public_key_encoded = OpenSSL::BN.new '04AC2C77F529F91689FEA0EA5EFEC7F210D8EEA0B9E047ED56' \ + '3BC723E57670BD4887EBC732C523063D0A7C957BC97C1C43', 16 + public_key = OpenSSL::PKey::EC::Point.new group, public_key_encoded + key = OpenSSL::PKey::EC.new group + key.private_key = private_key + key.public_key = public_key + + hash = OpenSSL::Digest.new('SHA256').digest('test') + degree_bytes = group.degree / 8 + + k = OpenSSL::BN.new '5C4CE89CF56D9E7C77C8585339B006B97B5F0680B4306C6C', 16 + expected_r = OpenSSL::BN.new '3A718BD8B4926C3B52EE6BBE67EF79B18CB6EB62B1AD97AE', 16 + expected_s = OpenSSL::BN.new '5662E6848A4A19B1F1AE2F72ACD4B8BBE50F1EAC65D9124F', 16 + + inverse_k, r = group.send(:ecdsa_generate_signature_params, k) + assert_equal(expected_r, r) + + result = key.dsa_sign_asn1(hash, inverse_k, r) + assert_equal(true, key.dsa_verify_asn1(hash, result)) + + # TODO: pad to degree when hash is smaller + e = OpenSSL::BN.new hash[0..(degree_bytes - 1)], 2 + assert_equal(group.degree, e.num_bits) + s = inverse_k.mod_mul(e + (r * private_key), group.order) + assert_equal(expected_s, s) + + parsed_signature = OpenSSL::ASN1.decode(result) + assert_equal(expected_r, parsed_signature.value[0].value) + assert_equal(expected_s, parsed_signature.value[1].value) + end + # test Group: asn1_flag, point_conversion private diff --git a/test/openssl/test_pkey_ecdsa_conformance.rb b/test/openssl/test_pkey_ecdsa_conformance.rb new file mode 100644 index 000000000..3ee5a37a1 --- /dev/null +++ b/test/openssl/test_pkey_ecdsa_conformance.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require_relative 'utils' + +if defined?(OpenSSL) && defined?(OpenSSL::PKey::EC) + +class OpenSSL::TestECConformance < OpenSSL::PKeyTestCase + include OpenSSL::TestUtils::Fixtures + + def setup + super + + @curves = [ { data: read_yaml('ecdsa', 'prime192v1.yml') } ] + @curves.each do |curve| + curve[:group] = OpenSSL::PKey::EC::Group.new curve[:data]['curve'] + curve[:private_bn] = OpenSSL::BN.new(curve[:data]['key']['private'], 16) + curve[:public_bn] = OpenSSL::BN.new("04#{curve[:data]['key']['public']['x']}#{curve[:data]['key']['public']['y']}", 16) + curve[:key] = OpenSSL::PKey::EC.new(curve[:group]) + curve[:key].private_key = curve[:private_bn] + curve[:public_key] = OpenSSL::PKey::EC.new(curve[:group]) + curve[:public_key].public_key = OpenSSL::PKey::EC::Point.new(curve[:group], curve[:public_bn]) + end + end + + def test_examples + @curves.each do |curve| + key = curve[:key] + assert_not_nil(key.private_key) + + verify_key = curve[:public_key] + assert_not_nil(verify_key.public_key) + assert_true(verify_key.public_key.on_curve?) + + curve[:data]['examples'].each do |example| + hash = OpenSSL::Digest.new(example['hash']) + digest = hash.digest(example['message']) + + nonce = OpenSSL::BN.new example['nonce'], 16 + r_value = OpenSSL::BN.new example['signature']['r'], 16 + s_value = OpenSSL::BN.new example['signature']['s'], 16 + assert_false(r_value.negative?) + assert_false(s_value.negative?) + assert_false(nonce.negative?) + + sig_compute_nonce = key.dsa_sign_asn1(digest) + assert_true(verify_key.dsa_verify_asn1(digest, sig_compute_nonce)) + + sig_fixed_nonce = key.dsa_sign_asn1(digest, nonce) + # assert_true(verify_key.dsa_verify_asn1(digest, sig_fixed_nonce)) + + parsed_asn = OpenSSL::ASN1.decode(sig_fixed_nonce) + + assert_false(parsed_asn.value[0].value.negative?) + assert_false(parsed_asn.value[1].value.negative?) + + assert_equal(r_value, parsed_asn.value[0].value) + assert_equal(s_value, parsed_asn.value[1].value) + end + end + end +end + +end \ No newline at end of file diff --git a/test/openssl/utils.rb b/test/openssl/utils.rb index 8ee011641..97372f57e 100644 --- a/test/openssl/utils.rb +++ b/test/openssl/utils.rb @@ -33,6 +33,7 @@ require "tempfile" require "socket" require "envutil" +require 'yaml' if defined?(OpenSSL) @@ -44,10 +45,15 @@ def pkey(name) OpenSSL::PKey.read(read_file("pkey", name)) end - def read_file(category, name) + def read_file(category, name, ext = ".pem") @file_cache ||= {} - @file_cache[[category, name]] ||= - File.read(File.join(__dir__, "fixtures", category, name + ".pem")) + @file_cache[[category, name, ext]] ||= + File.read(File.join(__dir__, "fixtures", category, name + ext)) + end + + def read_yaml(category, name) + @file_cache ||= {} + @file_cache[[category, name]] ||= YAML.load_file(file_path(category, name)) end def file_path(category, name)