tls: more session configuration options, methods
authorFedor Indutny <fedor.indutny@gmail.com>
Mon, 3 Feb 2014 21:32:13 +0000 (01:32 +0400)
committerFedor Indutny <fedor.indutny@gmail.com>
Wed, 5 Feb 2014 19:28:34 +0000 (23:28 +0400)
Introduce `ticketKeys` server option, `session` client option,
`getSession()` and `getTLSTicket()` methods.

fix #7032

doc/api/tls.markdown
lib/_tls_wrap.js
src/node_crypto.cc
src/node_crypto.h
test/simple/test-tls-ticket.js [new file with mode: 0644]

index cf7a87f..2a4c312 100644 (file)
@@ -205,6 +205,12 @@ automatically set as a listener for the [secureConnection][] event.  The
     session identifiers and TLS session tickets created by the server are
     timed out. See [SSL_CTX_set_timeout] for more details.
 
+  - `ticketKeys`: A 48-byte `Buffer` instance consisting of 16-byte prefix,
+    16-byte hmac key, 16-byte AES key. You could use it to accept tls session
+    tickets on multiple instances of tls server.
+
+    NOTE: Automatically shared between `cluster` module workers.
+
   - `sessionIdContext`: A string containing a opaque identifier for session
     resumption. If `requestCert` is `true`, the default is MD5 hash value
     generated from command-line. Otherwise, the default is not provided.
@@ -314,6 +320,8 @@ Creates a new client connection to the given `port` and `host` (old API) or
     SSL version 3. The possible values depend on your installation of
     OpenSSL and are defined in the constant [SSL_METHODS][].
 
+  - `session`: A `Buffer` instance, containing TLS session.
+
 The `callback` parameter will be added as a listener for the
 ['secureConnect'][] event.
 
@@ -398,6 +406,8 @@ Construct a new TLSSocket object from existing TCP socket.
 
   - `SNICallback`: Optional, see [tls.createServer][]
 
+  - `session`: Optional, a `Buffer` instance, containing TLS session
+
 ## tls.createSecurePair([credentials], [isServer], [requestCert], [rejectUnauthorized])
 
     Stability: 0 - Deprecated. Use tls.TLSSocket instead.
@@ -646,6 +656,18 @@ and their processing can be delayed due to packet loss or reordering. However,
 smaller fragments add extra TLS framing bytes and CPU overhead, which may
 decrease overall server throughput.
 
+### tlsSocket.getSession()
+
+Return ASN.1 encoded TLS session or `undefined` if none was negotiated. Could
+be used to speed up handshake establishment when reconnecting to the server.
+
+### tlsSocket.getTLSTicket()
+
+NOTE: Works only with client TLS sockets. Useful only for debugging, for
+session reuse provide `session` option to `tls.connect`.
+
+Return TLS session ticket or `undefined` if none was negotiated.
+
 ### tlsSocket.address()
 
 Returns the bound address, the address family name and port of the
index 9324130..ff76210 100644 (file)
@@ -232,6 +232,9 @@ TLSSocket.prototype._init = function(socket) {
   } else {
     this.ssl.onhandshakestart = function() {};
     this.ssl.onhandshakedone = this._finishInit.bind(this);
+
+    if (options.session)
+      this.ssl.setSession(options.session);
   }
 
   this.ssl.onerror = function(err) {
@@ -321,6 +324,10 @@ TLSSocket.prototype.setMaxSendFragment = function setMaxSendFragment(size) {
   return this.ssl.setMaxSendFragment(size) == 1;
 };
 
+TLSSocket.prototype.getTLSTicket = function getTLSTicket() {
+  return this.ssl.getTLSTicket();
+};
+
 TLSSocket.prototype._handleTimeout = function() {
   this._tlsError(new Error('TLS handshake timeout'));
 };
@@ -620,6 +627,7 @@ Server.prototype.setOptions = function(options) {
   if (!util.isUndefined(options.ecdhCurve))
     this.ecdhCurve = options.ecdhCurve;
   if (options.sessionTimeout) this.sessionTimeout = options.sessionTimeout;
+  if (options.ticketKeys) this.ticketKeys = options.ticketKeys;
   var secureOptions = options.secureOptions || 0;
   if (options.honorCipherOrder) {
     secureOptions |= constants.SSL_OP_CIPHER_SERVER_PREFERENCE;
@@ -747,6 +755,7 @@ exports.connect = function(/* [port, host], options, cb */) {
       isServer: false,
       requestCert: true,
       rejectUnauthorized: options.rejectUnauthorized,
+      session: options.session,
       NPNProtocols: NPN.NPNProtocols
     });
     result = socket;
index 987e2cf..9952b2c 100644 (file)
@@ -856,6 +856,7 @@ void SSLWrap<Base>::AddMethods(Handle<FunctionTemplate> t) {
   NODE_SET_PROTOTYPE_METHOD(t, "endParser", EndParser);
   NODE_SET_PROTOTYPE_METHOD(t, "renegotiate", Renegotiate);
   NODE_SET_PROTOTYPE_METHOD(t, "shutdown", Shutdown);
+  NODE_SET_PROTOTYPE_METHOD(t, "getTLSTicket", GetTLSTicket);
 
 #ifdef SSL_set_max_send_fragment
   NODE_SET_PROTOTYPE_METHOD(t, "setMaxSendFragment", SetMaxSendFragment);
@@ -1129,7 +1130,7 @@ void SSLWrap<Base>::GetSession(const FunctionCallbackInfo<Value>& args) {
   unsigned char* sbuf = new unsigned char[slen];
   unsigned char* p = sbuf;
   i2d_SSL_SESSION(sess, &p);
-  args.GetReturnValue().Set(Encode(sbuf, slen, BINARY));
+  args.GetReturnValue().Set(Encode(sbuf, slen, BUFFER));
   delete[] sbuf;
 }
 
@@ -1247,6 +1248,25 @@ void SSLWrap<Base>::Shutdown(const FunctionCallbackInfo<Value>& args) {
 }
 
 
+template <class Base>
+void SSLWrap<Base>::GetTLSTicket(const FunctionCallbackInfo<Value>& args) {
+  HandleScope scope(args.GetIsolate());
+
+  Base* w = Unwrap<Base>(args.This());
+  Environment* env = w->ssl_env();
+
+  SSL_SESSION* sess = SSL_get_session(w->ssl_);
+  if (sess == NULL || sess->tlsext_tick == NULL)
+    return;
+
+  Local<Object> buf = Buffer::New(env,
+                                  reinterpret_cast<char*>(sess->tlsext_tick),
+                                  sess->tlsext_ticklen);
+
+  args.GetReturnValue().Set(buf);
+}
+
+
 #ifdef SSL_set_max_send_fragment
 template <class Base>
 void SSLWrap<Base>::SetMaxSendFragment(
index aa670db..8e94950 100644 (file)
@@ -187,6 +187,7 @@ class SSLWrap {
   static void EndParser(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Renegotiate(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Shutdown(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void GetTLSTicket(const v8::FunctionCallbackInfo<v8::Value>& args);
 
 #ifdef SSL_set_max_send_fragment
   static void SetMaxSendFragment(
diff --git a/test/simple/test-tls-ticket.js b/test/simple/test-tls-ticket.js
new file mode 100644 (file)
index 0000000..471d8c3
--- /dev/null
@@ -0,0 +1,95 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+if (!process.versions.openssl) {
+  console.error('Skipping because node compiled without OpenSSL.');
+  process.exit(0);
+}
+
+var assert = require('assert');
+var fs = require('fs');
+var net = require('net');
+var tls = require('tls');
+var crypto = require('crypto');
+
+var common = require('../common');
+
+var keys = crypto.randomBytes(48);
+var serverLog = [];
+var ticketLog = [];
+
+var serverCount = 0;
+function createServer() {
+  var id = serverCount++;
+
+  var server = tls.createServer({
+    key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'),
+    cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem'),
+    ticketKeys: keys
+  }, function(c) {
+    serverLog.push(id);
+    c.end();
+  });
+
+  return server;
+}
+
+var servers = [ createServer(), createServer(), createServer(), createServer(), createServer(), createServer() ];
+
+// Create one TCP server and balance sockets to multiple TLS server instances
+var shared = net.createServer(function(c) {
+  servers.shift().emit('connection', c);
+}).listen(common.PORT, function() {
+  start(function() {
+    shared.close();
+  });
+});
+
+function start(callback) {
+  var sess = null;
+  var left = servers.length;
+
+  function connect() {
+    var s = tls.connect(common.PORT, {
+      session: sess,
+      rejectUnauthorized: false
+    }, function() {
+      sess = s.getSession() || sess;
+      ticketLog.push(s.getTLSTicket().toString('hex'));
+    });
+    s.on('close', function() {
+      if (--left === 0)
+        callback();
+      else
+        connect();
+    });
+  }
+
+  connect();
+}
+
+process.on('exit', function() {
+  assert.equal(ticketLog.length, serverLog.length);
+  for (var i = 0; i < serverLog.length - 1; i++) {
+    assert.notEqual(serverLog[i], serverLog[i + 1]);
+    assert.equal(ticketLog[i], ticketLog[i + 1]);
+  }
+});