diff options
-rw-r--r-- | ChangeLog | 37 | ||||
-rw-r--r-- | ext/openssl/extconf.rb | 4 | ||||
-rw-r--r-- | ext/openssl/openssl_missing.c | 37 | ||||
-rw-r--r-- | ext/openssl/openssl_missing.h | 6 | ||||
-rw-r--r-- | ext/openssl/ossl_ssl.c | 118 | ||||
-rw-r--r-- | test/openssl/test_pair.rb | 87 | ||||
-rw-r--r-- | test/openssl/utils.rb | 2 |
7 files changed, 258 insertions, 33 deletions
@@ -1,3 +1,40 @@ +Mon May 30 18:29:28 2016 Kazuki Yamaguchi <k@rhe.jp> + + * ext/openssl/ossl_ssl.c (ossl_sslctx_s_alloc): Enable the automatic + curve selection for ECDH by calling SSL_CTX_set_ecdh_auto(). With + this a TLS server automatically selects a curve which both the client + and the server support to use in ECDH. This changes the default + behavior but users can still disable ECDH by excluding 'ECDH' cipher + suites from the cipher list (with SSLContext#ciphers=). This commit + also deprecate #tmp_ecdh_callback=. It was added in Ruby 2.3.0. It + wraps SSL_CTX_set_tmp_ecdh_callback() which will be removed in OpenSSL + 1.1.0. Its callback receives two values 'is_export' and 'keylength' + but both are completely useless for determining a curve to use in + ECDH. The automatic curve selection was introduced to replace this. + + (ossl_sslctx_setup): Deprecate SSLContext#tmp_ecdh_callback=. Emit a + warning if this is in use. + + (ossl_sslctx_set_ecdh_curves): Add SSLContext#ecdh_curves=. Wrap + SSL_CTX_set1_curves_list(). If it is not available, this falls back + to SSL_CTX_set_tmp_ecdh(). + + (Init_ossl_ssl): Define SSLContext#ecdh_curves=. + + * ext/openssl/extconf.rb: Check the existence of EC_curve_nist2nid(), + SSL_CTX_set1_curves_list(), SSL_CTX_set_ecdh_auto() and + SSL_CTX_set_tmp_ecdh_callback(). + + * ext/openssl/openssl_missing.[ch]: Implement EC_curve_nist2nid() if + missing. + + * test/openssl/test_pair.rb (test_ecdh_callback): Use + EnvUtil.suppress_warning to suppress deprecated warning. + + (test_ecdh_curves): Test that SSLContext#ecdh_curves= works. + + * test/openssl/utils.rb (start_server): Use SSLContext#ecdh_curves=. + Mon May 30 16:28:53 2016 Nobuyoshi Nakada <nobu@ruby-lang.org> * ext/socket/raddrinfo.c (host_str, port_str): use RSTRING_LEN diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index 14daae9786..eeeae448c0 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -98,13 +98,17 @@ have_func("SSL_CTX_set_next_proto_select_cb") have_macro("EVP_CTRL_GCM_GET_TAG", ['openssl/evp.h']) && $defs.push("-DHAVE_AUTHENTICATED_ENCRYPTION") # added in 1.0.2 +have_func("EC_curve_nist2nid") have_func("X509_REVOKED_dup") have_func("SSL_CTX_set_alpn_select_cb") +OpenSSL.check_func_or_macro("SSL_CTX_set1_curves_list", "openssl/ssl.h") +OpenSSL.check_func_or_macro("SSL_CTX_set_ecdh_auto", "openssl/ssl.h") OpenSSL.check_func_or_macro("SSL_get_server_tmp_key", "openssl/ssl.h") # added in 1.1.0 have_func("X509_STORE_get_ex_data") have_func("X509_STORE_set_ex_data") +OpenSSL.check_func_or_macro("SSL_CTX_set_tmp_ecdh_callback", "openssl/ssl.h") # removed Logging::message "=== Checking done. ===\n" diff --git a/ext/openssl/openssl_missing.c b/ext/openssl/openssl_missing.c index 7b00ae4d8c..796d8aa082 100644 --- a/ext/openssl/openssl_missing.c +++ b/ext/openssl/openssl_missing.c @@ -58,3 +58,40 @@ HMAC_CTX_copy(HMAC_CTX *out, HMAC_CTX *in) } #endif /* HAVE_HMAC_CTX_COPY */ #endif /* NO_HMAC */ + +/* added in 1.0.2 */ +#if !defined(OPENSSL_NO_EC) +#if !defined(HAVE_EC_CURVE_NIST2NID) +static struct { + const char *name; + int nid; +} nist_curves[] = { + {"B-163", NID_sect163r2}, + {"B-233", NID_sect233r1}, + {"B-283", NID_sect283r1}, + {"B-409", NID_sect409r1}, + {"B-571", NID_sect571r1}, + {"K-163", NID_sect163k1}, + {"K-233", NID_sect233k1}, + {"K-283", NID_sect283k1}, + {"K-409", NID_sect409k1}, + {"K-571", NID_sect571k1}, + {"P-192", NID_X9_62_prime192v1}, + {"P-224", NID_secp224r1}, + {"P-256", NID_X9_62_prime256v1}, + {"P-384", NID_secp384r1}, + {"P-521", NID_secp521r1} +}; + +int +EC_curve_nist2nid(const char *name) +{ + size_t i; + for (i = 0; i < (sizeof(nist_curves) / sizeof(nist_curves[0])); i++) { + if (!strcmp(nist_curves[i].name, name)) + return nist_curves[i].nid; + } + return NID_undef; +} +#endif +#endif diff --git a/ext/openssl/openssl_missing.h b/ext/openssl/openssl_missing.h index 0f82a180ef..b8f9d3d183 100644 --- a/ext/openssl/openssl_missing.h +++ b/ext/openssl/openssl_missing.h @@ -20,6 +20,12 @@ void HMAC_CTX_copy(HMAC_CTX *out, HMAC_CTX *in); #endif /* added in 1.0.2 */ +#if !defined(OPENSSL_NO_EC) +#if !defined(HAVE_EC_CURVE_NIST2NID) +int EC_curve_nist2nid(const char *); +#endif +#endif + #if !defined(HAVE_X509_REVOKED_DUP) # define X509_REVOKED_dup(rev) (X509_REVOKED *)ASN1_dup((i2d_of_void *)i2d_X509_REVOKED, \ (d2i_of_void *)d2i_X509_REVOKED, (char *)(rev)) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 282ed2915c..0f5313bd75 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -162,6 +162,18 @@ ossl_sslctx_s_alloc(VALUE klass) RTYPEDDATA_DATA(obj) = ctx; SSL_CTX_set_ex_data(ctx, ossl_ssl_ex_ptr_idx, (void*)obj); +#if defined(HAVE_SSL_CTX_SET_ECDH_AUTO) + /* We use SSL_CTX_set1_curves_list() to specify the curve used in ECDH. It + * allows to specify multiple curve names and OpenSSL will select + * automatically from them. In OpenSSL 1.0.2, the automatic selection has to + * be enabled explicitly. But OpenSSL 1.1.0 removed the knob and it is + * always enabled. To uniform the behavior, we enable the automatic + * selection also in 1.0.2. Users can still disable ECDH by removing ECDH + * cipher suites by SSLContext#ciphers=. */ + if (!SSL_CTX_set_ecdh_auto(ctx, 1)) + ossl_raise(eSSLError, "SSL_CTX_set_ecdh_auto"); +#endif + return obj; } @@ -711,10 +723,24 @@ ossl_sslctx_setup(VALUE self) #endif #if !defined(OPENSSL_NO_EC) - if (RTEST(ossl_sslctx_get_tmp_ecdh_cb(self))){ + /* We added SSLContext#tmp_ecdh_callback= in Ruby 2.3.0, + * but SSL_CTX_set_tmp_ecdh_callback() was removed in OpenSSL 1.1.0. */ + if (RTEST(ossl_sslctx_get_tmp_ecdh_cb(self))) { +# if defined(HAVE_SSL_CTX_SET_TMP_ECDH_CALLBACK) + rb_warn("#tmp_ecdh_callback= is deprecated; use #ecdh_curves= instead"); SSL_CTX_set_tmp_ecdh_callback(ctx, ossl_tmp_ecdh_callback); +# if defined(HAVE_SSL_CTX_SET_ECDH_AUTO) + /* tmp_ecdh_callback and ecdh_auto conflict; OpenSSL ignores + * tmp_ecdh_callback. So disable ecdh_auto. */ + if (!SSL_CTX_set_ecdh_auto(ctx, 0)) + ossl_raise(eSSLError, "SSL_CTX_set_ecdh_auto"); +# endif +# else + ossl_raise(eSSLError, "OpenSSL does not support tmp_ecdh_callback; " + "use #ecdh_curves= instead"); +# endif } -#endif +#endif /* OPENSSL_NO_EC */ val = ossl_sslctx_get_cert_store(self); if(!NIL_P(val)){ @@ -953,6 +979,87 @@ ossl_sslctx_set_ciphers(VALUE self, VALUE v) return v; } +#if !defined(OPENSSL_NO_EC) +/* + * call-seq: + * ctx.ecdh_curves = curve_list -> curve_list + * + * Sets the list of "supported elliptic curves" for this context. + * + * For a TLS client, the list is directly used in the Supported Elliptic Curves + * Extension. For a server, the list is used by OpenSSL to determine the set of + * shared curves. OpenSSL will pick the most appropriate one from it. + * + * Note that this works differently with old OpenSSL (<= 1.0.1). Only one curve + * can be set, and this has no effect for TLS clients. + * + * === Example + * ctx1 = OpenSSL::SSL::SSLContext.new + * ctx1.ecdh_curves = "X25519:P-256:P-224" + * svr = OpenSSL::SSL::SSLServer.new(tcp_svr, ctx1) + * Thread.new { svr.accept } + * + * ctx2 = OpenSSL::SSL::SSLContext.new + * ctx2.ecdh_curves = "P-256" + * cli = OpenSSL::SSL::SSLSocket.new(tcp_sock, ctx2) + * cli.connect + * + * p cli.tmp_key.group.curve_name + * # => "prime256v1" (is an alias for NIST P-256) + */ +static VALUE +ossl_sslctx_set_ecdh_curves(VALUE self, VALUE arg) +{ + SSL_CTX *ctx; + + rb_check_frozen(self); + GetSSLCTX(self, ctx); + StringValueCStr(arg); + +#if defined(HAVE_SSL_CTX_SET1_CURVES_LIST) + if (!SSL_CTX_set1_curves_list(ctx, RSTRING_PTR(arg))) + ossl_raise(eSSLError, NULL); +#else + /* OpenSSL does not have SSL_CTX_set1_curves_list()... Fallback to + * SSL_CTX_set_tmp_ecdh(). So only the first curve is used. */ + { + VALUE curve, splitted; + EC_KEY *ec; + int nid; + + splitted = rb_str_split(arg, ":"); + if (!RARRAY_LEN(splitted)) + ossl_raise(eSSLError, "invalid input format"); + curve = RARRAY_AREF(splitted, 0); + StringValueCStr(curve); + + /* SSL_CTX_set1_curves_list() accepts NIST names */ + nid = EC_curve_nist2nid(RSTRING_PTR(curve)); + if (nid == NID_undef) + nid = OBJ_txt2nid(RSTRING_PTR(curve)); + if (nid == NID_undef) + ossl_raise(eSSLError, "unknown curve name"); + + ec = EC_KEY_new_by_curve_name(nid); + if (!ec) + ossl_raise(eSSLError, NULL); + EC_KEY_set_asn1_flag(ec, OPENSSL_EC_NAMED_CURVE); + SSL_CTX_set_tmp_ecdh(ctx, ec); +# if defined(HAVE_SSL_CTX_SET_ECDH_AUTO) + /* tmp_ecdh and ecdh_auto conflict. tmp_ecdh is ignored when ecdh_auto + * is enabled. So disable ecdh_auto. */ + if (!SSL_CTX_set_ecdh_auto(ctx, 0)) + ossl_raise(eSSLError, "SSL_CTX_set_ecdh_auto"); +# endif + } +#endif + + return arg; +} +#else +#define ossl_sslctx_set_ecdh_curves rb_f_notimplement +#endif + /* * call-seq: * ctx.session_add(session) -> true | false @@ -2119,6 +2226,7 @@ Init_ossl_ssl(void) */ rb_attr(cSSLContext, rb_intern("client_cert_cb"), 1, 1, Qfalse); +#if defined(HAVE_SSL_CTX_SET_TMP_ECDH_CALLBACK) /* * A callback invoked when ECDH parameters are required. * @@ -2126,10 +2234,11 @@ Init_ossl_ssl(void) * flag indicating the use of an export cipher and the keylength * required. * - * The callback must return an OpenSSL::PKey::EC instance of the correct - * key length. + * The callback is deprecated. This does not work with recent versions of + * OpenSSL. Use OpenSSL::SSL::SSLContext#ecdh_curves= instead. */ rb_attr(cSSLContext, rb_intern("tmp_ecdh_callback"), 1, 1, Qfalse); +#endif /* * Sets the context in which a session can be reused. This allows @@ -2265,6 +2374,7 @@ Init_ossl_ssl(void) rb_define_method(cSSLContext, "ssl_version=", ossl_sslctx_set_ssl_version, 1); rb_define_method(cSSLContext, "ciphers", ossl_sslctx_get_ciphers, 0); rb_define_method(cSSLContext, "ciphers=", ossl_sslctx_set_ciphers, 1); + rb_define_method(cSSLContext, "ecdh_curves=", ossl_sslctx_set_ecdh_curves, 1); rb_define_method(cSSLContext, "setup", ossl_sslctx_setup, 0); diff --git a/test/openssl/test_pair.rb b/test/openssl/test_pair.rb index 38b33a4622..d9341ad123 100644 --- a/test/openssl/test_pair.rb +++ b/test/openssl/test_pair.rb @@ -372,41 +372,73 @@ module OpenSSL::TestPairM end def test_ecdh_callback - called = false - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.ciphers = "ECDH" - ctx2.tmp_ecdh_callback = ->(*args) { - called = true - OpenSSL::PKey::EC.new "prime256v1" - } + return unless OpenSSL::SSL::SSLContext.instance_methods.include?(:tmp_ecdh_callback) + EnvUtil.suppress_warning do # tmp_ecdh_callback is deprecated (2016-05) + begin + called = false + ctx2 = OpenSSL::SSL::SSLContext.new + ctx2.ciphers = "ECDH" + ctx2.tmp_ecdh_callback = ->(*args) { + called = true + OpenSSL::PKey::EC.new "prime256v1" + } + + sock1, sock2 = tcp_pair + + s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) + ctx1 = OpenSSL::SSL::SSLContext.new + ctx1.ciphers = "ECDH" + + s1 = OpenSSL::SSL::SSLSocket.new(sock1, ctx1) + th = Thread.new do + begin + rv = s1.connect_nonblock(exception: false) + case rv + when :wait_writable + IO.select(nil, [s1], nil, 5) + when :wait_readable + IO.select([s1], nil, nil, 5) + end + end until rv == s1 + end + + accepted = s2.accept + assert called, 'ecdh callback should be called' + rescue OpenSSL::SSL::SSLError => e + if e.message =~ /no cipher match/ + skip "ECDH cipher not supported." + else + raise e + end + ensure + th.join if th + s1.close if s1 + s2.close if s2 + sock1.close if sock1 + sock2.close if sock2 + end + end + end + def test_ecdh_curves sock1, sock2 = tcp_pair - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) ctx1 = OpenSSL::SSL::SSLContext.new ctx1.ciphers = "ECDH" - + ctx1.ecdh_curves = "P-384:P-224" s1 = OpenSSL::SSL::SSLSocket.new(sock1, ctx1) - th = Thread.new do - begin - rv = s1.connect_nonblock(exception: false) - case rv - when :wait_writable - IO.select(nil, [s1], nil, 5) - when :wait_readable - IO.select([s1], nil, nil, 5) - end - end until rv == s1 - end - accepted = s2.accept + ctx2 = OpenSSL::SSL::SSLContext.new + ctx2.ciphers = "ECDH" + ctx2.ecdh_curves = "P-256:P-384" + s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - assert called, 'ecdh callback should be called' - rescue OpenSSL::SSL::SSLError => e - if e.message =~ /no cipher match/ - skip "ECDH cipher not supported." - else - raise e + th = Thread.new { s1.accept } + s2.connect + + assert s2.cipher[0].start_with?("AECDH"), "AECDH should be used" + if s2.respond_to?(:tmp_key) + assert_equal "secp384r1", s2.tmp_key.group.curve_name end ensure th.join if th @@ -414,7 +446,6 @@ module OpenSSL::TestPairM s2.close if s2 sock1.close if sock1 sock2.close if sock2 - accepted.close if accepted.respond_to?(:close) end def test_connect_accept_nonblock_no_exception diff --git a/test/openssl/utils.rb b/test/openssl/utils.rb index 450250169b..9619a00b12 100644 --- a/test/openssl/utils.rb +++ b/test/openssl/utils.rb @@ -281,7 +281,7 @@ AQjjxMXhwULlmuR/K+WwlaZPiLIBYalLAZQ7ZbOPeVkJ8ePao0eLAgEC ctx.cert = @svr_cert ctx.key = @svr_key ctx.tmp_dh_callback = proc { OpenSSL::TestUtils::TEST_KEY_DH1024 } - ctx.tmp_ecdh_callback = proc { OpenSSL::TestUtils::TEST_KEY_EC_P256V1 } + ctx.ecdh_curves = "P-256" ctx.verify_mode = verify_mode ctx_proc.call(ctx) if ctx_proc |