Server must not request cert.
authorRyan Dahl <ry@tinyclouds.org>
Sat, 4 Dec 2010 01:07:09 +0000 (17:07 -0800)
committerRyan Dahl <ry@tinyclouds.org>
Mon, 6 Dec 2010 02:13:20 +0000 (18:13 -0800)
lib/securepair.js
lib/tls.js
src/node_crypto.cc
test/disabled/test-tls-server.js

index 4ecc8f6..2db5a23 100644 (file)
@@ -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);
+  }
 };
 
 
index 749fd25..7d57426 100644 (file)
@@ -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;
index b7bc8f3..3db4e89 100644 (file)
@@ -389,8 +389,26 @@ Handle<Value> 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_);
index 1e6594d..363a530 100644 (file)
@@ -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);