From fe2e8a4a24c01b9d67806e82a2dbfd28cfc04f6f Mon Sep 17 00:00:00 2001 From: Timothy J Fontaine Date: Mon, 20 Oct 2014 17:43:37 -0700 Subject: [PATCH 1/4] 2014.10.20, Version 0.10.33 (Stable) * openssl: Update to 1.0.1j (Addressing multiple CVEs) * uv: Update to v0.10.29 * child_process: properly support optional args (cjihrig) * crypto: Disable autonegotiation for SSLv2/3 by default (Fedor Indutny, Timothy J Fontaine, Alexis Campailla) This is a behavior change, by default we will not allow the negotiation to SSLv2 or SSLv3. If you want this behavior, run Node.js with either `--enable-ssl2` or `--enable-ssl3` respectively. This does not change the behavior for users specifically requesting `SSLv2_method` or `SSLv3_method`. While this behavior is not advised, it is assumed you know what you're doing since you're specifically asking to use these methods. --- AUTHORS | 3 +++ ChangeLog | 23 ++++++++++++++++++++++- src/node_version.h | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 3af7fc63a2..b51cd7160b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -515,3 +515,6 @@ Kevin Simper Jackson Tian Tristan Berger Mathias Schreck +Calvin Metcalf +Matthew Fitzsimmons +Swaagie diff --git a/ChangeLog b/ChangeLog index 73f42e0dd0..185b0d30eb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,25 @@ -2014.09.16, Version 0.10.32 (Stable) +2014.10.20, Version 0.10.33 (Stable) + +* openssl: Update to 1.0.1j (Addressing multiple CVEs) + +* uv: Update to v0.10.29 + +* child_process: properly support optional args (cjihrig) + +* crypto: Disable autonegotiation for SSLv2/3 by default (Fedor Indutny, + Timothy J Fontaine, Alexis Campailla) + + This is a behavior change, by default we will not allow the negotiation to + SSLv2 or SSLv3. If you want this behavior, run Node.js with either + `--enable-ssl2` or `--enable-ssl3` respectively. + + This does not change the behavior for users specifically requesting + `SSLv2_method` or `SSLv3_method`. While this behavior is not advised, it is + assumed you know what you're doing since you're specifically asking to use + these methods. + + +2014.09.16, Version 0.10.32 (Stable), 0fe0d121551593c23a565db8397f85f17bb0f00e * npm: Update to 1.4.28 diff --git a/src/node_version.h b/src/node_version.h index 3971158985..5d1c54ae7b 100644 --- a/src/node_version.h +++ b/src/node_version.h @@ -26,7 +26,7 @@ #define NODE_MINOR_VERSION 10 #define NODE_PATCH_VERSION 33 -#define NODE_VERSION_IS_RELEASE 0 +#define NODE_VERSION_IS_RELEASE 1 #ifndef NODE_TAG # define NODE_TAG "" From b9283cf9d17a51f9654b438216ecb743ed69a7ce Mon Sep 17 00:00:00 2001 From: Timothy J Fontaine Date: Wed, 22 Oct 2014 10:27:56 -0700 Subject: [PATCH 2/4] tls: honorCipherOrder should not degrade defaults Specifying honorCipherOrder should not change the SSLv2/SSLv3 defaults for a TLS server. Use secureOptions logic in both lib/tls.js and lib/crypto.js --- lib/crypto.js | 44 +++--- lib/tls.js | 12 +- ...test-tls-honorcipherorder-secureOptions.js | 131 ++++++++++++++++++ 3 files changed, 167 insertions(+), 20 deletions(-) create mode 100644 test/simple/test-tls-honorcipherorder-secureOptions.js diff --git a/lib/crypto.js b/lib/crypto.js index f88c55d0a2..597d196f2f 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -61,6 +61,31 @@ var StringDecoder = require('string_decoder').StringDecoder; var CONTEXT_DEFAULT_OPTIONS = undefined; +function getSecureOptions(secureProtocol, secureOptions) { + if (CONTEXT_DEFAULT_OPTIONS === undefined) { + CONTEXT_DEFAULT_OPTIONS = 0; + + if (!binding.SSL3_ENABLE) + CONTEXT_DEFAULT_OPTIONS |= constants.SSL_OP_NO_SSLv3; + + if (!binding.SSL2_ENABLE) + CONTEXT_DEFAULT_OPTIONS |= constants.SSL_OP_NO_SSLv2; + } + + if (secureOptions === undefined) { + if (secureProtocol === undefined || + secureProtocol === 'SSLv23_method' || + secureProtocol === 'SSLv23_server_method' || + secureProtocol === 'SSLv23_client_method') { + secureOptions |= CONTEXT_DEFAULT_OPTIONS; + } + } + + return secureOptions; +} +exports._getSecureOptions = getSecureOptions; + + function Credentials(secureProtocol, flags, context) { if (!(this instanceof Credentials)) { return new Credentials(secureProtocol, flags, context); @@ -82,24 +107,7 @@ function Credentials(secureProtocol, flags, context) { } } - if (CONTEXT_DEFAULT_OPTIONS === undefined) { - CONTEXT_DEFAULT_OPTIONS = 0; - - if (!binding.SSL3_ENABLE) - CONTEXT_DEFAULT_OPTIONS |= constants.SSL_OP_NO_SSLv3; - - if (!binding.SSL2_ENABLE) - CONTEXT_DEFAULT_OPTIONS |= constants.SSL_OP_NO_SSLv2; - } - - if (flags === undefined) { - if (secureProtocol === undefined || - secureProtocol === 'SSLv23_method' || - secureProtocol === 'SSLv23_server_method' || - secureProtocol === 'SSLv23_client_method') { - flags |= CONTEXT_DEFAULT_OPTIONS; - } - } + flags = getSecureOptions(secureProtocol, flags); this.context.setOptions(flags); } diff --git a/lib/tls.js b/lib/tls.js index 392f7ad2ba..adc8efa634 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -1239,11 +1239,16 @@ 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; - var secureOptions = options.secureOptions || 0; + + var secureOptions = crypto._getSecureOptions(options.secureProtocol, + options.secureOptions); + if (options.honorCipherOrder) { secureOptions |= constants.SSL_OP_CIPHER_SERVER_PREFERENCE; } - if (secureOptions) this.secureOptions = secureOptions; + + this.secureOptions = secureOptions; + if (options.NPNProtocols) convertNPNProtocols(options.NPNProtocols, this); if (options.SNICallback) { this.SNICallback = options.SNICallback; @@ -1326,6 +1331,9 @@ exports.connect = function(/* [port, host], options, cb */) { }; options = util._extend(defaults, options || {}); + options.secureOptions = crypto._getSecureOptions(options.secureProtocol, + options.secureOptions); + var socket = options.socket ? options.socket : new net.Stream(); var sslcontext = crypto.createCredentials(options); diff --git a/test/simple/test-tls-honorcipherorder-secureOptions.js b/test/simple/test-tls-honorcipherorder-secureOptions.js new file mode 100644 index 0000000000..e70cfb1ef4 --- /dev/null +++ b/test/simple/test-tls-honorcipherorder-secureOptions.js @@ -0,0 +1,131 @@ +// 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. + +var common = require('../common'); +var assert = require('assert'); +var tls = require('tls'); +var fs = require('fs'); +var nconns = 0; +var SSL_Method = 'SSLv23_method'; +var localhost = '127.0.0.1'; +var opCipher = process.binding('constants').SSL_OP_CIPHER_SERVER_PREFERENCE; + +/* + * This test is to make sure we are preserving secureOptions that are passed + * to the server. + * + * Also that if honorCipherOrder is passed we are preserving that in the + * options. + * + * And that if we are passing in secureOptions no new options (aside from the + * honorCipherOrder case) are added to the secureOptions + */ + + +process.on('exit', function() { + assert.equal(nconns, 6); +}); + +function test(honorCipherOrder, clientCipher, expectedCipher, secureOptions, cb) { + var soptions = { + secureProtocol: SSL_Method, + key: fs.readFileSync(common.fixturesDir + '/keys/agent2-key.pem'), + cert: fs.readFileSync(common.fixturesDir + '/keys/agent2-cert.pem'), + ciphers: 'AES256-SHA:RC4-SHA:DES-CBC-SHA', + secureOptions: secureOptions, + honorCipherOrder: !!honorCipherOrder + }; + + var server = tls.createServer(soptions, function(cleartextStream) { + nconns++; + }); + + if (!!honorCipherOrder) { + assert.strictEqual(server.secureOptions & opCipher, opCipher, 'we should preserve cipher preference'); + } + + if (secureOptions) { + var expectedSecureOpts = secureOptions; + if (!!honorCipherOrder) expectedSecureOpts |= opCipher; + + assert.strictEqual(server.secureOptions & expectedSecureOpts, + expectedSecureOpts, 'we should preserve secureOptions'); + assert.strictEqual(server.secureOptions & ~expectedSecureOpts, + 0, + 'we should not add extra options'); + } + + server.listen(common.PORT, localhost, function() { + var coptions = { + rejectUnauthorized: false, + secureProtocol: SSL_Method + }; + if (clientCipher) { + coptions.ciphers = clientCipher; + } + var client = tls.connect(common.PORT, localhost, coptions, function() { + var cipher = client.getCipher(); + client.end(); + server.close(); + assert.equal(cipher.name, expectedCipher); + if (cb) cb(); + }); + }); +} + +test1(); + +function test1() { + // Client has the preference of cipher suites by default + test(false, 'DES-CBC-SHA:RC4-SHA:AES256-SHA','DES-CBC-SHA', 0, test2); +} + +function test2() { + // Server has the preference of cipher suites where AES256-SHA is in + // the first. + test(true, 'DES-CBC-SHA:RC4-SHA:AES256-SHA', 'AES256-SHA', 0, test3); +} + +function test3() { + // Server has the preference of cipher suites. RC4-SHA is given + // higher priority over DES-CBC-SHA among client cipher suites. + test(true, 'DES-CBC-SHA:RC4-SHA', 'RC4-SHA', 0, test4); +} + +function test4() { + // As client has only one cipher, server has no choice in regardless + // of honorCipherOrder. + test(true, 'DES-CBC-SHA', 'DES-CBC-SHA', 0, test5); +} + +function test5() { + test(false, + 'DES-CBC-SHA', + 'DES-CBC-SHA', + process.binding('constants').SSL_OP_SINGLE_DH_USE, test6); +} + +function test6() { + test(true, + 'DES-CBC-SHA', + 'DES-CBC-SHA', + process.binding('constants').SSL_OP_SINGLE_DH_USE); +} From 69080f5474369fc7fc4be7ab74ad2e1619eb2fbc Mon Sep 17 00:00:00 2001 From: Timothy J Fontaine Date: Wed, 22 Oct 2014 12:14:10 -0700 Subject: [PATCH 3/4] tls: enforce secureOptions on incoming clients Reuse the secureProtocol and secureOptions of the server when creating the secure context for incoming clients. --- lib/tls.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/tls.js b/lib/tls.js index adc8efa634..77a7089218 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -1145,7 +1145,12 @@ function Server(/* [options], listener */) { // constructor call net.Server.call(this, function(socket) { - var creds = crypto.createCredentials(null, sharedCreds.context); + var connOps = { + secureProtocol: self.secureProtocol, + secureOptions: self.secureOptions + }; + + var creds = crypto.createCredentials(connOps, sharedCreds.context); var pair = new SecurePair(creds, true, From 8d045a30e95602b443eb259a5021d33feb4df079 Mon Sep 17 00:00:00 2001 From: Julien Gilli Date: Wed, 22 Oct 2014 17:58:16 -0700 Subject: [PATCH 4/4] tests: add TLS tests matrix Add a test that goes through the whole matrix of: - command line options (--enable-ssl*) - secureOptions - secureProtocols and makes sure that compatible test setups actually work as expected. The test works by spawning two processes for each test case: one client and one server. The test passes if a SSL/TLS connection from the client to the server is successful and the test case was supposed to pass, or if the connection couldn't be established and the test case was supposed to fail. The test is currently located in the directory 'test/external' because it has external dependencies. --- test/external/ssl-options/.gitignore | 1 + test/external/ssl-options/package.json | 15 + test/external/ssl-options/test.js | 729 +++++++++++++++++++++++++ 3 files changed, 745 insertions(+) create mode 100644 test/external/ssl-options/.gitignore create mode 100644 test/external/ssl-options/package.json create mode 100644 test/external/ssl-options/test.js diff --git a/test/external/ssl-options/.gitignore b/test/external/ssl-options/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/test/external/ssl-options/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/test/external/ssl-options/package.json b/test/external/ssl-options/package.json new file mode 100644 index 0000000000..114dce6afb --- /dev/null +++ b/test/external/ssl-options/package.json @@ -0,0 +1,15 @@ +{ + "name": "ssl-options-tests", + "version": "1.0.0", + "description": "", + "main": "test.js", + "scripts": { + "test": "node test.js" + }, + "author": "", + "license": "MIT", + "dependencies": { + "async": "^0.9.0", + "debug": "^2.1.0" + } +} diff --git a/test/external/ssl-options/test.js b/test/external/ssl-options/test.js new file mode 100644 index 0000000000..f7e06c93df --- /dev/null +++ b/test/external/ssl-options/test.js @@ -0,0 +1,729 @@ +var tls = require('tls'); +var fs = require('fs'); +var path = require('path'); +var fork = require('child_process').fork; +var assert = require('assert'); +var constants = require('constants'); +var os = require('os'); + +var async = require('async'); +var debug = require('debug')('test-node-ssl'); + +var common = require('../../common'); + +var SSL2_COMPATIBLE_CIPHERS = 'RC4-MD5'; + +var CMD_LINE_OPTIONS = [ null, "--enable-ssl2", "--enable-ssl3" ]; + +var SERVER_SSL_PROTOCOLS = [ + null, + 'SSLv2_method', 'SSLv2_server_method', + 'SSLv3_method', 'SSLv3_server_method', + 'TLSv1_method', 'TLSv1_server_method', + 'SSLv23_method','SSLv23_server_method' +]; + +var CLIENT_SSL_PROTOCOLS = [ + null, + 'SSLv2_method', 'SSLv2_client_method', + 'SSLv3_method', 'SSLv3_client_method', + 'TLSv1_method', 'TLSv1_client_method', + 'SSLv23_method','SSLv23_client_method' +]; + +var SECURE_OPTIONS = [ + null, + 0, + constants.SSL_OP_NO_SSLv2, + constants.SSL_OP_NO_SSLv3, + constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 +]; + +function xtend(source) { + var clone = {}; + + for (var property in source) { + if (source.hasOwnProperty(property)) { + clone[property] = source[property]; + } + } + + return clone; +} + +function isAutoNegotiationProtocol(sslProtocol) { + assert(sslProtocol === null || typeof sslProtocol === 'string'); + + return sslProtocol == null || + sslProtocol === 'SSLv23_method' || + sslProtocol === 'SSLv23_client_method' || + sslProtocol === 'SSLv23_server_method'; +} + +function isSameSslProtocolVersion(serverSecureProtocol, clientSecureProtocol) { + assert(serverSecureProtocol === null || typeof serverSecureProtocol === 'string'); + assert(clientSecureProtocol === null || typeof clientSecureProtocol === 'string'); + + if (serverSecureProtocol === clientSecureProtocol) { + return true; + } + + var serverProtocolPrefix = ''; + if (serverSecureProtocol) + serverProtocolPrefix = serverSecureProtocol.split('_')[0]; + + var clientProtocolPrefix = ''; + if (clientSecureProtocol) + clientProtocolPrefix = clientSecureProtocol.split('_')[0]; + + if (serverProtocolPrefix === clientProtocolPrefix) { + return true; + } + + return false; +} + +function secureProtocolsCompatible(serverSecureProtocol, clientSecureProtocol) { + if (isAutoNegotiationProtocol(serverSecureProtocol) || + isAutoNegotiationProtocol(clientSecureProtocol)) { + return true; + } + + if (isSameSslProtocolVersion(serverSecureProtocol, + clientSecureProtocol)) { + return true; + } + + return false; +} + +function isSsl3Protocol(secureProtocol) { + assert(secureProtocol === null || typeof secureProtocol === 'string'); + + return secureProtocol === 'SSLv3_method' || + secureProtocol === 'SSLv3_client_method' || + secureProtocol === 'SSLv3_server_method'; +} + +function isSsl2Protocol(secureProtocol) { + assert(secureProtocol === null || typeof secureProtocol === 'string'); + + return secureProtocol === 'SSLv2_method' || + secureProtocol === 'SSLv2_client_method' || + secureProtocol === 'SSLv2_server_method'; +} + +function secureProtocolCompatibleWithSecureOptions(secureProtocol, secureOptions, cmdLineOption) { + if (secureOptions == null) { + if (isSsl2Protocol(secureProtocol) && + (!cmdLineOption || cmdLineOption.indexOf('--enable-ssl2') === -1)) { + return false; + } + + if (isSsl3Protocol(secureProtocol) && + (!cmdLineOption || cmdLineOption.indexOf('--enable-ssl3') === -1)) { + return false; + } + } else { + if (secureOptions & constants.SSL_OP_NO_SSLv2 && isSsl2Protocol(secureProtocol)) { + return false; + } + + if (secureOptions & constants.SSL_OP_NO_SSLv3 && isSsl3Protocol(secureProtocol)) { + return false; + } + } + + return true; +} + +function testSetupsCompatible(serverSetup, clientSetup) { + debug('Determing test result for:'); + debug(serverSetup); + debug(clientSetup); + + /* + * If the protocols specified by the client and server are + * not compatible (e.g SSLv2 vs SSLv3), then the test should fail. + */ + if (!secureProtocolsCompatible(serverSetup.secureProtocol, + clientSetup.secureProtocol)) { + debug('secureProtocols not compatible! server secureProtocol: ' + + serverSetup.secureProtocol + ', client secureProtocol: ' + + clientSetup.secureProtocol); + return false; + } + + /* + * If the client's options are not compatible with the server's protocol, + * then the test should fail. Same if server's options are not compatible + * with the client's protocol. + */ + if (!secureProtocolCompatibleWithSecureOptions(serverSetup.secureProtocol, + clientSetup.secureOptions, + clientSetup.cmdLine) || + !secureProtocolCompatibleWithSecureOptions(clientSetup.secureProtocol, + serverSetup.secureOptions, + serverSetup.cmdLine)) { + debug('Secure protocol not compatible with secure options!'); + return false; + } + + if (isSsl2Protocol(serverSetup.secureProtocol) || + isSsl2Protocol(clientSetup.secureProtocol)) { + + /* + * It seems that in order to be able to use SSLv2, at least the server + * *needs* to advertise at least one cipher compatible with it. + */ + if (serverSetup.ciphers !== SSL2_COMPATIBLE_CIPHERS) { + return false; + } + + /* + * If only either one of the client or server specify SSLv2 as their + * protocol, then *both* of them *need* to advertise at least one cipher + * that is compatible with SSLv2. + */ + if ((!isSsl2Protocol(serverSetup.secureProtocol) || !isSsl2Protocol(clientSetup.secureProtocol)) && + (clientSetup.ciphers !== SSL2_COMPATIBLE_CIPHERS || serverSetup.ciphers !== SSL2_COMPATIBLE_CIPHERS)) { + return false; + } + } + + return true; +} + +function sslSetupMakesSense(cmdLineOption, secureProtocol, secureOption) { + if (isSsl2Protocol(secureProtocol)) { + if (secureOption & constants.SSL_OP_NO_SSLv2 || + (secureOption == null && (!cmdLineOption || cmdLineOption.indexOf('--enable-ssl2') === -1))) { + return false; + } + } + + if (isSsl3Protocol(secureProtocol)) { + if (secureOption & constants.SSL_OP_NO_SSLv3 || + (secureOption == null && (!cmdLineOption || cmdLineOption.indexOf('--enable-ssl3') === -1))) { + return false; + } + } + + return true; +} + +function createTestsSetups() { + + var serversSetup = []; + var clientsSetup = []; + + CMD_LINE_OPTIONS.forEach(function (cmdLineOption) { + SERVER_SSL_PROTOCOLS.forEach(function (serverSecureProtocol) { + SECURE_OPTIONS.forEach(function (secureOption) { + if (sslSetupMakesSense(cmdLineOption, + serverSecureProtocol, + secureOption)) { + var serverSetup = { + cmdLine: cmdLineOption, + secureProtocol: serverSecureProtocol, + secureOptions: secureOption + }; + + serversSetup.push(serverSetup); + + if (isSsl2Protocol(serverSecureProtocol)) { + var setupWithSsl2Ciphers = xtend(serverSetup); + setupWithSsl2Ciphers.ciphers = SSL2_COMPATIBLE_CIPHERS; + serversSetup.push(setupWithSsl2Ciphers); + } + } + }); + }); + + CLIENT_SSL_PROTOCOLS.forEach(function (clientSecureProtocol) { + SECURE_OPTIONS.forEach(function (secureOption) { + if (sslSetupMakesSense(cmdLineOption, + clientSecureProtocol, + secureOption)) { + var clientSetup = { + cmdLine: cmdLineOption, + secureProtocol: clientSecureProtocol, + secureOptions: secureOption + }; + + clientsSetup.push(clientSetup); + + if (isSsl2Protocol(clientSecureProtocol)) { + var setupWithSsl2Ciphers = xtend(clientSetup); + setupWithSsl2Ciphers.ciphers = SSL2_COMPATIBLE_CIPHERS; + clientsSetup.push(setupWithSsl2Ciphers); + } + } + }); + }); + }); + + var testSetups = []; + var testId = 0; + serversSetup.forEach(function (serverSetup) { + clientsSetup.forEach(function (clientSetup) { + var testSetup = { + server: serverSetup, + client: clientSetup, + ID: testId++ + }; + + var successExpected = false; + if (testSetupsCompatible(serverSetup, clientSetup)) { + successExpected = true; + } + testSetup.successExpected = successExpected; + + testSetups.push(testSetup); + }); + }); + + return testSetups; +} + +function runServer(port, secureProtocol, secureOptions, ciphers) { + debug('Running server!'); + debug('port: ' + port); + debug('secureProtocol: ' + secureProtocol); + debug('secureOptions: ' + secureOptions); + debug('ciphers: ' + ciphers); + + var keyPath = path.join(common.fixturesDir, 'agent.key'); + var certPath = path.join(common.fixturesDir, 'agent.crt'); + + var key = fs.readFileSync(keyPath).toString(); + var cert = fs.readFileSync(certPath).toString(); + + var server = new tls.Server({ key: key, + cert: cert, + ca: [], + ciphers: ciphers, + secureProtocol: secureProtocol, + secureOptions: secureOptions + }); + + server.listen(port, function() { + process.on('message', function onChildMsg(msg) { + if (msg === 'close') { + server.close(); + process.exit(0); + } + }); + + process.send('server_listening'); + }); + + server.on('error', function onServerError(err) { + debug('Server error: ' + err); + process.exit(1); + }); + + server.on('clientError', function onClientError(err) { + debug('Client error on server: ' + err); + process.exit(1); + }); +} + +function runClient(port, secureProtocol, secureOptions, ciphers) { + debug('Running client!'); + debug('port: ' + port); + debug('secureProtocol: ' + secureProtocol); + debug('secureOptions: ' + secureOptions); + debug('ciphers: ' + ciphers); + + var con = tls.connect(port, + { + rejectUnauthorized: false, + secureProtocol: secureProtocol, + secureOptions: secureOptions + }, + function() { + + // TODO jgilli: test that sslProtocolUsed is at least as "secure" as + // "secureProtocol" + /* + * var sslProtocolUsed = con.getVersion(); + * debug('Protocol used: ' + sslProtocolUsed); + */ + + process.send('client_done'); + }); + + con.on('error', function(err) { + debug('Client could not connect:' + err); + process.exit(1); + }); +} + +function stringToSecureOptions(secureOptionsString) { + assert(typeof secureOptionsString === 'string'); + + var secureOptions; + + var optionStrings = secureOptionsString.split('|'); + optionStrings.forEach(function (option) { + if (option === 'SSL_OP_NO_SSLv2') { + secureOptions |= constants.SSL_OP_NO_SSLv2; + } + + if (option === 'SSL_OP_NO_SSLv3') { + secureOptions |= constants.SSL_OP_NO_SSLv3; + } + + if (option === '0') { + secureOptions = 0; + } + }); + + return secureOptions; +} + +function processTestCmdLineOptions(argv){ + var options = {}; + + argv.forEach(function (arg) { + var key; + var value; + + var keyValue = arg.split(':'); + var key = keyValue[0]; + + if (keyValue.length == 2 && keyValue[1].length > 0) { + value = keyValue[1]; + + if (key === 'secureOptions') { + value = stringToSecureOptions(value); + } + + if (key === 'port') { + value = +value; + } + } + + options[key] = value; + }); + + return options; +} + +function checkTestExitCode(testSetup, serverExitCode, clientExitCode) { + if (testSetup.successExpected) { + if (serverExitCode === 0 && clientExitCode === 0) { + debug('Test succeeded as expected!'); + return true; + } + } else { + if (serverExitCode !== 0 || clientExitCode !== 0) { + debug('Test failed as expected!'); + return true; + } + } + + return false; +} + +function secureOptionsToString(secureOptions) { + var secureOptsString = ''; + + if (secureOptions & constants.SSL_OP_NO_SSLv2) { + secureOptsString += 'SSL_OP_NO_SSLv2'; + } + + if (secureOptions & constants.SSL_OP_NO_SSLv3) { + secureOptsString += '|SSL_OP_NO_SSLv3'; + } + + if (secureOptions === 0) { + secureOptsString = '0'; + } + + return secureOptsString; +} + +function forkTestProcess(processType, testSetup, port) { + var argv = [ processType ]; + + if (testSetup.secureProtocol) { + argv.push('secureProtocol:' + testSetup.secureProtocol); + } else { + argv.push('secureProtocol:'); + } + + argv.push('secureOptions:' + secureOptionsToString(testSetup.secureOptions)); + + if (testSetup.ciphers) { + argv.push('ciphers:' + testSetup.ciphers); + } else { + argv.push('ciphers:'); + } + + argv.push('port:' + port); + + var forkOptions; + if (testSetup.cmdLine) { + forkOptions = { + execArgv: [ testSetup.cmdLine ] + } + } + + return fork(process.argv[1], + argv, + forkOptions); +} + +function runTest(testSetup, testDone) { + var clientSetup = testSetup.client; + var serverSetup = testSetup.server; + + assert(clientSetup); + assert(serverSetup); + + debug('Starting new test on port: ' + testSetup.port); + + debug('client setup:'); + debug(clientSetup); + + debug('server setup:'); + debug(serverSetup); + + debug('Success expected:' + testSetup.successExpected); + + var serverExitCode; + + var clientStarted = false; + var clientExitCode; + + var serverChild = forkTestProcess('server', serverSetup, testSetup.port); + assert(serverChild); + + serverChild.on('message', function onServerMsg(msg) { + if (msg === 'server_listening') { + debug('Starting client!'); + clientStarted = true; + + var clientChild = forkTestProcess('client', clientSetup, testSetup.port); + assert(clientChild); + + clientChild.on('exit', function onClientExited(exitCode) { + debug('Client exited with code:' + exitCode); + + clientExitCode = exitCode; + if (serverExitCode != null) { + var err; + if (!checkTestExitCode(testSetup, serverExitCode, clientExitCode)) + err = new Error("Test failed!"); + + return testDone(err); + } else { + if (serverChild.connected) { + serverChild.send('close'); + } + } + }); + + clientChild.on('message', function onClientMsg(msg) { + if (msg === 'client_done' && serverChild.connected) { + serverChild.send('close'); + } + }) + } + }); + + serverChild.on('exit', function onServerExited(exitCode) { + debug('Server exited with code:' + exitCode); + + serverExitCode = exitCode; + if (clientExitCode != null || !clientStarted) { + var err; + if (!checkTestExitCode(testSetup, serverExitCode, clientExitCode)) + err = new Error("Test failed!"); + + return testDone(err); + } + }); +} + +function usage() { + console.log('Usage: test-node-ssl [-j N] [--list-tests] [-s startIndex] ' + + '[-e endIndex] [-o outputFile]'); + process.exit(1); +} + +function processDriverCmdLineOptions(argv) { + var options = { + parallelTests: 1 + }; + + for (var i = 1; i < argv.length; ++i) { + if (argv[i] === '-j') { + + var nbParallelTests = +argv[i + 1]; + if (!nbParallelTests) { + usage(); + } else { + options.parallelTests = argv[++i]; + } + } + + if (argv[i] === '-s') { + var start = +argv[i + 1]; + if (!start) { + usage(); + } else { + options.start = argv[++i]; + } + } + + if (argv[i] === '-e') { + var end = +argv[i + 1]; + if (!end) { + usage(); + } else { + options.end = argv[++i]; + } + } + + if (argv[i] === '--list-tests') { + options.listTests = true; + } + + if (argv[i] === '-o') { + var outputFile = argv[i + 1]; + if (!outputFile) { + usage(); + } else { + options.outputFile = argv[++i]; + } + } + } + + return options; +} + +function outputTestResult(test, err, output) { + output.write(os.EOL); + output.write('Test:' + os.EOL); + output.write(JSON.stringify(test, null, " ")); + output.write(os.EOL); + output.write('Result:'); + output.write(err ? 'failure' : 'success'); + output.write(os.EOL); +} + +var agentType = process.argv[2]; +if (agentType === 'client' || agentType === 'server') { + var options = processTestCmdLineOptions(process.argv); + debug('secureProtocol: ' + options.secureProtocol); + debug('secureOptions: ' + options.secureOptions); + debug('ciphers:' + options.ciphers); + debug('port:' + options.port); + + if (agentType === 'client') { + runClient(options.port, + options.secureProtocol, + options.secureOptions, + options.ciphers); + } else if (agentType === 'server') { + runServer(options.port, + options.secureProtocol, + options.secureOptions, + options.ciphers); + } +} else { + var driverOptions = processDriverCmdLineOptions(process.argv); + debug('Tests driver options:'); + debug(driverOptions); + /* + * This is the tests driver process. + * + * It forks itself twice for each test. Each of the two forked processees are + * respectfully used as an SSL client and an SSL server. The client and + * server setup their SSL connection as generated by the "createTestsSetups" + * function. Once both processes have exited, the tests driver process + * compare both client and server exit codes with the expected test result + * of the test setup. If they match, the test is successful, otherwise it + * failed. + */ + + var testSetups = createTestsSetups(); + + if (driverOptions.listTests) { + console.log(testSetups); + process.exit(0); + } + + var testOutput = process.stdout; + if (driverOptions.outputFile) { + testOutput = fs.createWriteStream(driverOptions.outputFile) + .on('error', function onError(err) { + console.error(err); + process.exit(1); + }); + } + + debug('Tests setups:'); + debug('Number of tests: ' + testSetups.length); + debug(JSON.stringify(testSetups, null, " ")); + debug(); + + var nbTestsStarted = 0; + + function runTests(tests, callback) { + var nbTests = tests.length; + if (nbTests === 0) { + return callback(); + } + var error; + var nbTestsDone = 0; + + debug('Starting new batch of tests...'); + + var port = common.PORT; + async.each(tests, function (test, testDone) { + test.port = port++; + + ++nbTestsStarted; + debug('Starting test nb: ' + nbTestsStarted); + + runTest(test, function onTestDone(err) { + ++nbTestsDone; + if (err && error === undefined) { + error = new Error('Test with ID ' + test.ID + ' failed: ' + err); + } + + outputTestResult(test, err, testOutput); + + if (nbTestsDone === nbTests) + return testDone(error); + return testDone(); + }); + + }, function testsDone(err, results) { + if (err) { + assert(false, + "At least one test in the most recent batch failed: " + err); + } + + return callback(err); + }); + } + + function runAllTests(allTests, allTestsDone) { + if (allTests.length === 0) { + return allTestsDone(); + } + + return runTests(allTests.splice(0, driverOptions.parallelTests), + runAllTests.bind(global, allTests, allTestsDone)); + } + + runAllTests(testSetups.slice(driverOptions.start, driverOptions.end), + function allDone(err) { + console.log('All tests done!'); + }); +}