From 25e6db3e3cb52c1a81e1e4a958a8d520a996812e Mon Sep 17 00:00:00 2001 From: emboss Date: Fri, 31 Aug 2012 09:47:36 +0000 Subject: * ext/openssl/extconf.rb: Check existence of OPENSSL_NPN_NEGOTIATED. ext/ossl_ssl.c: Support Next Protocol Negotiation. Protocols to be advertised by the server can be set in the SSLContext by using SSLContext#npn_protocols=, protocol selection on the client is supported by providing a selection callback with SSLContext#npn_select_cb. The protocol that was finally negotiated is available through SSL#npn_protocol. test/openssl/test_ssl.rb: Add tests for Next Protocol Negotiation. NEWS: add news about NPN support. [Feature #6503] [ruby-core:45272] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@36871 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ChangeLog | 13 +++ NEWS | 2 + ext/openssl/extconf.rb | 1 + ext/openssl/ossl_ssl.c | 223 ++++++++++++++++++++++++++++++++++------------- test/openssl/test_ssl.rb | 85 ++++++++++++++++-- 5 files changed, 256 insertions(+), 68 deletions(-) diff --git a/ChangeLog b/ChangeLog index 631bbbf838..9f31e661e1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,16 @@ +Fri 31 Aug 2012 18:35:02 AM CEST BOSSLET, Martin + + * ext/openssl/extconf.rb: Check existence of OPENSSL_NPN_NEGOTIATED. + ext/ossl_ssl.c: Support Next Protocol Negotiation. Protocols to be + advertised by the server can be set in the SSLContext by using + SSLContext#npn_protocols=, protocol selection on the client is + supported by providing a selection callback with + SSLContext#npn_select_cb. The protocol that was finally negotiated + is available through SSL#npn_protocol. + test/openssl/test_ssl.rb: Add tests for Next Protocol Negotiation. + NEWS: add news about NPN support. + [Feature #6503] [ruby-core:45272] + Fri Aug 31 17:38:43 2012 Akinori MUSHA * lib/set.rb (Set#{each,reject!,select!}, SortedSet#each): Pass diff --git a/NEWS b/NEWS index 7b05b9ef89..c335ccd91c 100644 --- a/NEWS +++ b/NEWS @@ -171,6 +171,8 @@ with all sufficient information, see the ChangeLog file. OpenSSL::PKey::EC therefore now enforce the same check when exporting a private key to PEM with a password - it has to be at least four characters long. + * SSL/TLS support for the Next Protocol Negotiation extension. Supported + with OpenSSL 1.0.1 and higher. * yaml * Syck has been removed. YAML now completely depends on libyaml being diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index a144dd2960..91caa529ad 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -108,6 +108,7 @@ have_func("TLSv1_1_client_method") have_func("TLSv1_2_method") have_func("TLSv1_2_server_method") have_func("TLSv1_2_client_method") +have_func("OPENSSL_NPN_NEGOTIATED", ['openssl/ssl.h']) unless have_func("SSL_set_tlsext_host_name", ['openssl/ssl.h']) have_macro("SSL_set_tlsext_host_name", ['openssl/ssl.h']) && $defs.push("-DHAVE_SSL_SET_TLSEXT_HOST_NAME") end diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 9233488b50..cc84ad1e51 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -70,6 +70,9 @@ static const char *ossl_sslctx_attrs[] = { #ifdef HAVE_SSL_SET_TLSEXT_HOST_NAME "servername_cb", #endif +#ifdef HAVE_OPENSSL_NPN_NEGOTIATED + "npn_protocols", +#endif }; #define ossl_ssl_get_io(o) rb_iv_get((o),"@io") @@ -560,6 +563,66 @@ ssl_renegotiation_cb(const SSL *ssl) (void) rb_funcall(cb, rb_intern("call"), 1, ssl_obj); } +#ifdef HAVE_OPENSSL_NPN_NEGOTIATED +static VALUE +ssl_npn_encode_protocol_i(VALUE cur, VALUE encoded) +{ + int len = RSTRING_LENINT(cur); + if (len < 1 || len > 255) + ossl_raise(eSSLError, "Advertised protocol must have length 1..255"); + /* Encode the length byte */ + rb_str_buf_cat(encoded, (const char *) &len, 1); + rb_str_buf_cat(encoded, RSTRING_PTR(cur), len); + return Qnil; +} + +static void +ssl_npn_encode_protocols(VALUE sslctx, VALUE protocols) +{ + VALUE encoded = rb_str_new2(""); + rb_iterate(rb_each, protocols, ssl_npn_encode_protocol_i, encoded); + StringValueCStr(encoded); + rb_iv_set(sslctx, "@_protocols", encoded); +} + +static int +ssl_npn_advertise_cb(SSL *ssl, const unsigned char **out, unsigned int *outlen, void *arg) +{ + VALUE sslctx_obj = (VALUE) arg; + VALUE protocols = rb_iv_get(sslctx_obj, "@_protocols"); + + *out = (const unsigned char *) RSTRING_PTR(protocols); + *outlen = RSTRING_LENINT(protocols); + + return SSL_TLSEXT_ERR_OK; +} + +static int +ssl_npn_select_cb(SSL *s, unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned int inlen, void *arg) +{ + int i = 0; + VALUE sslctx_obj, cb, protocols, selected; + + sslctx_obj = (VALUE) arg; + cb = rb_iv_get(sslctx_obj, "@npn_select_cb"); + protocols = rb_ary_new(); + + /* The format is len_1|proto_1|...|len_n|proto_n\0 */ + while (in[i]) { + VALUE protocol = rb_str_new((const char *) &in[i + 1], in[i]); + rb_ary_push(protocols, protocol); + i += in[i] + 1; + } + + selected = rb_funcall(cb, rb_intern("call"), 1, protocols); + StringValue(selected); + *out = (unsigned char *) StringValuePtr(selected); + *outlen = RSTRING_LENINT(selected); + + return SSL_TLSEXT_ERR_OK; +} +#endif + /* This function may serve as the entry point to support further * callbacks. */ static void @@ -690,6 +753,20 @@ ossl_sslctx_setup(VALUE self) } else { SSL_CTX_set_options(ctx, SSL_OP_ALL); } + +#ifdef HAVE_OPENSSL_NPN_NEGOTIATED + val = rb_iv_get(self, "@npn_protocols"); + if (!NIL_P(val)) { + ssl_npn_encode_protocols(self, val); + SSL_CTX_set_next_protos_advertised_cb(ctx, ssl_npn_advertise_cb, (void *) self); + OSSL_Debug("SSL NPN advertise callback added"); + } + if (RTEST(rb_iv_get(self, "@npn_select_cb"))) { + SSL_CTX_set_next_proto_select_cb(ctx, ssl_npn_select_cb, (void *) self); + OSSL_Debug("SSL NPN select callback added"); + } +#endif + rb_obj_freeze(self); val = ossl_sslctx_get_sess_id_ctx(self); @@ -1137,6 +1214,15 @@ ossl_ssl_setup(VALUE self) #define ssl_get_error(ssl, ret) SSL_get_error((ssl), (ret)) #endif +#define ossl_ssl_data_get_struct(v, ssl) \ +do { \ + Data_Get_Struct((v), SSL, (ssl)); \ + if (!(ssl)) { \ + rb_warning("SSL session is not started yet."); \ + return Qnil; \ + } \ +} while (0) + static void write_would_block(int nonblock) { @@ -1167,7 +1253,8 @@ ossl_start_ssl(VALUE self, int (*func)(), const char *funcname, int nonblock) rb_ivar_set(self, ID_callback_state, Qnil); - Data_Get_Struct(self, SSL, ssl); + ossl_ssl_data_get_struct(self, ssl); + GetOpenFile(ossl_ssl_get_io(self), fptr); for(;;){ ret = func(ssl); @@ -1445,7 +1532,8 @@ ossl_ssl_close(VALUE self) { SSL *ssl; - Data_Get_Struct(self, SSL, ssl); + ossl_ssl_data_get_struct(self, ssl); + ossl_ssl_shutdown(ssl); if (RTEST(ossl_ssl_get_sync_close(self))) rb_funcall(ossl_ssl_get_io(self), rb_intern("close"), 0); @@ -1465,11 +1553,7 @@ ossl_ssl_get_cert(VALUE self) SSL *ssl; X509 *cert = NULL; - Data_Get_Struct(self, SSL, ssl); - if (!ssl) { - rb_warning("SSL session is not started yet."); - return Qnil; - } + ossl_ssl_data_get_struct(self, ssl); /* * Is this OpenSSL bug? Should add a ref? @@ -1496,12 +1580,7 @@ ossl_ssl_get_peer_cert(VALUE self) X509 *cert = NULL; VALUE obj; - Data_Get_Struct(self, SSL, ssl); - - if (!ssl){ - rb_warning("SSL session is not started yet."); - return Qnil; - } + ossl_ssl_data_get_struct(self, ssl); cert = SSL_get_peer_certificate(ssl); /* Adds a ref => Safe to FREE. */ @@ -1529,11 +1608,8 @@ ossl_ssl_get_peer_cert_chain(VALUE self) VALUE ary; int i, num; - Data_Get_Struct(self, SSL, ssl); - if(!ssl){ - rb_warning("SSL session is not started yet."); - return Qnil; - } + ossl_ssl_data_get_struct(self, ssl); + chain = SSL_get_peer_cert_chain(ssl); if(!chain) return Qnil; num = sk_X509_num(chain); @@ -1558,11 +1634,8 @@ ossl_ssl_get_version(VALUE self) { SSL *ssl; - Data_Get_Struct(self, SSL, ssl); - if (!ssl) { - rb_warning("SSL session is not started yet."); - return Qnil; - } + ossl_ssl_data_get_struct(self, ssl); + return rb_str_new2(SSL_get_version(ssl)); } @@ -1578,11 +1651,8 @@ ossl_ssl_get_cipher(VALUE self) SSL *ssl; SSL_CIPHER *cipher; - Data_Get_Struct(self, SSL, ssl); - if (!ssl) { - rb_warning("SSL session is not started yet."); - return Qnil; - } + ossl_ssl_data_get_struct(self, ssl); + cipher = (SSL_CIPHER *)SSL_get_current_cipher(ssl); return ossl_ssl_cipher_to_ary(cipher); @@ -1600,11 +1670,8 @@ ossl_ssl_get_state(VALUE self) SSL *ssl; VALUE ret; - Data_Get_Struct(self, SSL, ssl); - if (!ssl) { - rb_warning("SSL session is not started yet."); - return Qnil; - } + ossl_ssl_data_get_struct(self, ssl); + ret = rb_str_new2(SSL_state_string(ssl)); if (ruby_verbose) { rb_str_cat2(ret, ": "); @@ -1624,11 +1691,7 @@ ossl_ssl_pending(VALUE self) { SSL *ssl; - Data_Get_Struct(self, SSL, ssl); - if (!ssl) { - rb_warning("SSL session is not started yet."); - return Qnil; - } + ossl_ssl_data_get_struct(self, ssl); return INT2NUM(SSL_pending(ssl)); } @@ -1644,11 +1707,7 @@ ossl_ssl_session_reused(VALUE self) { SSL *ssl; - Data_Get_Struct(self, SSL, ssl); - if (!ssl) { - rb_warning("SSL session is not started yet."); - return Qnil; - } + ossl_ssl_data_get_struct(self, ssl); switch(SSL_session_reused(ssl)) { case 1: return Qtrue; @@ -1674,11 +1733,7 @@ ossl_ssl_set_session(VALUE self, VALUE arg1) /* why is ossl_ssl_setup delayed? */ ossl_ssl_setup(self); - Data_Get_Struct(self, SSL, ssl); - if (!ssl) { - rb_warning("SSL session is not started yet."); - return Qnil; - } + ossl_ssl_data_get_struct(self, ssl); SafeGetSSLSession(arg1, sess); @@ -1702,11 +1757,7 @@ ossl_ssl_get_verify_result(VALUE self) { SSL *ssl; - Data_Get_Struct(self, SSL, ssl); - if (!ssl) { - rb_warning("SSL session is not started yet."); - return Qnil; - } + ossl_ssl_data_get_struct(self, ssl); return INT2FIX(SSL_get_verify_result(ssl)); } @@ -1728,16 +1779,37 @@ ossl_ssl_get_client_ca_list(VALUE self) SSL *ssl; STACK_OF(X509_NAME) *ca; - Data_Get_Struct(self, SSL, ssl); - if (!ssl) { - rb_warning("SSL session is not started yet."); - return Qnil; - } + ossl_ssl_data_get_struct(self, ssl); ca = SSL_get_client_CA_list(ssl); return ossl_x509name_sk2ary(ca); } +#ifdef HAVE_OPENSSL_NPN_NEGOTIATED +/* + * call-seq: + * ssl.npn_protocol => String + * + * Returns the protocol string that was finally selected by the client + * during the handshake. + */ +static VALUE +ossl_ssl_npn_protocol(VALUE self) +{ + SSL *ssl; + const unsigned char *out; + unsigned int outlen; + + ossl_ssl_data_get_struct(self, ssl); + + SSL_get0_next_proto_negotiated(ssl, &out, &outlen); + if (!outlen) + return Qnil; + else + return rb_str_new((const char *) out, outlen); +} +#endif + void Init_ossl_ssl() { @@ -1950,6 +2022,37 @@ Init_ossl_ssl() * end */ rb_attr(cSSLContext, rb_intern("renegotiation_cb"), 1, 1, Qfalse); +#ifdef HAVE_OPENSSL_NPN_NEGOTIATED + /* + * An Enumerable of Strings. Each String represents a protocol to be + * advertised as the list of supported protocols for Next Protocol + * Negotiation. Supported in OpenSSL 1.0.1 and higher. Has no effect + * on the client side. If not set explicitly, the NPN extension will + * not be sent by the server in the handshake. + * + * === Example + * + * ctx.npn_protocols = ["http/1.1", "spdy/2"] + */ + rb_attr(cSSLContext, rb_intern("npn_protocols"), 1, 1, Qfalse); + /* + * A callback invoked on the client side when the client needs to select + * a protocol from the list sent by the server. Supported in OpenSSL 1.0.1 + * and higher. The client MUST select a protocol of those advertised by + * the server. If none is acceptable, raising an error in the callback + * will cause the handshake to fail. Not setting this callback explicitly + * means not supporting the NPN extension on the client - any protocols + * advertised by the server will be ignored. + * + * === Example + * + * ctx.npn_select_cb = lambda do |protocols| + * #inspect the protocols and select one + * protocols.first + * end + */ + rb_attr(cSSLContext, rb_intern("npn_select_cb"), 1, 1, Qfalse); +#endif rb_define_alias(cSSLContext, "ssl_timeout", "timeout"); rb_define_alias(cSSLContext, "ssl_timeout=", "timeout="); @@ -1960,7 +2063,6 @@ Init_ossl_ssl() rb_define_method(cSSLContext, "setup", ossl_sslctx_setup, 0); - /* * No session caching for client or server */ @@ -2061,6 +2163,9 @@ Init_ossl_ssl() rb_define_method(cSSLSocket, "session=", ossl_ssl_set_session, 1); rb_define_method(cSSLSocket, "verify_result", ossl_ssl_get_verify_result, 0); rb_define_method(cSSLSocket, "client_ca", ossl_ssl_get_client_ca_list, 0); +#ifdef HAVE_OPENSSL_NPN_NEGOTIATED + rb_define_method(cSSLSocket, "npn_protocol", ossl_ssl_npn_protocol, 0); +#endif #define ossl_ssl_def_const(x) rb_define_const(mSSL, #x, INT2NUM(SSL_##x)) diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index b72e75ed41..74c7af52f8 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -411,7 +411,7 @@ class OpenSSL::TestSSL < OpenSSL::SSLTestCase # different OpenSSL versions react differently when being faced with a # SSL/TLS version that has been marked as forbidden, therefore either of # these may be raised - FORBIDDEN_PROTOCOL_ERRORS = [OpenSSL::SSL::SSLError, Errno::ECONNRESET] + HANDSHAKE_ERRORS = [OpenSSL::SSL::SSLError, Errno::ECONNRESET] if OpenSSL::SSL::SSLContext::METHODS.include? :TLSv1 @@ -420,7 +420,7 @@ if OpenSSL::SSL::SSLContext::METHODS.include? :TLSv1 start_server_version(:SSLv23, ctx_proc) { |server, port| ctx = OpenSSL::SSL::SSLContext.new ctx.ssl_version = :SSLv3 - assert_raise(*FORBIDDEN_PROTOCOL_ERRORS) { server_connect(port, ctx) } + assert_raise(*HANDSHAKE_ERRORS) { server_connect(port, ctx) } } end @@ -428,7 +428,7 @@ if OpenSSL::SSL::SSLContext::METHODS.include? :TLSv1 start_server_version(:SSLv3) { |server, port| ctx = OpenSSL::SSL::SSLContext.new ctx.options = OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_SSLv3 - assert_raise(*FORBIDDEN_PROTOCOL_ERRORS) { server_connect(port, ctx) } + assert_raise(*HANDSHAKE_ERRORS) { server_connect(port, ctx) } } end @@ -447,7 +447,7 @@ if OpenSSL::SSL::SSLContext::METHODS.include? :TLSv1_1 start_server_version(:SSLv23, ctx_proc) { |server, port| ctx = OpenSSL::SSL::SSLContext.new ctx.ssl_version = :TLSv1 - assert_raise(*FORBIDDEN_PROTOCOL_ERRORS) { server_connect(port, ctx) } + assert_raise(*HANDSHAKE_ERRORS) { server_connect(port, ctx) } } end @@ -455,7 +455,7 @@ if OpenSSL::SSL::SSLContext::METHODS.include? :TLSv1_1 start_server_version(:TLSv1) { |server, port| ctx = OpenSSL::SSL::SSLContext.new ctx.options = OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_TLSv1 - assert_raise(*FORBIDDEN_PROTOCOL_ERRORS) { server_connect(port, ctx) } + assert_raise(*HANDSHAKE_ERRORS) { server_connect(port, ctx) } } end @@ -474,7 +474,7 @@ if OpenSSL::SSL::SSLContext::METHODS.include? :TLSv1_2 start_server_version(:SSLv23, ctx_proc) { |server, port| ctx = OpenSSL::SSL::SSLContext.new ctx.ssl_version = :TLSv1_1 - assert_raise(*FORBIDDEN_PROTOCOL_ERRORS) { server_connect(port, ctx) } + assert_raise(*HANDSHAKE_ERRORS) { server_connect(port, ctx) } } end if defined?(OpenSSL::SSL::OP_NO_TLSv1_1) @@ -482,7 +482,7 @@ if OpenSSL::SSL::SSLContext::METHODS.include? :TLSv1_2 start_server_version(:TLSv1_1) { |server, port| ctx = OpenSSL::SSL::SSLContext.new ctx.options = OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_TLSv1_1 - assert_raise(*FORBIDDEN_PROTOCOL_ERRORS) { server_connect(port, ctx) } + assert_raise(*HANDSHAKE_ERRORS) { server_connect(port, ctx) } } end if defined?(OpenSSL::SSL::OP_NO_TLSv1_1) @@ -491,7 +491,7 @@ if OpenSSL::SSL::SSLContext::METHODS.include? :TLSv1_2 start_server_version(:SSLv23, ctx_proc) { |server, port| ctx = OpenSSL::SSL::SSLContext.new ctx.ssl_version = :TLSv1_2 - assert_raise(*FORBIDDEN_PROTOCOL_ERRORS) { server_connect(port, ctx) } + assert_raise(*HANDSHAKE_ERRORS) { server_connect(port, ctx) } } end if defined?(OpenSSL::SSL::OP_NO_TLSv1_2) @@ -499,7 +499,7 @@ if OpenSSL::SSL::SSLContext::METHODS.include? :TLSv1_2 start_server_version(:TLSv1_2) { |server, port| ctx = OpenSSL::SSL::SSLContext.new ctx.options = OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_TLSv1_2 - assert_raise(*FORBIDDEN_PROTOCOL_ERRORS) { server_connect(port, ctx) } + assert_raise(*HANDSHAKE_ERRORS) { server_connect(port, ctx) } } end if defined?(OpenSSL::SSL::OP_NO_TLSv1_2) @@ -516,6 +516,73 @@ end } end +if OpenSSL::OPENSSL_VERSION_NUMBER > 0x10001000 + + def test_npn_protocol_selection_ary + advertised = ["http/1.1", "spdy/2"] + ctx_proc = Proc.new { |ctx| ctx.npn_protocols = advertised } + start_server_version(:SSLv23, ctx_proc) { |server, port| + selector = lambda { |which| + ctx = OpenSSL::SSL::SSLContext.new + ctx.npn_select_cb = -> (protocols) { protocols.send(which) } + server_connect(port, ctx) { |ssl| + assert_equal(advertised.send(which), ssl.npn_protocol) + } + } + selector.call(:first) + selector.call(:last) + } + end + + def test_npn_protocol_selection_enum + advertised = Object.new + def advertised.each + yield "http/1.1" + yield "spdy/2" + end + ctx_proc = Proc.new { |ctx| ctx.npn_protocols = advertised } + start_server_version(:SSLv23, ctx_proc) { |server, port| + selector = lambda { |selected, which| + ctx = OpenSSL::SSL::SSLContext.new + ctx.npn_select_cb = -> (protocols) { protocols.to_a.send(which) } + server_connect(port, ctx) { |ssl| + assert_equal(selected, ssl.npn_protocol) + } + } + selector.call("http/1.1", :first) + selector.call("spdy/2", :last) + } + end + + def test_npn_protocol_selection_cancel + ctx_proc = Proc.new { |ctx| ctx.npn_protocols = ["http/1.1"] } + start_server_version(:SSLv23, ctx_proc) { |server, port| + ctx = OpenSSL::SSL::SSLContext.new + ctx.npn_select_cb = -> (protocols) { raise RuntimeError.new } + assert_raise(RuntimeError) { server_connect(port, ctx) } + } + end + + def test_npn_advertised_protocol_too_long + ctx_proc = Proc.new { |ctx| ctx.npn_protocols = ["a" * 256] } + start_server_version(:SSLv23, ctx_proc) { |server, port| + ctx = OpenSSL::SSL::SSLContext.new + ctx.npn_select_cb = -> (protocols) { protocols.first } + assert_raise(*HANDSHAKE_ERRORS) { server_connect(port, ctx) } + } + end + + def test_npn_selected_protocol_too_long + ctx_proc = Proc.new { |ctx| ctx.npn_protocols = ["http/1.1"] } + start_server_version(:SSLv23, ctx_proc) { |server, port| + ctx = OpenSSL::SSL::SSLContext.new + ctx.npn_select_cb = -> (protocols) { "a" * 256 } + assert_raise(*HANDSHAKE_ERRORS) { server_connect(port, ctx) } + } + end + +end + private def start_server_version(version, ctx_proc=nil, server_proc=nil, &blk) -- cgit v1.2.3