diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index e1e8d2c66c8..7a9f2eba381 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -578,6 +578,18 @@ 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` might contain following +fields: `rejectUnauthorized`, `requestCert`. `callback` will be executed, +once the renegotiation will be completed. + +NOTE: Could be used to request peer's certificate after secure connection +establishment. + +ANOTHER NOTE: When running it from server's side, socket will be destroyed +with an error after `handshakeTimeout` timeout. + ### tlsSocket.address() Returns the bound address, the address family name and port of the diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index 7ed787ca47d..53c8b99d62b 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -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,40 @@ 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 (options) { + 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()) + throw new Error('Failed to renegotiate'); + + // Ensure that we'll cycle through internal openssl's state + this.write(''); + + if (callback) + this.once('secure', callback); +}; + +TLSSocket.prototype._handleTimeout = function() { + this._tlsError(new Error('TLS handshake timeout')); }; TLSSocket.prototype._tlsError = function(err) { @@ -256,9 +292,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 +309,7 @@ TLSSocket.prototype._finishInit = function() { debug('secure established'); this._secureEstablished = true; + this.setTimeout(0, this._handleTimeout); this.emit('secure'); }; @@ -453,37 +491,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) { diff --git a/src/node_crypto.cc b/src/node_crypto.cc index c50e2ec6a25..88396ea2d88 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -805,6 +805,7 @@ void SSLWrap::AddMethods(Handle 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); @@ -1159,6 +1160,17 @@ void SSLWrap::EndParser(const FunctionCallbackInfo& args) { } +template +void SSLWrap::Renegotiate(const FunctionCallbackInfo& args) { + HandleScope scope(node_isolate); + + Base* w = ObjectWrap::Unwrap(args.This()); + + bool yes = SSL_renegotiate(w->ssl_) == 1; + args.GetReturnValue().Set(yes); +} + + template void SSLWrap::IsInitFinished(const FunctionCallbackInfo& args) { HandleScope scope(node_isolate); diff --git a/src/node_crypto.h b/src/node_crypto.h index 98e0f58ec07..b1c2c7fb077 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -172,6 +172,7 @@ class SSLWrap { static void GetCurrentCipher(const v8::FunctionCallbackInfo& args); static void ReceivedShutdown(const v8::FunctionCallbackInfo& args); static void EndParser(const v8::FunctionCallbackInfo& args); + static void Renegotiate(const v8::FunctionCallbackInfo& args); #ifdef OPENSSL_NPN_NEGOTIATED static void GetNegotiatedProto( diff --git a/test/simple/test-tls-server-verify.js b/test/simple/test-tls-server-verify.js index 2b09d821636..2c364753754 100644 --- a/test/simple/test-tls-server-verify.js +++ b/test/simple/test-tls-server-verify.js @@ -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: @@ -197,7 +215,7 @@ function runClient(options, cb) { rejected = false; } - if (/_authed/g.test(out)) { + if (!authed && /_authed/g.test(out)) { console.error(' * authed'); client.stdin.end('goodbye\n'); authed = true; @@ -247,7 +265,20 @@ function runTest(testIndex) { var connections = 0; - var server = tls.Server(serverOptions, function(c) { + var renegotiated = false; + var server = tls.Server(serverOptions, function handleConnection(c) { + if (tcase.renegotiate && !renegotiated) { + renegotiated = true; + setTimeout(function() { + console.error('- connected, renegotiating'); + return c.renegotiate({ + requestCert: true, + rejectUnauthorized: true + }, handleConnection.bind(null, c)); + }, 500); + return; + } + connections++; if (c.authorized) { console.error('- authed connection: ' +