Skip to content

Commit

Permalink
openssl: add SSLContext#ecdh_curves=
Browse files Browse the repository at this point in the history
* 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=.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@55214 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
  • Loading branch information
rhenium committed May 31, 2016
1 parent 998d667 commit fcb9b4a
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 33 deletions.
4 changes: 4 additions & 0 deletions ext/openssl/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,17 @@
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"

Expand Down
37 changes: 37 additions & 0 deletions ext/openssl/openssl_missing.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions ext/openssl/openssl_missing.h
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
118 changes: 114 additions & 4 deletions ext/openssl/ossl_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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)){
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2119,17 +2226,19 @@ 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.
*
* The callback is invoked with the Session for the key exchange, an
* 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
Expand Down Expand Up @@ -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);

Expand Down
87 changes: 59 additions & 28 deletions test/test_pair.rb
Original file line number Diff line number Diff line change
Expand Up @@ -372,49 +372,80 @@ def test_connect_without_setting_dh_callback
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
s1.close if s1
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
Expand Down
2 changes: 1 addition & 1 deletion test/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ def start_server(verify_mode, start_immediately, args = {}, &block)
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

Expand Down

0 comments on commit fcb9b4a

Please sign in to comment.