diff --git a/lib/securepair.js b/lib/securepair.js index 4ecc8f6716..2db5a235a0 100644 --- a/lib/securepair.js +++ b/lib/securepair.js @@ -20,9 +20,9 @@ var SecureStream = null; * Provides a pair of streams to do encrypted communication. */ -function SecurePair(credentials, isServer, shouldVerifyPeer) { +function SecurePair(credentials, isServer, requestCert, rejectUnauthorized) { if (!(this instanceof SecurePair)) { - return new SecurePair(credentials, isServer, shouldVerifyPeer); + return new SecurePair(credentials, isServer, requestCert, rejectUnauthorized); } var self = this; @@ -53,16 +53,20 @@ function SecurePair(credentials, isServer, shouldVerifyPeer) { if (!this._isServer) { // For clients, we will always have either a given ca list or be using // default one - shouldVerifyPeer = true; + requestCert = true; } this._secureEstablished = false; this._encInPending = []; this._clearInPending = []; + this._rejectUnauthorized = rejectUnauthorized ? true : false; + this._requestCert = requestCert ? true : false; + this._ssl = new SecureStream(this.credentials.context, this._isServer ? true : false, - shouldVerifyPeer ? true : false); + this._requestCert, + this._rejectUnauthorized); /* Acts as a r/w stream to the cleartext side of the stream. */ @@ -144,20 +148,6 @@ function SecurePair(credentials, isServer, shouldVerifyPeer) { self._destroy(); }); - this.encrypted.on('end', function() { - if (!self._done) { - self._error( - new Error('Encrypted stream ended before secure pair was done')); - } - }); - - this.encrypted.on('close', function() { - if (!self._done) { - self._error( - new Error('Encrypted stream closed before secure pair was done')); - } - }); - this.cleartext.on('drain', function() { debug('source drain'); self._cycle(); @@ -179,8 +169,14 @@ function SecurePair(credentials, isServer, shouldVerifyPeer) { util.inherits(SecurePair, events.EventEmitter); -exports.createSecurePair = function(credentials, isServer, shouldVerifyPeer) { - var pair = new SecurePair(credentials, isServer, shouldVerifyPeer); +exports.createSecurePair = function(credentials, + isServer, + requestCert, + rejectUnauthorized) { + var pair = new SecurePair(credentials, + isServer, + requestCert, + rejectUnauthorized); return pair; }; @@ -330,16 +326,20 @@ SecurePair.prototype._cycle = function() { mover( function(pool, offset, length) { debug('reading from encOut'); + if (!self._ssl) return -1; return self._ssl.encOut(pool, offset, length); }, function(chunk) { self.encrypted.emit('data', chunk); }, function(bytesRead) { + if (!self._ssl) return false; return bytesRead > 0 && self._encryptedWriteState === true; }); - if (!this._secureEstablished && this._ssl.isInitFinished()) { + + + if (this._ssl && !this._secureEstablished && this._ssl.isInitFinished()) { this._secureEstablished = true; debug('secure established'); this.emit('secure'); @@ -353,13 +353,22 @@ SecurePair.prototype._destroy = function(err) { this._done = true; this._ssl.close(); this._ssl = null; + this.encrypted.emit('close'); + this.cleartext.emit('close'); this.emit('end', err); } }; SecurePair.prototype._error = function(err) { - this.emit('error', err); + if (this._isServer && + this._rejectUnauthorized && + /peer did not return a certificate/.test(err.message)) { + // Not really an error. + this._destroy(); + } else { + this.emit('error', err); + } }; diff --git a/lib/tls.js b/lib/tls.js index 749fd251c6..7d5742675a 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -1,14 +1,58 @@ var crypto = require('crypto'); +var securepair = require('securepair'); var net = require('net'); var events = require('events'); var inherits = require('util').inherits; +var assert = process.assert; + // TODO: support anonymous (nocert) and PSK // TODO: how to proxy maxConnections? +// AUTHENTICATION MODES +// +// There are several levels of authentication that TLS/SSL supports. +// Read more about this in "man SSL_set_verify". +// +// 1. The server sends a certificate to the client but does not request a +// cert from the client. This is common for most HTTPS servers. The browser +// can verify the identity of the server, but the server does not know who +// the client is. Authenticating the client is usually done over HTTP using +// login boxes and cookies and stuff. +// +// 2. The server sends a cert to the client and requests that the client +// also send it a cert. The client knows who the server is and the server is +// requesting the client also identify themselves. There are several +// outcomes: +// +// A) verifyError returns null meaning the client's certificate is signed +// by one of the server's CAs. The server know's the client idenity now +// and the client is authorized. +// +// B) For some reason the client's certificate is not acceptable - +// verifyError returns a string indicating the problem. The server can +// either (i) reject the client or (ii) allow the client to connect as an +// unauthorized connection. +// +// The mode is controlled by two boolean variables. +// +// requestCert +// If true the server requests a certificate from client connections. For +// the common HTTPS case, users will want this to be false, which is what +// it defaults to. +// +// rejectUnauthorized +// If true clients whose certificates are invalid for any reason will not +// be allowed to make connections. If false, they will simply be marked as +// unauthorized but secure communication will continue. By default this is +// false. +// +// +// // Options: -// - unauthorizedPeers. Boolean, default to false. +// - requestCert. Send verify request. Default to false. +// - rejectUnauthorized. Boolean, default to false. // - key. string. // - cert: string. // - ca: string or array of strings. @@ -56,24 +100,23 @@ function Server(/* [options], listener */) { { key: self.key, cert: self.cert, ca: self.ca }); creds.context.setCiphers('RC4-SHA:AES128-SHA:AES256-SHA'); - var pair = crypto.createPair(creds, - true, - !self.unauthorizedPeers); + var pair = securepair.createSecurePair(creds, + true, + self.requestCert, + self.rejectUnauthorized); pair.encrypted.pipe(socket); socket.pipe(pair.encrypted); - pair.on('secure', function() { - var verifyError = pair._ssl.verifyError(); - - if (verifyError) { - if (self.unauthorizedPeers) { + pair.on('secure', function(verifyError) { + if (!self.requestCert) { + self.emit('unauthorized', pair.cleartext); + } else { + var verifyError = pair._ssl.verifyError(); + if (verifyError) { self.emit('unauthorized', pair.cleartext, verifyError); } else { - console.error('REJECT PEER. verify error: %s', verifyError); - socket.destroy(); + self.emit('authorized', pair.cleartext); } - } else { - self.emit('authorized', pair.cleartext); } }); @@ -97,7 +140,6 @@ function Server(/* [options], listener */) { } // Handle option defaults: - this.setOptions(options); } @@ -109,10 +151,16 @@ exports.createServer = function(options, listener) { Server.prototype.setOptions = function(options) { - if (typeof options.unauthorizedPeers == 'boolean') { - this.unauthorizedPeers = options.unauthorizedPeers; + if (typeof options.requestCert == 'boolean') { + this.requestCert = options.requestCert; + } else { + this.requestCert = false; + } + + if (typeof options.rejectUnauthorized == 'boolean') { + this.rejectUnauthorized = options.rejectUnauthorized; } else { - this.unauthorizedPeers = false; + this.rejectUnauthorized = false; } if (options.key) this.key = options.key; diff --git a/src/node_crypto.cc b/src/node_crypto.cc index b7bc8f3efd..3db4e89ef2 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -389,8 +389,26 @@ Handle SecureStream::New(const Arguments& args) { SSL_set_mode(p->ssl_, mode | SSL_MODE_RELEASE_BUFFERS); #endif + + int verify_mode; + if (is_server) { + bool request_cert = args[2]->BooleanValue(); + if (!request_cert) { + // Note reject_unauthorized ignored. + verify_mode = SSL_VERIFY_NONE; + } else { + bool reject_unauthorized = args[3]->BooleanValue(); + verify_mode = SSL_VERIFY_PEER; + if (reject_unauthorized) verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + } + } else { + // Note request_cert and reject_unauthorized are ignored for clients. + verify_mode = SSL_VERIFY_NONE; + } + + // Always allow a connection. We'll reject in javascript. - SSL_set_verify(p->ssl_, SSL_VERIFY_PEER, VerifyCallback); + SSL_set_verify(p->ssl_, verify_mode, VerifyCallback); if ((p->is_server_ = is_server)) { SSL_set_accept_state(p->ssl_); diff --git a/test/disabled/test-tls-server.js b/test/disabled/test-tls-server.js index 1e6594db88..363a530506 100644 --- a/test/disabled/test-tls-server.js +++ b/test/disabled/test-tls-server.js @@ -13,7 +13,11 @@ var join = require('path').join; var key = fs.readFileSync(join(common.fixturesDir, 'agent.key')).toString(); var cert = fs.readFileSync(join(common.fixturesDir, 'agent.crt')).toString(); -s = tls.Server({key: key, cert: cert, unauthorizedPeers: false}); +s = tls.Server({ key: key, + cert: cert, + ca: [], + requestCert: true, + rejectUnauthorized: true }); s.listen(common.PORT, function() { console.log('TLS server on 127.0.0.1:%d', common.PORT);