diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index f89de220596..9bea27d0166 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -164,6 +164,10 @@ automatically set as a listener for the [secureConnection][] event. The SecureContext). If `SNICallback` wasn't provided - default callback with high-level API will be used (see below). + - `sessionTimeout`: An integer specifiying the seconds after which TLS + session identifiers and TLS session tickets created by the server are + timed out. See [SSL_CTX_set_timeout] for more details. + - `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. @@ -561,3 +565,4 @@ The numeric representation of the remote port. For example, `443`. [secureConnection]: #tls_event_secureconnection [Stream]: stream.html#stream_stream [tls.Server]: #tls_class_tls_server +[SSL_CTX_set_timeout]: http://www.openssl.org/docs/ssl/SSL_CTX_set_timeout.html diff --git a/lib/tls.js b/lib/tls.js index df2afec5333..334f4116ba1 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -990,6 +990,7 @@ SecurePair.prototype.error = function() { // - key. string. // - cert: string. // - ca: string or array of strings. +// - sessionTimeout: integer. // // emit 'secureConnection' // function (cleartextStream, encryptedStream) { } @@ -1058,6 +1059,10 @@ function Server(/* [options], listener */) { throw new TypeError('handshakeTimeout must be a number'); } + if (self.sessionTimeout) { + sharedCreds.context.setSessionTimeout(self.sessionTimeout); + } + // constructor call net.Server.call(this, function(socket) { var creds = crypto.createCredentials(null, sharedCreds.context); @@ -1154,6 +1159,7 @@ Server.prototype.setOptions = function(options) { if (options.secureProtocol) this.secureProtocol = options.secureProtocol; if (options.crl) this.crl = options.crl; if (options.ciphers) this.ciphers = options.ciphers; + if (options.sessionTimeout) this.sessionTimeout = options.sessionTimeout; var secureOptions = options.secureOptions || 0; if (options.honorCipherOrder) { secureOptions |= constants.SSL_OP_CIPHER_SERVER_PREFERENCE; diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 21541f34dee..5adb7e56949 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -157,6 +157,8 @@ void SecureContext::Initialize(Handle target) { NODE_SET_PROTOTYPE_METHOD(t, "setOptions", SecureContext::SetOptions); NODE_SET_PROTOTYPE_METHOD(t, "setSessionIdContext", SecureContext::SetSessionIdContext); + NODE_SET_PROTOTYPE_METHOD(t, "setSessionTimeout", + SecureContext::SetSessionTimeout); NODE_SET_PROTOTYPE_METHOD(t, "close", SecureContext::Close); NODE_SET_PROTOTYPE_METHOD(t, "loadPKCS12", SecureContext::LoadPKCS12); @@ -634,6 +636,21 @@ Handle SecureContext::SetSessionIdContext(const Arguments& args) { return True(node_isolate); } +Handle SecureContext::SetSessionTimeout(const Arguments& args) { + HandleScope scope; + + SecureContext *sc = ObjectWrap::Unwrap(args.Holder()); + + if (args.Length() != 1 || !args[0]->IsInt32()) { + return ThrowTypeError("Bad parameter"); + } + + int32_t sessionTimeout = args[0]->Int32Value(); + SSL_CTX_set_timeout(sc->ctx_, sessionTimeout); + + return True(); +} + Handle SecureContext::Close(const Arguments& args) { HandleScope scope(node_isolate); SecureContext *sc = ObjectWrap::Unwrap(args.Holder()); diff --git a/src/node_crypto.h b/src/node_crypto.h index 0ceb1f69c97..74e07c227d5 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -73,6 +73,7 @@ class SecureContext : ObjectWrap { static v8::Handle SetCiphers(const v8::Arguments& args); static v8::Handle SetOptions(const v8::Arguments& args); static v8::Handle SetSessionIdContext(const v8::Arguments& args); + static v8::Handle SetSessionTimeout(const v8::Arguments& args); static v8::Handle Close(const v8::Arguments& args); static v8::Handle LoadPKCS12(const v8::Arguments& args); diff --git a/test/fixtures/tls-session-ticket.txt b/test/fixtures/tls-session-ticket.txt new file mode 100644 index 00000000000..bc0f6b58e1c --- /dev/null +++ b/test/fixtures/tls-session-ticket.txt @@ -0,0 +1,23 @@ +-----BEGIN SSL SESSION PARAMETERS----- +MIID2wIBAQICAwEEAgA1BCAMjLe+70uBSPGvybkTnPVUMwdbdtVbkMIXf8L5M8Kl +VAQwog+Afs00cnYUcgD1BQewJyxX1e561oRuDTpy7BHABC1hC7hxTaul+pwv+cBx +8D72oQYCBFFQF3OiBAICASyjggNhMIIDXTCCAkWgAwIBAgIJAMUSOvlaeyQHMA0G +CSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRl +MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTAxMTE2MDkz +MjQ5WhcNMTMxMTE1MDkzMjQ5WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29t +ZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+LXZOjcQCJq3+ZKUFabj71oo/ex +/XsBcFqtBThjjTw9CVEVwfPQQp4XwtPiB204vnYXwQ1/R2NdTQqCZu47l79LssL/ +u2a5Y9+0NEU3nQA5qdt+1FAE0c5oexPimXOrR3GWfKz7PmZ2O0117IeCUUXPG5U8 +umhDe/4mDF4ZNJiKc404WthquTqgS7rLQZHhZ6D0EnGnOkzlmxJMYPNHSOY1/6iv +dNUUcC87awNEA3lgfhy25IyBK3QJc+aYKNTbt70Lery3bu2wWLFGtmNiGlQTS4Js +xImRsECTI727ObS7/FWAQsqW+COL0Sa5BuMFrFIpjPrEe0ih7vRRbdmXRwIDAQAB +o1AwTjAdBgNVHQ4EFgQUDnV4d6mDtOnluLoCjkUHTX/n4agwHwYDVR0jBBgwFoAU +DnV4d6mDtOnluLoCjkUHTX/n4agwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUF +AAOCAQEAFwV4MQfTo+qMv9JMiynoIEiqfOz4RgtmBqRnXUffcjS2dhc7/z+FPZnM +79Kej8eLHoVfxCyWRHFlzm93vEdvwxOCrD13EDOi08OOZfxWyIlCa6Bg8cMAKqQz +d2OvQOWqlRWBTThBJIhWflU33izXQn5GdmYqhfpc+9ZHHGhvXNydtRQkdxVK2dZN +zLBvBlLlRmtoClU7xm3A+/5dddePAQHEPtyFlUw49VYtZ3ru6KqPms7MKvcRhYLs +y9rwSfuuniMlx4d0bDR7TOkw0QQSA0N8MGQRQpzl4mw4jLzyM5d5QtuGBh2P6hPG +a0YQxtI3RPT/p6ENzzBiAKXiSfzox6QCBAClAwIBEg== +-----END SSL SESSION PARAMETERS----- diff --git a/test/pummel/test-tls-session-timeout.js b/test/pummel/test-tls-session-timeout.js new file mode 100644 index 00000000000..47c5f977f00 --- /dev/null +++ b/test/pummel/test-tls-session-timeout.js @@ -0,0 +1,133 @@ +// 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); +} +require('child_process').exec('openssl version', function(err) { + if (err !== null) { + console.error('Skipping because openssl command is not available.'); + process.exit(0); + } + doTest(); +}); + +// This test consists of three TLS requests -- +// * The first one should result in a new connection because we don't have +// a valid session ticket. +// * The second one should result in connection resumption because we used +// the session ticket we saved from the first connection. +// * The third one should result in a new connection because the ticket +// that we used has expired by now. + +function doTest() { + var common = require('../common'); + var assert = require('assert'); + var tls = require('tls'); + var fs = require('fs'); + var join = require('path').join; + var spawn = require('child_process').spawn; + + var SESSION_TIMEOUT = 1; + + var keyFile = join(common.fixturesDir, 'agent.key'); + var certFile = join(common.fixturesDir, 'agent.crt'); + var key = fs.readFileSync(keyFile); + var cert = fs.readFileSync(certFile); + var options = { + key: key, + cert: cert, + ca: [cert], + sessionTimeout: SESSION_TIMEOUT + }; + + // We need to store a sample session ticket in the fixtures directory because + // `s_client` behaves incorrectly if we do not pass in both the `-sess_in` + // and the `-sess_out` flags, and the `-sess_in` argument must point to a + // file containing a proper serialization of a session ticket. + // To avoid a source control diff, we copy the ticket to a temporary file. + + var sessionFileName = (function () { + var ticketFileName = 'tls-session-ticket.txt'; + var fixturesPath = join(common.fixturesDir, ticketFileName); + var tmpPath = join(common.tmpDir, ticketFileName); + fs.writeFileSync(tmpPath, fs.readFileSync(fixturesPath)); + return tmpPath; + }()); + + // Expects a callback -- cb(connectionType : enum ['New'|'Reused']) + + var Client = function (cb) { + var flags = [ + 's_client', + '-connect', 'localhost:' + common.PORT, + '-sess_in', sessionFileName, + '-sess_out', sessionFileName + ]; + var client = spawn('openssl', flags, { + stdio: ['ignore', 'pipe', 'ignore'] + }); + + var clientOutput = ''; + client.stdout.on('data', function(data) { + clientOutput += data.toString(); + }); + client.on('exit', function(code) { + var connectionType; + var grepConnectionType = function (line) { + var matches = line.match(/(New|Reused), /); + if (matches) { + connectionType = matches[1]; + return true; + } + }; + var lines = clientOutput.split('\n'); + if (!lines.some(grepConnectionType)) { + throw new Error('unexpected output from openssl client'); + } + cb(connectionType); + }); + }; + + var server = tls.createServer(options, function(cleartext) { + cleartext.on('error', function(er) { + if (er.code !== 'ECONNRESET') + throw er; + }); + cleartext.end(); + }); + + server.listen(common.PORT, function() { + Client(function(connectionType) { + assert(connectionType === 'New'); + Client(function(connectionType) { + assert(connectionType === 'Reused'); + setTimeout(function () { + Client(function(connectionType) { + assert(connectionType === 'New'); + server.close(); + }); + }, (SESSION_TIMEOUT + 1) * 1000); + }); + }); + }); +}