Project

General

Profile

Bug #11401

Updated by mkarnebeek (Michiel Karnebeek) about 9 years ago

See https://github.com/ruby/ruby/pull/964 

 ## Problem 

 When an initial SSL request is done, Net::HTTP stores the OpenSSL::SSL::Session object in @ssl_session. 

 When (after the http-keep-alive timeout has expired, or the connection was closed for some other reason) a second http request is made by Net::Http, resulting in calling Net::Http#connect (see the relevant pieces of code below, while reading the following points). 

 * #connect first calls `OpenSSL::SSL::SSLSocket#session=` at http.rb:924, which, in C code, eventually calls the C-method ossl_ssl_setup 
  * which executes the `if(!ssl) {` block at ossl_ssl.c:1205, but since @hostname has not been set yet, it will not execute SSL_set_tlsext_host_name. Also, because the OpenSSL::SSL::Session object does not contain a hostname, it is not known to OpenSSL at this point. 
 * it then calls `OpenSSL::SSL::SSLSocket#hostname=` at http.rb:927 which only sets @hostname on OpenSSL::SSL::SSLSocket 
 * and then it calls `OpenSSL::SSL::SSLSocket#connect` at http.rb:941 
  * which is doing the second call to the C-method ossl_ssl_setup, but since the `if(!ssl) {` already ran, it won't run again, and won't set the hostname from @hostname to SSL_set_tlsext_host_name. 

 This causes the second request to contains a SSL Session Ticket, but not a SNI header. This is easily verified by doing 2 calls in a ruby script, with a sleep 2.1 in between (http-keep-alive timeout is 2 seconds by default) and checking the second Client Hello message in Wireshark. 

 Normally this does not cause any issues, because the server looks at the SSL Session Ticket and knows for which virtual host it issued the ticket. So when a second request comes in with that ticket, it assumes the request should be handled by that vhost. 

 However, this breaks when the client (Ruby) thinks the session ticket is still valid (#10533 did some fixing), sends it to the server, but the server denies it. The server then starts to renegotiate the SSL session, but since the SNI header is missing, it won't know for which vhost, and sends the SSL certificate for the default vhost, which may not be the vhost it wants to connect to. The client (Ruby) then checks the certificate against the hostname it was connecting to, and finds out it doesn't match. 

 So, this only occurs on SSL session resumption (the second http request after http-keep-alive expired, or the connection was closed), when connecting to non-default vhosts which has a different certificate set than the default vhost, and when the client thinks the SSL Session Ticket is valid, but the server disagrees. 

 Why would the server deny the SSL session ticket? The client already checked if it was valid, right? Well, all kind of reasons: 

 * Server may have invalidated the ticket earlier than the client 
 * Server rebooted 
 * Time drift 
 * ... 
 * but mostly because the SSL termination for a specific hostname may be handled by multiple servers, which are not sharing their SSL session tickets (or sharing them in a delayed matter). 

 ## Solution 
 The solution is to move the call to `OpenSSL::SSL::SSLSocket#hostname=` before the call to `OpenSSL::SSL::SSLSocket#session=`, so the hostname gets set when ossl_ssl_set_session calls ossl_ssl_setup 

 ## Relevant code pieces 

 http.rb 

 ~~~ 
 868 	     def connect 
 ... 
 878 	       s = Timeout.timeout(@open_timeout, Net::OpenTimeout) { 
 ... 
 885 	       } 
 886 	       s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) 
 887 	       D "opened" 
 888 	       if use_ssl? 
 ... 
 897 	         @ssl_context = OpenSSL::SSL::SSLContext.new 
 898 	         @ssl_context.set_params(ssl_parameters) 
 899 	         D "starting SSL for #{conn_address}:#{conn_port}..." 
 900 	         s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)                <-- it always re-creates the OpenSSL::SSL::SSLSocket object 
 901 	         s.sync_close = true 
 902 	         D "SSL established" 
 903 	       end 
 904 	       @socket = BufferedIO.new(s) 
 ... 
 908 	       if use_ssl? 
 909 	         begin 
 910 	           if proxy? 
 ... 
 921 	           end 
 922 	           if @ssl_session and 
 923 	              Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout 
 924 	             s.session = @ssl_session if @ssl_session                   <-- (2nd request) this call already sets up the connection  
                                                                                  (calls the C-method ossl_ssl_setup) 
                                                                                  also, this does not set the hostname,  
                                                                                  as OpenSSL::SSL::Session does not contain a hostname 
 925 	           end 
 926 	           # Server Name Indication (SNI) RFC 3546 
 927 	           s.hostname = @address if s.respond_to? :hostname=            <-- Only sets the hostname to @hostname on OpenSSL::SSL::SSLSocket. 
                                                                                  It relies on ossl_ssl_setup to actually set it to openssl  
                                                                                  (SSL_set_tlsext_host_name at ossl_ssl.c:1221 
 928 	           if timeout = @open_timeout 
 929 	             while true 
 930 	               raise Net::OpenTimeout if timeout <= 0 
 931 	               start = Process.clock_gettime Process::CLOCK_MONOTONIC 
 932 	               # to_io is requied because SSLSocket doesn't have wait_readable yet 
 933 	               case s.connect_nonblock(exception: false) 
 934 	               when :wait_readable; s.to_io.wait_readable(timeout) 
 935 	               when :wait_writable; s.to_io.wait_writable(timeout) 
 936 	               else; break 
 937 	               end 
 938 	               timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start 
 939 	             end 
 940 	           else 
 941 	             s.connect                                                  <-- triggers another call to the C-method ossl_ssl_setup,  
                                                                                  but does not set up hostname, as it already ran once 
                                                                                 (see ossl_ssl.c:1205) 
 942 	           end 
 943 	           if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE 
 944 	             s.post_connection_check(@address) 
 945 	           end 
 946 	           @ssl_session = s.session                                     <-- (1st request) does not store hostname, so the call at line 924 does not set it. 
 947 	         rescue => exception 
 948 	           D "Conn close because of connect error #{exception}" 
 949 	           @socket.close if @socket and not @socket.closed? 
 950 	           raise exception 
 951 	         end 
 952 	       end 
 953 	       on_connect 
 954 	     end 
 ~~~ 

 ossl_ssl.c 

 ~~~ 
 101      static const char *ossl_ssl_attrs[] = { 
 102      #ifdef HAVE_SSL_SET_TLSEXT_HOST_NAME 
 103          "hostname", 
 104      #endif 
 105          "sync_close", 
 106      }; 
 ... 
 1330      ossl_ssl_connect(VALUE self) 
 1331      { 
 1332          ossl_ssl_setup(self); 
 1333          return ossl_start_ssl(self, SSL_connect, "SSL_connect", 0, 0); 
 1334      } 
 ... 
 1197      ossl_ssl_setup(VALUE self)     
 1198      {     
 1199          VALUE io, v_ctx, cb;     
 1200          SSL_CTX *ctx;     
 1201          SSL *ssl;     
 1202          rb_io_t *fptr;     
 1203         
 1204          GetSSL(self, ssl);     
 1205          if(!ssl){     
 1206      #ifdef HAVE_SSL_SET_TLSEXT_HOST_NAME     
 1207          VALUE hostname = rb_iv_get(self, "@hostname"); 
 1208      #endif     
 1209         
 1210              v_ctx = ossl_ssl_get_ctx(self);     
 1211              GetSSLCTX(v_ctx, ctx);     
 1212         
 1213              ssl = SSL_new(ctx);     
 1214              if (!ssl) {     
 1215                  ossl_raise(eSSLError, "SSL_new");     
 1216              }     
 1217              DATA_PTR(self) = ssl;     
 1218         
 1219      #ifdef HAVE_SSL_SET_TLSEXT_HOST_NAME     
 1220              if (!NIL_P(hostname)) {     
 1221                 if (SSL_set_tlsext_host_name(ssl, StringValuePtr(hostname)) != 1)     
 1222                     ossl_raise(eSSLError, "SSL_set_tlsext_host_name");     
 1223              }     
 1224      #endif     
 1225              io = ossl_ssl_get_io(self);     
 1226              GetOpenFile(io, fptr);     
 1227              rb_io_check_readable(fptr);     
 1228              rb_io_check_writable(fptr);     
 1229              SSL_set_fd(ssl, TO_SOCKET(FPTR_TO_FD(fptr)));     
 1230              SSL_set_ex_data(ssl, ossl_ssl_ex_ptr_idx, (void*)self); 
 1231              cb = ossl_sslctx_get_verify_cb(v_ctx); 
 1232              SSL_set_ex_data(ssl, ossl_ssl_ex_vcb_idx, (void*)cb); 
 1233              cb = ossl_sslctx_get_client_cert_cb(v_ctx); 
 1234              SSL_set_ex_data(ssl, ossl_ssl_ex_client_cert_cb_idx, (void*)cb); 
 1235              cb = ossl_sslctx_get_tmp_dh_cb(v_ctx); 
 1236              SSL_set_ex_data(ssl, ossl_ssl_ex_tmp_dh_callback_idx, (void*)cb); 
 1237              SSL_set_info_callback(ssl, ssl_info_cb); 
 1238          }     
 1239         
 1240          return Qtrue;     
 1241      }     
 ... 
 1825      ossl_ssl_set_session(VALUE self, VALUE arg1) 
 1826      { 
 1827          SSL *ssl; 
 1828          SSL_SESSION *sess; 
 1829     
 1830      /* why is ossl_ssl_setup delayed? */ 
 1831          ossl_ssl_setup(self); 
 1832     
 1833          ossl_ssl_data_get_struct(self, ssl); 
 1834     
 1835          SafeGetSSLSession(arg1, sess); 
 1836     
 1837          if (SSL_set_session(ssl, sess) != 1) 
 1838              ossl_raise(eSSLError, "SSL_set_session"); 
 1839     
 1840          return arg1; 
 1841      } 

 ~~~ 

 Patch to http.rb: 

 ~~~ 

 @@ -914,12 +914,12 @@ module Net     #:nodoc: 
              @socket.write(buf) 
              HTTPResponse.read_new(@socket).value 
            end 
 +            # Server Name Indication (SNI) RFC 3546 
 +            s.hostname = @address if s.respond_to? :hostname= 
            if @ssl_session and 
               Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout 
              s.session = @ssl_session if @ssl_session 
            end 
 -            # Server Name Indication (SNI) RFC 3546 
 -            s.hostname = @address if s.respond_to? :hostname= 
            Timeout.timeout(@open_timeout, Net::OpenTimeout) { s.connect } 
            if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE 
              s.post_connection_check(@address) 
 ~~~

Back