[前][次][番号順一覧][スレッド一覧]

ruby-changes:24820

From: emboss <ko1@a...>
Date: Fri, 31 Aug 2012 18:47:50 +0900 (JST)
Subject: [ruby-changes:24820] emboss:r36871 (trunk): * ext/openssl/extconf.rb: Check existence of OPENSSL_NPN_NEGOTIATED.

emboss	2012-08-31 18:47:36 +0900 (Fri, 31 Aug 2012)

  New Revision: 36871

  http://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=rev&revision=36871

  Log:
    * 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]

  Modified files:
    trunk/ChangeLog
    trunk/NEWS
    trunk/ext/openssl/extconf.rb
    trunk/ext/openssl/ossl_ssl.c
    trunk/test/openssl/test_ssl.rb

Index: ChangeLog
===================================================================
--- ChangeLog	(revision 36870)
+++ ChangeLog	(revision 36871)
@@ -1,3 +1,16 @@
+Fri 31 Aug 2012 18:35:02 AM CEST  BOSSLET, Martin  <Martin.Bosslet@g...>
+
+	* 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  <knu@i...>
 
 	* lib/set.rb (Set#{each,reject!,select!}, SortedSet#each): Pass
Index: ext/openssl/ossl_ssl.c
===================================================================
--- ext/openssl/ossl_ssl.c	(revision 36870)
+++ ext/openssl/ossl_ssl.c	(revision 36871)
@@ -70,6 +70,9 @@
 #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 @@
     (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 @@
     } 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 @@
 #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 @@
 
     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 @@
 {
     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 @@
     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,13 +1580,8 @@
     X509 *cert = NULL;
     VALUE obj;
 
-    Data_Get_Struct(self, SSL, ssl);
+    ossl_ssl_data_get_struct(self, ssl);
 
-    if (!ssl){
-        rb_warning("SSL session is not started yet.");
-        return Qnil;
-    }
-
     cert = SSL_get_peer_certificate(ssl); /* Adds a ref => Safe to FREE. */
 
     if (!cert) {
@@ -1529,11 +1608,8 @@
     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 @@
 {
     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 @@
     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 @@
     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 @@
 {
     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 @@
 {
     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 @@
 /* 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 @@
 {
     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 @@
     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 @@
      *   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 @@
 
     rb_define_method(cSSLContext, "setup", ossl_sslctx_setup, 0);
 
-
     /*
      * No session caching for client or server
      */
@@ -2061,6 +2163,9 @@
     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))
 
Index: ext/openssl/extconf.rb
===================================================================
--- ext/openssl/extconf.rb	(revision 36870)
+++ ext/openssl/extconf.rb	(revision 36871)
@@ -108,6 +108,7 @@
 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
Index: NEWS
===================================================================
--- NEWS	(revision 36870)
+++ NEWS	(revision 36871)
@@ -171,6 +171,8 @@
     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
Index: test/openssl/test_ssl.rb
===================================================================
--- test/openssl/test_ssl.rb	(revision 36870)
+++ test/openssl/test_ssl.rb	(revision 36871)
@@ -411,7 +411,7 @@
   # 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 @@
     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 @@
     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 @@
     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 @@
     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 @@
     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 @@
     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 @@
     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 @@
     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
 
+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)

--
ML: ruby-changes@q...
Info: http://www.atdot.net/~ko1/quickml/

[前][次][番号順一覧][スレッド一覧]