diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown
index e1e8d2c66c..de50a7964b 100644
--- a/doc/api/tls.markdown
+++ b/doc/api/tls.markdown
@@ -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
diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js
index 7ed787ca47..32e9a257a9 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,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');
diff --git a/src/node_crypto.cc b/src/node_crypto.cc
index c50e2ec6a2..ff90e91a46 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,20 @@ void SSLWrap::EndParser(const FunctionCallbackInfo& args) {
}
+template
+void SSLWrap::Renegotiate(const FunctionCallbackInfo& args) {
+ HandleScope scope(node_isolate);
+
+ Base* w = ObjectWrap::Unwrap(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
void SSLWrap::IsInitFinished(const FunctionCallbackInfo& args) {
HandleScope scope(node_isolate);
diff --git a/src/node_crypto.h b/src/node_crypto.h
index 98e0f58ec0..b1c2c7fb07 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 2b09d82163..03f598d70f 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:
@@ -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: ' +