From 2725e238ef4ae0311fc76a2474d2b10f22f3b6ed Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Wed, 19 Oct 2022 18:26:03 +0300 Subject: [PATCH] OpenSSL::PKey.from_data for generating PKey objects from user data --- ext/openssl/ossl_pkey.c | 222 +++++++++++++++++++++++++++++++++++++ test/openssl/test_pkey.rb | 223 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 445 insertions(+) diff --git a/ext/openssl/ossl_pkey.c b/ext/openssl/ossl_pkey.c index 5cbc0d195..fe0fecc02 100644 --- a/ext/openssl/ossl_pkey.c +++ b/ext/openssl/ossl_pkey.c @@ -447,6 +447,225 @@ pkey_generate(int argc, VALUE *argv, VALUE self, int genparam) return ossl_pkey_wrap(gen_arg.pkey); } +#if OSSL_OPENSSL_PREREQ(3, 0, 0) +#include +#include + +struct pkey_from_data_alias { + const char *alias; + const char *param_name; +}; + +static const struct pkey_from_data_alias rsa_aliases[] = { + { "p", OSSL_PKEY_PARAM_RSA_FACTOR1 }, + { "q", OSSL_PKEY_PARAM_RSA_FACTOR2 }, + { "dmp1", OSSL_PKEY_PARAM_RSA_EXPONENT1 }, + { "dmq1", OSSL_PKEY_PARAM_RSA_EXPONENT2 }, + { "iqmp", OSSL_PKEY_PARAM_RSA_COEFFICIENT1 }, + { NULL, NULL } +}; + +/* Maps the parameter names used by DH#params and DSA#params to the standard + * OSSL_PARAM names */ +static const struct pkey_from_data_alias default_aliases[] = { + { "pub_key", OSSL_PKEY_PARAM_PUB_KEY }, + { "priv_key", OSSL_PKEY_PARAM_PRIV_KEY }, + { NULL, NULL } +}; + +struct pkey_from_data_arg { + VALUE options; + VALUE converted; /* list of [name, BN or String value, data type] */ + const OSSL_PARAM *settable_params; + const struct pkey_from_data_alias *aliases; +}; + +static int +pkey_from_data_convert_i(VALUE key, VALUE value, VALUE argp) +{ + const struct pkey_from_data_arg *arg = (const struct pkey_from_data_arg *)argp; + const struct pkey_from_data_alias *alias; + const OSSL_PARAM *p; + const char *key_ptr; + VALUE name, supported; + + if (NIL_P(value)) + return ST_CONTINUE; + + if (SYMBOL_P(key)) + key = rb_sym2str(key); + key_ptr = StringValueCStr(key); + name = key; + + for (alias = arg->aliases; alias->alias; alias++) { + if (strcmp(alias->alias, key_ptr) == 0) { + key_ptr = alias->param_name; + name = rb_str_new_cstr(alias->param_name); + break; + } + } + + for (p = arg->settable_params; p->key != NULL; p++) { + if (strcmp(p->key, key_ptr) != 0) + continue; + switch (p->data_type) { + case OSSL_PARAM_INTEGER: + case OSSL_PARAM_UNSIGNED_INTEGER: + /* GetBNPtr() replaces value with the converted OpenSSL::BN */ + GetBNPtr(value); + break; + case OSSL_PARAM_UTF8_STRING: + case OSSL_PARAM_OCTET_STRING: + StringValue(value); + break; + default: + ossl_raise(ePKeyError, "Unsupported parameter \"%s\"", key_ptr); + } + rb_ary_push(arg->converted, + rb_ary_new3(3, name, value, INT2FIX(p->data_type))); + return ST_CONTINUE; + } + + supported = rb_ary_new(); + for (p = arg->settable_params; p->key != NULL; p++) + rb_ary_push(supported, rb_str_new_cstr(p->key)); + for (alias = arg->aliases; alias->alias; alias++) + rb_ary_push(supported, rb_str_new_cstr(alias->alias)); + ossl_raise(ePKeyError, "Invalid parameter \"%s\". Supported parameters: %"PRIsVALUE, + key_ptr, rb_ary_join(supported, rb_str_new_cstr(", "))); +} + +static VALUE +pkey_from_data_convert(VALUE argp) +{ + struct pkey_from_data_arg *arg = (struct pkey_from_data_arg *)argp; + + rb_hash_foreach(arg->options, pkey_from_data_convert_i, argp); + return Qnil; +} + +/* + * call-seq: + * OpenSSL::PKey.from_data(algo_name, parameters) -> pkey + * + * Creates a new key from the key components given in the Hash _parameters_ + * without validating them. _algo_name_ is a String that represents the + * algorithm, for example "RSA", "DSA", "DH" or "EC". + * NOTE: Requires OpenSSL 3.0 or later. + * + * Integer and OpenSSL::BN values are converted to BIGNUM parameters, + * String values are passed as UTF-8 or octet string parameters, as expected + * by the algorithm. For the parameter names, see the OpenSSL documentation + * for EVP_PKEY_fromdata[https://www.openssl.org/docs/manmaster/man3/EVP_PKEY_fromdata.html]. + * + * == Example + * pkey = OpenSSL::PKey.from_data("RSA", n: 3161751493, e: 65537, d: 2064855961) + * pkey.private? #=> true + * pkey.n #=> # + */ +static VALUE +ossl_pkey_s_from_data(int argc, VALUE *argv, VALUE self) +{ + VALUE alg, options; + EVP_PKEY_CTX *ctx; + OSSL_PARAM_BLD *param_bld; + OSSL_PARAM *params; + EVP_PKEY *pkey = NULL; + struct pkey_from_data_arg arg = { 0 }; + int state; + long i; + + rb_scan_args(argc, argv, "2", &alg, &options); + Check_Type(options, T_HASH); + + ctx = EVP_PKEY_CTX_new_from_name(NULL, StringValueCStr(alg), NULL); + if (!ctx) + ossl_raise(ePKeyError, "EVP_PKEY_CTX_new_from_name"); + + arg.options = options; + arg.converted = rb_ary_new(); + arg.settable_params = EVP_PKEY_fromdata_settable(ctx, EVP_PKEY_KEYPAIR); + if (!arg.settable_params) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_fromdata_settable"); + } + arg.aliases = EVP_PKEY_CTX_is_a(ctx, "RSA") ? rsa_aliases : default_aliases; + + /* + * Convert all keys and values to BN or String first. After this point no + * Ruby object is allocated until EVP_PKEY_fromdata() completes, so GC + * cannot free or move the buffers referenced by param_bld. + */ + rb_protect(pkey_from_data_convert, (VALUE)&arg, &state); + if (state) { + EVP_PKEY_CTX_free(ctx); + rb_jump_tag(state); + } + + param_bld = OSSL_PARAM_BLD_new(); + if (!param_bld) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "OSSL_PARAM_BLD_new"); + } + + for (i = 0; i < RARRAY_LEN(arg.converted); i++) { + VALUE tuple = RARRAY_AREF(arg.converted, i); + VALUE name = RARRAY_AREF(tuple, 0); + VALUE value = RARRAY_AREF(tuple, 1); + int ok; + + switch (FIX2INT(RARRAY_AREF(tuple, 2))) { + case OSSL_PARAM_INTEGER: + case OSSL_PARAM_UNSIGNED_INTEGER: + ok = OSSL_PARAM_BLD_push_BN(param_bld, RSTRING_PTR(name), + GetBNPtr(value)); + break; + case OSSL_PARAM_UTF8_STRING: + ok = OSSL_PARAM_BLD_push_utf8_string(param_bld, RSTRING_PTR(name), + RSTRING_PTR(value), + RSTRING_LENINT(value)); + break; + default: /* OSSL_PARAM_OCTET_STRING */ + ok = OSSL_PARAM_BLD_push_octet_string(param_bld, RSTRING_PTR(name), + RSTRING_PTR(value), + RSTRING_LENINT(value)); + break; + } + if (!ok) { + OSSL_PARAM_BLD_free(param_bld); + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "OSSL_PARAM_BLD_push(%"PRIsVALUE")", name); + } + } + + params = OSSL_PARAM_BLD_to_param(param_bld); + OSSL_PARAM_BLD_free(param_bld); + if (!params) { + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "OSSL_PARAM_BLD_to_param"); + } + + if (EVP_PKEY_fromdata_init(ctx) <= 0) { + OSSL_PARAM_free(params); + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_fromdata_init"); + } + + if (EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params) <= 0) { + OSSL_PARAM_free(params); + EVP_PKEY_CTX_free(ctx); + ossl_raise(ePKeyError, "EVP_PKEY_fromdata"); + } + + OSSL_PARAM_free(params); + EVP_PKEY_CTX_free(ctx); + RB_GC_GUARD(arg.converted); + + return ossl_pkey_wrap(pkey); +} + +#endif + /* * call-seq: * OpenSSL::PKey.generate_parameters(algo_name [, options]) -> pkey @@ -1946,6 +2165,9 @@ Init_ossl_pkey(void) rb_define_module_function(mPKey, "read", ossl_pkey_new_from_data, -1); rb_define_module_function(mPKey, "generate_parameters", ossl_pkey_s_generate_parameters, -1); rb_define_module_function(mPKey, "generate_key", ossl_pkey_s_generate_key, -1); +#if OSSL_OPENSSL_PREREQ(3, 0, 0) + rb_define_module_function(mPKey, "from_data", ossl_pkey_s_from_data, -1); +#endif rb_define_module_function(mPKey, "new_raw_private_key", ossl_pkey_new_raw_private_key, 2); rb_define_module_function(mPKey, "new_raw_public_key", ossl_pkey_new_raw_public_key, 2); diff --git a/test/openssl/test_pkey.rb b/test/openssl/test_pkey.rb index d9503c4e7..100259066 100644 --- a/test/openssl/test_pkey.rb +++ b/test/openssl/test_pkey.rb @@ -394,4 +394,227 @@ def test_legacy_error_classes assert_same(OpenSSL::PKey::PKeyError, OpenSSL::PKey::ECError) assert_same(OpenSSL::PKey::PKeyError, OpenSSL::PKey::RSAError) end + + if openssl?(3, 0, 0) + def test_s_from_data_with_invalid_alg + assert_raise_with_message(OpenSSL::PKey::PKeyError, /EVP_PKEY_CTX_new_from_name/) do + OpenSSL::PKey.from_data("ASR", {}) + end + end + + def test_s_from_data_argument_errors + assert_raise(ArgumentError) { OpenSSL::PKey.from_data("RSA") } + assert_raise(TypeError) { OpenSSL::PKey.from_data("RSA", "not a hash") } + end + + def test_s_from_data_under_gc_stress + # The converted values must be protected from GC until + # EVP_PKEY_fromdata() has consumed them + new_key = begin + GC.stress = true + OpenSSL::PKey.from_data("RSA", "n" => 3161751493, "e" => 65537, "d" => 2064855961) + ensure + GC.stress = false + end + + assert_equal OpenSSL::BN.new(3161751493), new_key.n + assert_equal OpenSSL::BN.new(65537), new_key.e + assert_equal OpenSSL::BN.new(2064855961), new_key.d + end + + def test_s_from_data_rsa_with_n_e_and_d_given_as_integers + new_key = OpenSSL::PKey.from_data("RSA", "n" => 3161751493, + "e" => 65537, + "d" => 2064855961) + + assert_instance_of OpenSSL::PKey::RSA, new_key + assert_equal true, new_key.private? + assert_equal OpenSSL::BN.new(3161751493), new_key.n + assert_equal OpenSSL::BN.new(65537), new_key.e + assert_equal OpenSSL::BN.new(2064855961), new_key.d + end + + def test_s_from_data_rsa_with_n_e_and_d_given_as_symbols + new_key = OpenSSL::PKey.from_data("RSA", n: OpenSSL::BN.new(3161751493), + e: OpenSSL::BN.new(65537), + d: OpenSSL::BN.new(2064855961)) + + assert_instance_of OpenSSL::PKey::RSA, new_key + assert_equal true, new_key.private? + assert_equal OpenSSL::BN.new(3161751493), new_key.n + assert_equal OpenSSL::BN.new(65537), new_key.e + assert_equal OpenSSL::BN.new(2064855961), new_key.d + end + + def test_s_from_data_rsa_with_n_and_e_given + new_key = OpenSSL::PKey.from_data("RSA", "n" => OpenSSL::BN.new(3161751493), + "e" => OpenSSL::BN.new(65537)) + + assert_instance_of OpenSSL::PKey::RSA, new_key + assert_equal false, new_key.private? + assert_equal OpenSSL::BN.new(3161751493), new_key.n + assert_equal OpenSSL::BN.new(65537), new_key.e + assert_nil new_key.d + end + + def test_s_from_data_rsa_with_openssl_internal_names + source = Fixtures.pkey("rsa2048") + new_key = OpenSSL::PKey.from_data("RSA", "n" => source.n, + "e" => source.e, + "d" => source.d, + "rsa-factor1" => source.p, + "rsa-factor2" => source.q, + "rsa-exponent1" => source.dmp1, + "rsa-exponent2" => source.dmq1, + "rsa-coefficient1" => source.iqmp) + + assert_equal source.n, new_key.n + assert_equal source.e, new_key.e + assert_equal source.d, new_key.d + assert_equal source.p, new_key.p + assert_equal source.q, new_key.q + assert_equal source.dmp1, new_key.dmp1 + assert_equal source.dmq1, new_key.dmq1 + assert_equal source.iqmp, new_key.iqmp + assert_equal source.to_pem, new_key.to_pem + end + + def test_s_from_data_rsa_with_simple_names + source = Fixtures.pkey("rsa2048") + new_key = OpenSSL::PKey.from_data("RSA", "n" => source.n, + "e" => source.e, + "d" => source.d, + "p" => source.p, + "q" => source.q, + "dmp1" => source.dmp1, + "dmq1" => source.dmq1, + "iqmp" => source.iqmp) + + assert_equal source.n, new_key.n + assert_equal source.e, new_key.e + assert_equal source.d, new_key.d + assert_equal source.p, new_key.p + assert_equal source.q, new_key.q + assert_equal source.dmp1, new_key.dmp1 + assert_equal source.dmq1, new_key.dmq1 + assert_equal source.iqmp, new_key.iqmp + assert_equal source.to_pem, new_key.to_pem + end + + def test_s_from_data_rsa_with_invalid_parameter + assert_raise_with_message(OpenSSL::PKey::PKeyError, /Invalid parameter "invalid"/) do + OpenSSL::PKey.from_data("RSA", "invalid" => 1234) + end + end + + def test_s_from_data_ec_pub_given_as_string + source = OpenSSL::PKey::EC.generate("prime256v1") + new_key = OpenSSL::PKey.from_data("EC", "group" => source.group.curve_name, + "pub" => source.public_key.to_bn.to_s(2)) + assert_instance_of OpenSSL::PKey::EC, new_key + assert_equal source.group.curve_name, new_key.group.curve_name + assert_equal source.public_key, new_key.public_key + assert_nil new_key.private_key + end + + def test_s_from_data_ec_priv_given_as_bn + source = OpenSSL::PKey::EC.generate("prime256v1") + new_key = OpenSSL::PKey.from_data("EC", "group" => source.group.curve_name, + "priv" => source.private_key.to_bn) + assert_instance_of OpenSSL::PKey::EC, new_key + assert_equal source.group.curve_name, new_key.group.curve_name + assert_equal source.private_key, new_key.private_key + assert_nil new_key.public_key + end + + def test_s_from_data_ec_priv_given_as_integer + source = OpenSSL::PKey::EC.generate("prime256v1") + new_key = OpenSSL::PKey.from_data("EC", "group" => source.group.curve_name, + "priv" => source.private_key.to_i) + assert_instance_of OpenSSL::PKey::EC, new_key + assert_equal source.group.curve_name, new_key.group.curve_name + assert_equal source.private_key, new_key.private_key + assert_nil new_key.public_key + end + + def test_s_from_data_ec_priv_and_pub_given_for_different_curves + [OpenSSL::PKey::EC.generate("prime256v1"), + OpenSSL::PKey::EC.generate("secp384r1"), + OpenSSL::PKey::EC.generate("secp521r1")].each do |source| + new_key = OpenSSL::PKey.from_data("EC", "group" => source.group.curve_name, + "pub" => source.public_key.to_bn.to_s(2), + "priv" => source.private_key.to_i) + assert_instance_of OpenSSL::PKey::EC, new_key + assert_equal source.group.curve_name, new_key.group.curve_name + assert_equal source.private_key, new_key.private_key + assert_equal source.public_key, new_key.public_key + end + end + + def test_s_from_data_ec_pub_given_as_integer + assert_raise_with_message(TypeError, "no implicit conversion of Integer into String") do + OpenSSL::PKey.from_data("EC", { "group" => "prime256v1", "pub" => 12345 }) + end + end + + def test_s_from_data_ec_with_invalid_parameter + assert_raise_with_message(OpenSSL::PKey::PKeyError, /Invalid parameter "invalid"/) do + OpenSSL::PKey.from_data("EC", "invalid" => 1234) + end + end + + def test_s_from_data_dsa_with_all_supported_parameters + source = Fixtures.pkey("dsa2048") + new_key = OpenSSL::PKey.from_data("DSA", "pub" => source.params["pub_key"], + "priv" => source.params["priv_key"], + "p" => source.params["p"], + "q" => source.params["q"], + "g" => source.params["g"]) + + assert_instance_of OpenSSL::PKey::DSA, new_key + assert_equal source.params, new_key.params + end + + def test_s_from_data_dsa_with_gem_specific_keys + source = Fixtures.pkey("dsa2048") + new_key = OpenSSL::PKey.from_data("DSA", source.params) + + assert_equal source.params, new_key.params + end + + def test_s_from_data_dsa_with_invalid_parameter + assert_raise_with_message(OpenSSL::PKey::PKeyError, /Invalid parameter "invalid"/) do + OpenSSL::PKey.from_data("DSA", "invalid" => 1234) + end + end + + def test_s_from_data_dh_with_all_supported_parameters + source = Fixtures.pkey("dh2048_ffdhe2048") + new_key = OpenSSL::PKey.from_data("DH", source.params) + + assert_instance_of OpenSSL::PKey::DH, new_key + assert_equal source.params, new_key.params + end + + def test_s_from_data_dh_with_invalid_parameter + assert_raise_with_message(OpenSSL::PKey::PKeyError, /Invalid parameter "invalid"/) do + OpenSSL::PKey.from_data("DH", "invalid" => 1234) + end + end + + def test_s_from_data_ed25519 + # Ed25519 is not FIPS-approved. + omit_on_fips + + pub_pem = <<~EOF + -----BEGIN PUBLIC KEY----- + MCowBQYDK2VwAyEA0I6olrZGYml7JGusuKJW9G7D0DZ9UormSady9kR7V4Q= + -----END PUBLIC KEY----- + EOF + + key = OpenSSL::PKey.from_data("ED25519", "pub" => "\xD0\x8E\xA8\x96\xB6Fbi{$k\xAC\xB8\xA2V\xF4n\xC3\xD06}R\x8A\xE6I\xA7r\xF6D{W\x84") + assert_instance_of OpenSSL::PKey::PKey, key + assert_equal pub_pem, key.public_to_pem + end + end end