Browse Source

tls: socket.renegotiate(options, callback)

This utility function allows renegotiaion of secure connection after
establishing it.

fix #2496
v0.11.8-release
Fedor Indutny 12 years ago
parent
commit
af76b08666
  1. 13
      doc/api/tls.markdown
  2. 73
      lib/_tls_wrap.js
  3. 15
      src/node_crypto.cc
  4. 1
      src/node_crypto.h
  5. 55
      test/simple/test-tls-server-verify.js

13
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 http://www.openssl.org/docs/ssl/ssl.html#DEALING_WITH_CIPHERS for more
information. 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() ### tlsSocket.address()
Returns the bound address, the address family name and port of the Returns the bound address, the address family name and port of the

73
lib/_tls_wrap.js

@ -194,6 +194,8 @@ TLSSocket.prototype._init = function() {
var requestCert = !!options.requestCert || !options.isServer, var requestCert = !!options.requestCert || !options.isServer,
rejectUnauthorized = !!options.rejectUnauthorized; rejectUnauthorized = !!options.rejectUnauthorized;
this._requestCert = requestCert;
this._rejectUnauthorized = rejectUnauthorized;
if (requestCert || rejectUnauthorized) if (requestCert || rejectUnauthorized)
this.ssl.setVerifyMode(requestCert, rejectUnauthorized); this.ssl.setVerifyMode(requestCert, rejectUnauthorized);
@ -246,6 +248,49 @@ TLSSocket.prototype._init = function() {
if (process.features.tls_npn && options.NPNProtocols) if (process.features.tls_npn && options.NPNProtocols)
this.ssl.setNPNProtocols(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) { TLSSocket.prototype._tlsError = function(err) {
@ -256,9 +301,10 @@ TLSSocket.prototype._tlsError = function(err) {
TLSSocket.prototype._releaseControl = function() { TLSSocket.prototype._releaseControl = function() {
if (this._controlReleased) if (this._controlReleased)
return; return false;
this._controlReleased = true; this._controlReleased = true;
this.removeListener('error', this._tlsError); this.removeListener('error', this._tlsError);
return true;
}; };
TLSSocket.prototype._finishInit = function() { TLSSocket.prototype._finishInit = function() {
@ -272,6 +318,8 @@ TLSSocket.prototype._finishInit = function() {
debug('secure established'); debug('secure established');
this._secureEstablished = true; this._secureEstablished = true;
if (this._tlsOptions.handshakeTimeout > 0)
this.setTimeout(0, this._handleTimeout);
this.emit('secure'); this.emit('secure');
}; };
@ -453,37 +501,26 @@ function Server(/* [options], listener */) {
server: self, server: self,
requestCert: self.requestCert, requestCert: self.requestCert,
rejectUnauthorized: self.rejectUnauthorized, rejectUnauthorized: self.rejectUnauthorized,
handshakeTimeout: timeout,
NPNProtocols: self.NPNProtocols, NPNProtocols: self.NPNProtocols,
SNICallback: options.SNICallback || SNICallback SNICallback: options.SNICallback || SNICallback
}); });
function listener() { socket.on('secure', function() {
socket._tlsError(new Error('TLS handshake timeout')); if (socket._requestCert) {
}
if (timeout > 0) {
socket.setTimeout(timeout, listener);
}
socket.once('secure', function() {
socket.setTimeout(0, listener);
if (self.requestCert) {
var verifyError = socket.ssl.verifyError(); var verifyError = socket.ssl.verifyError();
if (verifyError) { if (verifyError) {
socket.authorizationError = verifyError.message; socket.authorizationError = verifyError.message;
if (self.rejectUnauthorized) if (socket._rejectUnauthorized)
socket.destroy(); socket.destroy();
} else { } else {
socket.authorized = true; socket.authorized = true;
} }
} }
if (!socket.destroyed) { if (!socket.destroyed && socket._releaseControl())
socket._releaseControl();
self.emit('secureConnection', socket); self.emit('secureConnection', socket);
}
}); });
socket.on('_tlsError', function(err) { socket.on('_tlsError', function(err) {
@ -546,7 +583,7 @@ Server.prototype.setOptions = function(options) {
if (options.NPNProtocols) tls.convertNPNProtocols(options.NPNProtocols, this); if (options.NPNProtocols) tls.convertNPNProtocols(options.NPNProtocols, this);
if (options.sessionIdContext) { if (options.sessionIdContext) {
this.sessionIdContext = options.sessionIdContext; this.sessionIdContext = options.sessionIdContext;
} else if (this.requestCert) { } else {
this.sessionIdContext = crypto.createHash('md5') this.sessionIdContext = crypto.createHash('md5')
.update(process.argv.join(' ')) .update(process.argv.join(' '))
.digest('hex'); .digest('hex');

15
src/node_crypto.cc

@ -805,6 +805,7 @@ void SSLWrap<Base>::AddMethods(Handle<FunctionTemplate> t) {
NODE_SET_PROTOTYPE_METHOD(t, "getCurrentCipher", GetCurrentCipher); NODE_SET_PROTOTYPE_METHOD(t, "getCurrentCipher", GetCurrentCipher);
NODE_SET_PROTOTYPE_METHOD(t, "receivedShutdown", ReceivedShutdown); NODE_SET_PROTOTYPE_METHOD(t, "receivedShutdown", ReceivedShutdown);
NODE_SET_PROTOTYPE_METHOD(t, "endParser", EndParser); NODE_SET_PROTOTYPE_METHOD(t, "endParser", EndParser);
NODE_SET_PROTOTYPE_METHOD(t, "renegotiate", Renegotiate);
#ifdef OPENSSL_NPN_NEGOTIATED #ifdef OPENSSL_NPN_NEGOTIATED
NODE_SET_PROTOTYPE_METHOD(t, "getNegotiatedProtocol", GetNegotiatedProto); NODE_SET_PROTOTYPE_METHOD(t, "getNegotiatedProtocol", GetNegotiatedProto);
@ -1159,6 +1160,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> template <class Base>
void SSLWrap<Base>::IsInitFinished(const FunctionCallbackInfo<Value>& args) { void SSLWrap<Base>::IsInitFinished(const FunctionCallbackInfo<Value>& args) {
HandleScope scope(node_isolate); HandleScope scope(node_isolate);

1
src/node_crypto.h

@ -172,6 +172,7 @@ class SSLWrap {
static void GetCurrentCipher(const v8::FunctionCallbackInfo<v8::Value>& args); static void GetCurrentCipher(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ReceivedShutdown(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 EndParser(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Renegotiate(const v8::FunctionCallbackInfo<v8::Value>& args);
#ifdef OPENSSL_NPN_NEGOTIATED #ifdef OPENSSL_NPN_NEGOTIATED
static void GetNegotiatedProto( static void GetNegotiatedProto(

55
test/simple/test-tls-server-verify.js

@ -39,6 +39,7 @@ var testCases =
[{ title: 'Do not request certs. Everyone is unauthorized.', [{ title: 'Do not request certs. Everyone is unauthorized.',
requestCert: false, requestCert: false,
rejectUnauthorized: false, rejectUnauthorized: false,
renegotiate: false,
CAs: ['ca1-cert'], CAs: ['ca1-cert'],
clients: clients:
[{ name: 'agent1', shouldReject: false, shouldAuth: false }, [{ name: 'agent1', shouldReject: false, shouldAuth: false },
@ -51,6 +52,20 @@ var testCases =
{ title: 'Allow both authed and unauthed connections with CA1', { title: 'Allow both authed and unauthed connections with CA1',
requestCert: true, requestCert: true,
rejectUnauthorized: false, 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'], CAs: ['ca1-cert'],
clients: clients:
[{ name: 'agent1', shouldReject: false, shouldAuth: true }, [{ name: 'agent1', shouldReject: false, shouldAuth: true },
@ -63,6 +78,7 @@ var testCases =
{ title: 'Allow only authed connections with CA1', { title: 'Allow only authed connections with CA1',
requestCert: true, requestCert: true,
rejectUnauthorized: true, rejectUnauthorized: true,
renegotiate: false,
CAs: ['ca1-cert'], CAs: ['ca1-cert'],
clients: clients:
[{ name: 'agent1', shouldReject: false, shouldAuth: true }, [{ name: 'agent1', shouldReject: false, shouldAuth: true },
@ -75,6 +91,7 @@ var testCases =
{ title: 'Allow only authed connections with CA1 and CA2', { title: 'Allow only authed connections with CA1 and CA2',
requestCert: true, requestCert: true,
rejectUnauthorized: true, rejectUnauthorized: true,
renegotiate: false,
CAs: ['ca1-cert', 'ca2-cert'], CAs: ['ca1-cert', 'ca2-cert'],
clients: clients:
[{ name: 'agent1', shouldReject: false, shouldAuth: true }, [{ name: 'agent1', shouldReject: false, shouldAuth: true },
@ -88,6 +105,7 @@ var testCases =
{ title: 'Allow only certs signed by CA2 but not in the CRL', { title: 'Allow only certs signed by CA2 but not in the CRL',
requestCert: true, requestCert: true,
rejectUnauthorized: true, rejectUnauthorized: true,
renegotiate: false,
CAs: ['ca2-cert'], CAs: ['ca2-cert'],
crl: 'ca2-crl', crl: 'ca2-crl',
clients: clients:
@ -104,6 +122,7 @@ var testCases =
var common = require('../common'); var common = require('../common');
var constants = require('constants');
var assert = require('assert'); var assert = require('assert');
var fs = require('fs'); var fs = require('fs');
var tls = require('tls'); var tls = require('tls');
@ -185,20 +204,23 @@ function runClient(options, cb) {
var rejected = true; var rejected = true;
var authed = false; var authed = false;
var goodbye = false;
client.stdout.setEncoding('utf8'); client.stdout.setEncoding('utf8');
client.stdout.on('data', function(d) { client.stdout.on('data', function(d) {
out += d; out += d;
if (/_unauthed/g.test(out)) { if (!goodbye && /_unauthed/g.test(out)) {
console.error(' * unauthed'); console.error(' * unauthed');
goodbye = true;
client.stdin.end('goodbye\n'); client.stdin.end('goodbye\n');
authed = false; authed = false;
rejected = false; rejected = false;
} }
if (/_authed/g.test(out)) { if (!goodbye && /_authed/g.test(out)) {
console.error(' * authed'); console.error(' * authed');
goodbye = true;
client.stdin.end('goodbye\n'); client.stdin.end('goodbye\n');
authed = true; authed = true;
rejected = false; rejected = false;
@ -247,7 +269,34 @@ function runTest(testIndex) {
var connections = 0; 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++; connections++;
if (c.authorized) { if (c.authorized) {
console.error('- authed connection: ' + console.error('- authed connection: ' +

Loading…
Cancel
Save