tls: socket.renegotiate(options, callback)
authorFedor Indutny <fedor.indutny@gmail.com>
Fri, 23 Aug 2013 13:53:16 +0000 (17:53 +0400)
committerFedor Indutny <fedor.indutny@gmail.com>
Thu, 5 Sep 2013 14:10:31 +0000 (18:10 +0400)
This utility function allows renegotiaion of secure connection after
establishing it.

fix #2496

doc/api/tls.markdown
lib/_tls_wrap.js
src/node_crypto.cc
src/node_crypto.h
test/simple/test-tls-server-verify.js

index e1e8d2c..de50a79 100644 (file)
@@ -578,6 +578,19 @@ See SSL_CIPHER_get_name() and SSL_CIPHER_get_version() in
 http://www.openssl.org/docs/ssl/ssl.html#DEALING_WITH_CIPHERS for more
 information.
 
+### tlsSocket.renegotiate(options, callback)
+
+Initiate TLS renegotiation process. The `options` may contain the following
+fields: `rejectUnauthorized`, `requestCert` (See [tls.createServer][]
+for details). `callback(err)` will be executed with `null` as `err`,
+once the renegotiation is successfully completed.
+
+NOTE: Can be used to request peer's certificate after the secure connection
+has been established.
+
+ANOTHER NOTE: When running as the server, socket will be destroyed
+with an error after `handshakeTimeout` timeout.
+
 ### tlsSocket.address()
 
 Returns the bound address, the address family name and port of the
index 7ed787c..32e9a25 100644 (file)
@@ -194,6 +194,8 @@ TLSSocket.prototype._init = function() {
   var requestCert = !!options.requestCert || !options.isServer,
       rejectUnauthorized = !!options.rejectUnauthorized;
 
+  this._requestCert = requestCert;
+  this._rejectUnauthorized = rejectUnauthorized;
   if (requestCert || rejectUnauthorized)
     this.ssl.setVerifyMode(requestCert, rejectUnauthorized);
 
@@ -246,6 +248,49 @@ TLSSocket.prototype._init = function() {
 
   if (process.features.tls_npn && options.NPNProtocols)
     this.ssl.setNPNProtocols(options.NPNProtocols);
+
+  if (options.handshakeTimeout > 0)
+    this.setTimeout(options.handshakeTimeout, this._handleTimeout);
+};
+
+TLSSocket.prototype.renegotiate = function(options, callback) {
+  var requestCert = this._requestCert,
+      rejectUnauthorized = this._rejectUnauthorized;
+
+  if (typeof options.requestCert !== 'undefined')
+    requestCert = !!options.requestCert;
+  if (typeof options.rejectUnauthorized !== 'undefined')
+    rejectUnauthorized = !!options.rejectUnauthorized;
+
+  if (requestCert !== this._requestCert ||
+      rejectUnauthorized !== this._rejectUnauthorized) {
+    this.ssl.setVerifyMode(requestCert, rejectUnauthorized);
+    this._requestCert = requestCert;
+    this._rejectUnauthorized = rejectUnauthorized;
+  }
+  if (!this.ssl.renegotiate()) {
+    if (callback) {
+      process.nextTick(function() {
+        callback(new Error('Failed to renegotiate'));
+      });
+    }
+    return false;
+  }
+
+  // Ensure that we'll cycle through internal openssl's state
+  this.write('');
+
+  if (callback) {
+    this.once('secure', function() {
+      callback(null);
+    });
+  }
+
+  return true;
+};
+
+TLSSocket.prototype._handleTimeout = function() {
+  this._tlsError(new Error('TLS handshake timeout'));
 };
 
 TLSSocket.prototype._tlsError = function(err) {
@@ -256,9 +301,10 @@ TLSSocket.prototype._tlsError = function(err) {
 
 TLSSocket.prototype._releaseControl = function() {
   if (this._controlReleased)
-    return;
+    return false;
   this._controlReleased = true;
   this.removeListener('error', this._tlsError);
+  return true;
 };
 
 TLSSocket.prototype._finishInit = function() {
@@ -272,6 +318,8 @@ TLSSocket.prototype._finishInit = function() {
 
   debug('secure established');
   this._secureEstablished = true;
+  if (this._tlsOptions.handshakeTimeout > 0)
+    this.setTimeout(0, this._handleTimeout);
   this.emit('secure');
 };
 
@@ -453,37 +501,26 @@ function Server(/* [options], listener */) {
       server: self,
       requestCert: self.requestCert,
       rejectUnauthorized: self.rejectUnauthorized,
+      handshakeTimeout: timeout,
       NPNProtocols: self.NPNProtocols,
       SNICallback: options.SNICallback || SNICallback
     });
 
-    function listener() {
-      socket._tlsError(new Error('TLS handshake timeout'));
-    }
-
-    if (timeout > 0) {
-      socket.setTimeout(timeout, listener);
-    }
-
-    socket.once('secure', function() {
-      socket.setTimeout(0, listener);
-
-      if (self.requestCert) {
+    socket.on('secure', function() {
+      if (socket._requestCert) {
         var verifyError = socket.ssl.verifyError();
         if (verifyError) {
           socket.authorizationError = verifyError.message;
 
-          if (self.rejectUnauthorized)
+          if (socket._rejectUnauthorized)
             socket.destroy();
         } else {
           socket.authorized = true;
         }
       }
 
-      if (!socket.destroyed) {
-        socket._releaseControl();
+      if (!socket.destroyed && socket._releaseControl())
         self.emit('secureConnection', socket);
-      }
     });
 
     socket.on('_tlsError', function(err) {
@@ -546,7 +583,7 @@ Server.prototype.setOptions = function(options) {
   if (options.NPNProtocols) tls.convertNPNProtocols(options.NPNProtocols, this);
   if (options.sessionIdContext) {
     this.sessionIdContext = options.sessionIdContext;
-  } else if (this.requestCert) {
+  } else {
     this.sessionIdContext = crypto.createHash('md5')
                                   .update(process.argv.join(' '))
                                   .digest('hex');
index c50e2ec..ff90e91 100644 (file)
@@ -805,6 +805,7 @@ void SSLWrap<Base>::AddMethods(Handle<FunctionTemplate> t) {
   NODE_SET_PROTOTYPE_METHOD(t, "getCurrentCipher", GetCurrentCipher);
   NODE_SET_PROTOTYPE_METHOD(t, "receivedShutdown", ReceivedShutdown);
   NODE_SET_PROTOTYPE_METHOD(t, "endParser", EndParser);
+  NODE_SET_PROTOTYPE_METHOD(t, "renegotiate", Renegotiate);
 
 #ifdef OPENSSL_NPN_NEGOTIATED
   NODE_SET_PROTOTYPE_METHOD(t, "getNegotiatedProtocol", GetNegotiatedProto);
@@ -1160,6 +1161,20 @@ void SSLWrap<Base>::EndParser(const FunctionCallbackInfo<Value>& args) {
 
 
 template <class Base>
+void SSLWrap<Base>::Renegotiate(const FunctionCallbackInfo<Value>& args) {
+  HandleScope scope(node_isolate);
+
+  Base* w = ObjectWrap::Unwrap<Base>(args.This());
+
+  ClearErrorOnReturn clear_error_on_return;
+  (void) &clear_error_on_return;  // Silence unused variable warning.
+
+  bool yes = SSL_renegotiate(w->ssl_) == 1;
+  args.GetReturnValue().Set(yes);
+}
+
+
+template <class Base>
 void SSLWrap<Base>::IsInitFinished(const FunctionCallbackInfo<Value>& args) {
   HandleScope scope(node_isolate);
   Base* w = ObjectWrap::Unwrap<Base>(args.This());
index 98e0f58..b1c2c7f 100644 (file)
@@ -172,6 +172,7 @@ class SSLWrap {
   static void GetCurrentCipher(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void ReceivedShutdown(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void EndParser(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void Renegotiate(const v8::FunctionCallbackInfo<v8::Value>& args);
 
 #ifdef OPENSSL_NPN_NEGOTIATED
   static void GetNegotiatedProto(
index 2b09d82..03f598d 100644 (file)
@@ -39,6 +39,7 @@ var testCases =
     [{ title: 'Do not request certs. Everyone is unauthorized.',
       requestCert: false,
       rejectUnauthorized: false,
+      renegotiate: false,
       CAs: ['ca1-cert'],
       clients:
        [{ name: 'agent1', shouldReject: false, shouldAuth: false },
@@ -51,6 +52,20 @@ var testCases =
     { title: 'Allow both authed and unauthed connections with CA1',
       requestCert: true,
       rejectUnauthorized: false,
+      renegotiate: false,
+      CAs: ['ca1-cert'],
+      clients:
+       [{ name: 'agent1', shouldReject: false, shouldAuth: true },
+        { name: 'agent2', shouldReject: false, shouldAuth: false },
+        { name: 'agent3', shouldReject: false, shouldAuth: false },
+        { name: 'nocert', shouldReject: false, shouldAuth: false }
+       ]
+    },
+
+    { title: 'Do not request certs at connection. Do that later',
+      requestCert: false,
+      rejectUnauthorized: false,
+      renegotiate: true,
       CAs: ['ca1-cert'],
       clients:
        [{ name: 'agent1', shouldReject: false, shouldAuth: true },
@@ -63,6 +78,7 @@ var testCases =
     { title: 'Allow only authed connections with CA1',
       requestCert: true,
       rejectUnauthorized: true,
+      renegotiate: false,
       CAs: ['ca1-cert'],
       clients:
        [{ name: 'agent1', shouldReject: false, shouldAuth: true },
@@ -75,6 +91,7 @@ var testCases =
     { title: 'Allow only authed connections with CA1 and CA2',
       requestCert: true,
       rejectUnauthorized: true,
+      renegotiate: false,
       CAs: ['ca1-cert', 'ca2-cert'],
       clients:
        [{ name: 'agent1', shouldReject: false, shouldAuth: true },
@@ -88,6 +105,7 @@ var testCases =
     { title: 'Allow only certs signed by CA2 but not in the CRL',
       requestCert: true,
       rejectUnauthorized: true,
+      renegotiate: false,
       CAs: ['ca2-cert'],
       crl: 'ca2-crl',
       clients:
@@ -104,6 +122,7 @@ var testCases =
 
 
 var common = require('../common');
+var constants = require('constants');
 var assert = require('assert');
 var fs = require('fs');
 var tls = require('tls');
@@ -185,20 +204,23 @@ function runClient(options, cb) {
 
   var rejected = true;
   var authed = false;
+  var goodbye = false;
 
   client.stdout.setEncoding('utf8');
   client.stdout.on('data', function(d) {
     out += d;
 
-    if (/_unauthed/g.test(out)) {
+    if (!goodbye && /_unauthed/g.test(out)) {
       console.error('  * unauthed');
+      goodbye = true;
       client.stdin.end('goodbye\n');
       authed = false;
       rejected = false;
     }
 
-    if (/_authed/g.test(out)) {
+    if (!goodbye && /_authed/g.test(out)) {
       console.error('  * authed');
+      goodbye = true;
       client.stdin.end('goodbye\n');
       authed = true;
       rejected = false;
@@ -247,7 +269,34 @@ function runTest(testIndex) {
 
   var connections = 0;
 
-  var server = tls.Server(serverOptions, function(c) {
+  /*
+   * If renegotiating - session might be resumed and openssl won't request
+   * client's certificate (probably because of bug in the openssl)
+   */
+  if (tcase.renegotiate) {
+    serverOptions.secureOptions =
+        constants.SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION;
+  }
+
+  var renegotiated = false;
+  var server = tls.Server(serverOptions, function handleConnection(c) {
+    if (tcase.renegotiate && !renegotiated) {
+      renegotiated = true;
+      setTimeout(function() {
+        console.error('- connected, renegotiating');
+        c.write('\n_renegotiating\n');
+        return c.renegotiate({
+          requestCert: true,
+          rejectUnauthorized: false
+        }, function(err) {
+          assert(!err);
+          c.write('\n_renegotiated\n');
+          handleConnection(c);
+        });
+      }, 200);
+      return;
+    }
+
     connections++;
     if (c.authorized) {
       console.error('- authed connection: ' +