diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index fbd97e88a650..819ea727c934 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -109,6 +109,88 @@ handshake extensions allowing you: * SNI - to use one TLS server for multiple hostnames with different SSL certificates. +## Modifying the Default Cipher Suite + +Node.js is built with a default suite of enabled and disabled ciphers. +Currently, the default cipher suite is: + + ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:HIGH:!RC4:!MD5:!aNULL:!EDH + +This default can be overridden entirely using the `--cipher-list` command line +switch or `NODE_CIPHER_LIST` environment variable. For instance: + + node --cipher-list=ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384 + +Setting the environment variable would have the same effect: + + NODE_CIPHER_LIST=ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384 + +CAUTION: The default cipher suite has been carefully selected to reflect current +security best practices and risk mitigation. Changing the default cipher suite +can have a significant impact on the security of an application. The +`--cipher-list` and `NODE_CIPHER_LIST` options should only be used if +absolutely necessary. + +### Using Legacy Default Cipher Suite ### + +It is possible for the built-in default cipher suite to change from one release +of Node.js to another. For instance, v0.10.39 uses a different default than +v0.10.40. Such changes can cause issues with applications written to assume +certain specific defaults. To help buffer applications against such changes, +the `--enable-legacy-cipher-list` command line switch or `NODE_LEGACY_CIPHER_LIST` +environment variable can be set to specify a specific preset default: + + # Use the v0.10.40 defaults + node --enable-legacy-cipher-list=v0.10.40 + // or + NODE_LEGACY_CIPHER_LIST=v0.10.40 + +Currently, the values supported for the `enable-legacy-cipher-list` switch and +`NODE_LEGACY_CIPHER_LIST` environment variable include: + + v0.10.40 - To enable the default cipher suite used in v0.10.40 + + ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH + +These legacy cipher suites are also made available for use via the +`getLegacyCiphers()` method: + + var tls = require('tls'); + console.log(tls.getLegacyCiphers('v0.10.40')); + +CAUTION: Changes to the default cipher suite are typically made in order to +strengthen the default security for applications running within Node.js. +Reverting back to the defaults used by older releases can weaken the security +of your applications. The legacy cipher suites should only be used if absolutely +necessary. + +NOTE: Due to an error in Node.js v0.10.40, the default cipher list only applied +to servers using TLS. The default cipher list would _not_ be used by clients. +This behavior has been changed in v0.10.39 and the default cipher list is now +used by both the server and client when using TLS. However, when using +`--enable-legacy-cipher-list=v0.10.40`, Node.js is reverted back to the +v0.10.40 behavior of only using the default cipher list on the server. + +### Cipher List Precedence + +Note that the `--enable-legacy-cipher-list`, `NODE_LEGACY_CIPHER_LIST`, +`--cipher-list` and `NODE_CIPHER_LIST` options are mutually exclusive. + +If the `NODE_CIPHER_LIST` and `NODE_LEGACY_CIPHER_LIST` environment variables +are both specified, the `NODE_LEGACY_CIPHER_LIST` setting will take precedence. + +The `--cipher-list` and `--enable-legacy-cipher-list` command line options +will override the environment variables. If both happen to be specified, the +right-most (second one specified) will take precedence. For instance, in the +example: + + node --cipher-list=ABC --enable-legacy-cipher-list=v0.10.40 + +The v0.10.40 default cipher list will be used. + + node --enable-legacy-cipher-list=v0.10.40 --cipher-list=ABC + +The custom cipher list will be used. ## tls.getCiphers() @@ -120,6 +202,18 @@ Example: console.log(ciphers); // ['AES128-SHA', 'AES256-SHA', ...] +## tls.getLegacyCiphers(version) + +Returns a default cipher list used in a previous version of Node.js. The +version parameter must be a string whose value identifies previous Node.js +release version. The only value currently supported is `v0.10.40`. + +A TypeError will be thrown if: (a) the `version` is any type other than a +string, (b) the `version` parameter is not specified, or (c) additional +parameters are passed in. An Error will be thrown if the `version` parameter is +passed in as a string but the value does not correlate to any known Node.js +release for which a default cipher list is available. + ## tls.createServer(options, [secureConnectionListener]) Creates a new [tls.Server][]. The `connectionListener` argument is @@ -151,13 +245,13 @@ automatically set as a listener for the [secureConnection][] event. The conjunction with the `honorCipherOrder` option described below to prioritize the non-CBC cipher. - Defaults to `AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH`. + Defaults to `ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:HIGH:!RC4:!MD5:!aNULL:!EDH`. Consult the [OpenSSL cipher list format documentation] for details on the format. ECDH (Elliptic Curve Diffie-Hellman) ciphers are not yet supported. `AES128-GCM-SHA256` is used when node.js is linked against OpenSSL 1.0.1 - or newer and the client speaks TLS 1.2, RC4 is used as a secure fallback. + or newer and the client speaks TLS 1.2. **NOTE**: Previous revisions of this section suggested `AES256-SHA` as an acceptable cipher. Unfortunately, `AES256-SHA` is a CBC cipher and therefore @@ -333,7 +427,7 @@ Here is an example of a client of echo server as described previously: // These are necessary only if using the client certificate authentication key: fs.readFileSync('client-key.pem'), cert: fs.readFileSync('client-cert.pem'), - + // This is necessary only if the server uses the self-signed certificate ca: [ fs.readFileSync('server-cert.pem') ] }; @@ -525,7 +619,7 @@ A ClearTextStream is the `clear` member of a SecurePair object. ### Event: 'secureConnect' -This event is emitted after a new connection has been successfully handshaked. +This event is emitted after a new connection has been successfully handshaked. The listener will be called no matter if the server's certificate was authorized or not. It is up to the user to test `cleartextStream.authorized` to see if the server certificate was signed by one of the specified CAs. @@ -550,14 +644,14 @@ some properties corresponding to the field of the certificate. Example: - { subject: + { subject: { C: 'UK', ST: 'Acknack Ltd', L: 'Rhys Jones', O: 'node.js', OU: 'Test TLS Certificate', CN: 'localhost' }, - issuer: + issuer: { C: 'UK', ST: 'Acknack Ltd', L: 'Rhys Jones', diff --git a/lib/crypto.js b/lib/crypto.js index 597d196f2f28..f60b0b9cc7af 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -114,7 +114,6 @@ function Credentials(secureProtocol, flags, context) { exports.Credentials = Credentials; - exports.createCredentials = function(options, context) { if (!options) options = {}; @@ -134,7 +133,18 @@ exports.createCredentials = function(options, context) { if (options.cert) c.context.setCert(options.cert); - if (options.ciphers) c.context.setCiphers(options.ciphers); + if (options.ciphers) { + c.context.setCiphers(options.ciphers); + } else if (!(process._usingV1040Ciphers() && options.ciphers === undefined)) { + // Set the ciphers to the default ciphers list unless + // --enable-legacy-cipher-list=v0.10.40 was passed on the command line and + // no ciphers value was passed explicitly. In that case, we want to + // preserve the previous buggy behavior that existed in v0.10.x until + // v0.10.39, otherwise, a lot of client code might be broken. Server + // side TLS/HTTPS code always sets a default cipher list explicitly so it + // never reaches this, even for versions < v0.10.39. + c.context.setCiphers(binding.DEFAULT_CIPHER_LIST); + } if (options.ca) { if (Array.isArray(options.ca)) { diff --git a/lib/tls.js b/lib/tls.js index e3b90832236f..7fb858f1301a 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -19,6 +19,8 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +var _crypto = process.binding('crypto'); + var crypto = require('crypto'); var util = require('util'); var net = require('net'); @@ -31,8 +33,9 @@ var constants = require('constants'); var Timer = process.binding('timer_wrap').Timer; -var DEFAULT_CIPHERS = 'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:' + // TLS 1.2 - 'RC4:HIGH:!MD5:!aNULL:!EDH'; // TLS 1.0 +var DEFAULT_CIPHERS = _crypto.DEFAULT_CIPHER_LIST; + +exports.getLegacyCiphers = _crypto.getLegacyCiphers; // Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations // every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more @@ -44,7 +47,7 @@ exports.CLIENT_RENEG_WINDOW = 600; exports.SLAB_BUFFER_SIZE = 10 * 1024 * 1024; exports.getCiphers = function() { - var names = process.binding('crypto').getSSLCiphers(); + var names = _crypto.getSSLCiphers(); // Drop all-caps names in favor of their lowercase aliases, var ctx = {}; names.forEach(function(name) { @@ -65,7 +68,7 @@ if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) { var Connection = null; try { - Connection = process.binding('crypto').Connection; + Connection = _crypto.Connection; } catch (e) { throw new Error('node.js not compiled with openssl crypto support.'); } @@ -1335,6 +1338,16 @@ exports.connect = function(/* [port, host], options, cb */) { var defaults = { rejectUnauthorized: '0' !== process.env.NODE_TLS_REJECT_UNAUTHORIZED }; + if (!process._usingV1040Ciphers()) { + // only set the default ciphers if we are _not_ using the + // v0.10.40 legacy cipher list. Node v0.10.40 had a bug + // that failed to set the default ciphers on the default + // options. This has been fixed in v0.10.39 and above. + // However, when the user explicitly tells node to revert + // back to using the v0.10.40 cipher list, node should + // revert back to the original v0.10.40 behavior. + defaults.ciphers = DEFAULT_CIPHERS; + } options = util._extend(defaults, options || {}); options.secureOptions = crypto._getSecureOptions(options.secureProtocol, diff --git a/src/node.cc b/src/node.cc index e8ccb7276b87..d924abf04330 100644 --- a/src/node.cc +++ b/src/node.cc @@ -2566,6 +2566,8 @@ static void PrintHelp() { " --max-stack-size=val set max v8 stack size (bytes)\n" " --enable-ssl2 enable ssl2\n" " --enable-ssl3 enable ssl3\n" + " --cipher-list=val specify the default TLS cipher list\n" + " --enable-legacy-cipher-list=v0.10.40 \n" "\n" "Environment variables:\n" #ifdef _WIN32 @@ -2577,6 +2579,8 @@ static void PrintHelp() { "NODE_MODULE_CONTEXTS Set to 1 to load modules in their own\n" " global contexts.\n" "NODE_DISABLE_COLORS Set to 1 to disable colors in the REPL\n" + "NODE_CIPHER_LIST Override the default TLS cipher list\n" + "NODE_LEGACY_CIPHER_LIST=v0.10.40\n" "\n" "Documentation can be found at http://nodejs.org/\n"); } @@ -2652,6 +2656,18 @@ static void ParseArgs(int argc, char **argv) { } else if (strcmp(arg, "--throw-deprecation") == 0) { argv[i] = const_cast(""); throw_deprecation = true; + } else if (strncmp(arg, "--cipher-list=", 14) == 0) { + DEFAULT_CIPHER_LIST = arg + 14; + argv[i] = const_cast(""); + } else if (strncmp(arg, "--enable-legacy-cipher-list=", 28) == 0) { + const char * legacy_list = crypto::LegacyCipherList(arg+28); + if (legacy_list != NULL) { + DEFAULT_CIPHER_LIST = legacy_list; + } else { + fprintf(stderr, "Error: An unknown legacy cipher list was specified\n"); + exit(9); + } + argv[i] = const_cast(""); } else if (argv[i][0] != '-') { break; } @@ -2928,6 +2944,26 @@ char** Init(int argc, char *argv[]) { // Make inherited handles noninheritable. uv_disable_stdio_inheritance(); + // set the cipher list from the environment variable first, + // the command line switch will override if specified. + const char * cipher_list = getenv("NODE_CIPHER_LIST"); + if (cipher_list != NULL) { + DEFAULT_CIPHER_LIST = cipher_list; + } + + // Setting NODE_LEGACY_CIPHER_LIST will override the NODE_CIPHER_LIST + const char * leg_cipher_id = getenv("NODE_LEGACY_CIPHER_LIST"); + if (leg_cipher_id != NULL) { + const char * leg_cipher_list = + crypto::LegacyCipherList(leg_cipher_id); + if (leg_cipher_list != NULL) { + DEFAULT_CIPHER_LIST = leg_cipher_list; + } else { + fprintf(stderr, "Error: An unknown legacy cipher list was specified\n"); + exit(9); + } + } + // Parse a few arguments which are specific to Node. node::ParseArgs(argc, argv); // Parse the rest of the args (up to the 'option_end_index' (where '--' was diff --git a/src/node.js b/src/node.js index ab560405fa45..b294c459ee5c 100644 --- a/src/node.js +++ b/src/node.js @@ -46,6 +46,8 @@ startup.globalTimeouts(); startup.globalConsole(); + startup.setupLegacyCiphers(); + startup.processAssert(); startup.processConfig(); startup.processNextTick(); @@ -168,6 +170,25 @@ process._exiting = false; }; + startup.setupLegacyCiphers = function setupLegacyCiphers() { + process._usingV1040Ciphers = function _usingV1040Ciphers() { + // Returns true if the --enable-legacy-cipher-list command line + // switch, or the NODE_LEGACY_CIPHER_LIST environment variable + // are set to v0.10.40 and the DEFAULT_CIPHERS equal the v0.10.40 + // list. + var crypto = process.binding('crypto'); + + var argv = process.execArgv; + if ((argv.indexOf('--enable-legacy-cipher-list=v0.10.40') > -1 || + process.env.NODE_LEGACY_CIPHER_LIST === 'v0.10.40') && + crypto.DEFAULT_CIPHER_LIST === crypto.getLegacyCiphers('v0.10.40')) { + return true; + } + + return false; + }; + }; + startup.globalTimeouts = function() { global.setTimeout = function() { var t = NativeModule.require('timers'); diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 7a3922a797f8..440b23d4f369 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -62,6 +62,12 @@ static const int X509_NAME_FLAGS = ASN1_STRFLGS_ESC_CTRL | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_SN; +#define DEFAULT_CIPHER_LIST_V10_40 "ECDHE-RSA-AES128-SHA256:" \ + "AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH" + +#define DEFAULT_CIPHER_LIST_HEAD "ECDHE-RSA-AES128-SHA256:" \ + "AES128-GCM-SHA256:HIGH:!RC4:!MD5:!aNULL:!EDH" + namespace node { const char* root_certs[] = { @@ -71,6 +77,7 @@ const char* root_certs[] = { bool SSL2_ENABLE = false; bool SSL3_ENABLE = false; +const char * DEFAULT_CIPHER_LIST = DEFAULT_CIPHER_LIST_HEAD; namespace crypto { @@ -802,7 +809,7 @@ size_t ClientHelloParser::Write(const uint8_t* data, size_t len) { HandleScope scope; assert(state_ != kEnded); - + // Just accumulate data, everything will be pushed to BIO later if (state_ == kPaused) return 0; @@ -4190,6 +4197,43 @@ static void array_push_back(const TypeName* md, arr->Set(arr->Length(), String::New(from)); } +// borrowed from v8 +// (see http://v8.googlecode.com/svn/trunk/samples/shell.cc) +const char* ToCString(const node::Utf8Value& value) { + return *value ? *value : ""; +} + +const char* LegacyCipherList(const char * ver) { + if (ver == NULL) { + return NULL; + } + if (strncmp(ver, "v0.10.40", 8) == 0) { + return DEFAULT_CIPHER_LIST_V10_40; + } else { + return NULL; + } +} + +Handle GetLegacyCiphers(const Arguments& args) { + HandleScope scope; + + unsigned int len = args.Length(); + if (len != 1 || !args[0]->IsString()) { + return ThrowException( + Exception::TypeError( + String::New("A single string parameter is required"))); + } + + node::Utf8Value key(args[0]); + const char * list = LegacyCipherList(ToCString(key)); + + if (list != NULL) { + return scope.Close(v8::String::New(list)); + } else { + return ThrowException(Exception::Error(String::New( + "Unknown legacy cipher list"))); + } +} Handle GetCiphers(const Arguments& args) { HandleScope scope; @@ -4264,6 +4308,13 @@ void InitCrypto(Handle target) { NODE_DEFINE_CONSTANT(target, SSL3_ENABLE); NODE_DEFINE_CONSTANT(target, SSL2_ENABLE); + + (target)->ForceSet( + v8::String::New("DEFAULT_CIPHER_LIST"), + v8::String::New(DEFAULT_CIPHER_LIST), + static_cast(v8::ReadOnly | v8::DontDelete)); + + NODE_SET_METHOD(target, "getLegacyCiphers", GetLegacyCiphers); } } // namespace crypto diff --git a/src/node_crypto.h b/src/node_crypto.h index 54b9b88e437a..5d2edf617dc5 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -27,6 +27,7 @@ #include "node_object_wrap.h" #include "v8.h" +#include #include #include #include @@ -47,6 +48,7 @@ namespace node { extern bool SSL2_ENABLE; extern bool SSL3_ENABLE; +extern const char * DEFAULT_CIPHER_LIST; namespace crypto { @@ -294,6 +296,7 @@ class Connection : ObjectWrap { friend class SecureContext; }; +const char* LegacyCipherList(const char * ver); bool EntropySource(unsigned char* buffer, size_t length); void InitCrypto(v8::Handle target); diff --git a/test/external/ssl-options/test.js b/test/external/ssl-options/test.js index f7e06c93dfa7..a30167f3f7d9 100644 --- a/test/external/ssl-options/test.js +++ b/test/external/ssl-options/test.js @@ -11,9 +11,34 @@ 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" ]; +// RC4-MD5 is a SSLv2 cipher that can only be used if both ends explictly +// specify it as allowed. Indeed, MD5 is not available in the default ciphers +// list. +// It is a SSLv2 cipher, we use it to test that SSLv2 connections work. +var RC4_MD5_CIPHER = 'RC4-MD5'; + +// The 'RC4-SHA' cipher is one of the RC4 ciphers that is supported by +// versions of Node.js < v0.10.40 (before the deprecation of RC4), +// since both RC4 and SHA are included in the default ciphers list. +// +// 'RC4-SHA' is not supported by default with Node.js versions >= 0.10.40 +// since RC4 was deprecated at that time. +// +// This specific cipher is used to test that it can be used with current +// versions of Node.js *only* when both ends explicitly specify RC4-SHA +// as the cipher they want to use, or if --enable-legacy-cipher-list=v0.10.40 +// is passed at least to the client or the server. +// +// Note also that RC4-SHA is a SSLv3 cipher, not a SSLv2 cipher contrary to +// RC4-MD5. +var RC4_SHA_CIPHER = 'RC4-SHA'; + +var CMD_LINE_OPTIONS = [ + null, + "--enable-ssl2", + "--enable-ssl3", + "--enable-legacy-cipher-list=v0.10.40" +]; var SERVER_SSL_PROTOCOLS = [ null, @@ -137,6 +162,99 @@ function secureProtocolCompatibleWithSecureOptions(secureProtocol, secureOptions return true; } +// Returns true if the server and client setups passed as parameters +// would lead to a successful connection, false otherwise. +function testSSLv2Setups(serverSetup, clientSetup) { + // SSLv2 has to be explicitly specified on both sides to work + if (isSsl2Protocol(serverSetup.secureProtocol) && + !isSsl2Protocol(clientSetup.secureProtocol)) + return false; + + if (isSsl2Protocol(clientSetup.secureProtocol) && + !isSsl2Protocol(serverSetup.secureProtocol)) + return false; + + var ssl2UsedOnBothSides = isSsl2Protocol(serverSetup.secureProtocol) && + isSsl2Protocol(clientSetup.secureProtocol); + if (ssl2UsedOnBothSides) { + // Even when SSLv2 is specified on both sides, a SSLv2 compatible cipher + // has to be used on both sides too. + + // This is the case if for instance RC4-MD5 is passed explicitly + // as a cipher option + if (serverSetup.ciphers === RC4_MD5_CIPHER && + clientSetup.ciphers === RC4_MD5_CIPHER) + return true; + + // It is also the case if the server passes explicitly RC4-MD% + // but the client doesn't pass any cipher and passes + // --enable-legacy-cipher-list=v0.10.40 on the command line. This basically + // keeps the buggy be behavior of clients not using the default ciphers + // list when not explicitly passing any cipher, and as a result + // allowing RC4 and MD5 to be used. + if (serverSetup.ciphers === RC4_MD5_CIPHER && + clientSetup.ciphers === undefined && + clientSetup.cmdLine === '--enable-legacy-cipher-list=v0.10.40') + return true; + + // In all other cases, when using SSLv2 on both sides, + // the connection should fail. + return false; + } + + // When the client nor the server is using SSlv2, by default the connection + // should succeed. + // Other test functions looking for other incompatibilites between SSL/TLS + // options will take care of testing them. + return true; +} + +function usesDefaultCiphers(setup) { + assert(typeof setup === 'object', 'setup parameter must be an object'); + return setup.ciphers == null; +} + + +// Returns true if the server and client setups passed as parameters +// would lead to a successful connection, false otherwise. +function testRC4LegacyCiphers(serverSetup, clientSetup) { + + // To be able to use a RC4 cipher suite, either both ends specify it (like + // for the test using RC4-MD5), or one end pass it explicitly and the other + // uses the default ciphers list while passing the + // --enable-legacy-cipher-list=v0.10.40 command line option + // We're using RC4-SHA as our test cipher suite, because SHA is allowed by + // default and not RC4, so we know that we're only testing disabling/enabling + // RC4. + + if (serverSetup.ciphers === RC4_SHA_CIPHER || + clientSetup.ciphers === RC4_SHA_CIPHER) { + + if (serverSetup.ciphers === RC4_SHA_CIPHER && + clientSetup.ciphers === RC4_SHA_CIPHER) + return true; + + if (serverSetup.ciphers === RC4_SHA_CIPHER && + usesDefaultCiphers(clientSetup) && + clientSetup.cmdLine === '--enable-legacy-cipher-list=v0.10.40') + return true; + + if (clientSetup.ciphers === RC4_SHA_CIPHER && + usesDefaultCiphers(serverSetup) && + serverSetup.cmdLine === '--enable-legacy-cipher-list=v0.10.40') + return true; + + // Otherwise, if only one end passes a RC4 cipher suite explicitly, + // connection should fail. + return false; + } + + // When not using explicitly a RC4 cipher suite, connection should succeed. + // Other test functions looking for other incompatibilites between SSL/TLS + // options will take care of testing them. + return true; +} + function testSetupsCompatible(serverSetup, clientSetup) { debug('Determing test result for:'); debug(serverSetup); @@ -169,26 +287,12 @@ function testSetupsCompatible(serverSetup, clientSetup) { 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 (!testSSLv2Setups(serverSetup, clientSetup)) { + 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; - } + if (!testRC4LegacyCiphers(serverSetup, clientSetup)) { + return false; } return true; @@ -233,9 +337,15 @@ function createTestsSetups() { if (isSsl2Protocol(serverSecureProtocol)) { var setupWithSsl2Ciphers = xtend(serverSetup); - setupWithSsl2Ciphers.ciphers = SSL2_COMPATIBLE_CIPHERS; + setupWithSsl2Ciphers.ciphers = RC4_MD5_CIPHER; serversSetup.push(setupWithSsl2Ciphers); } + + if (isSsl3Protocol(serverSecureProtocol)) { + var setupWithSsl3Ciphers = xtend(serverSetup); + setupWithSsl3Ciphers.ciphers = RC4_SHA_CIPHER; + serversSetup.push(setupWithSsl3Ciphers); + } } }); }); @@ -255,9 +365,15 @@ function createTestsSetups() { if (isSsl2Protocol(clientSecureProtocol)) { var setupWithSsl2Ciphers = xtend(clientSetup); - setupWithSsl2Ciphers.ciphers = SSL2_COMPATIBLE_CIPHERS; + setupWithSsl2Ciphers.ciphers = RC4_MD5_CIPHER; clientsSetup.push(setupWithSsl2Ciphers); } + + if (isSsl3Protocol(clientSecureProtocol)) { + var setupWithSsl3Ciphers = xtend(clientSetup); + setupWithSsl3Ciphers.ciphers = RC4_SHA_CIPHER; + clientsSetup.push(setupWithSsl3Ciphers); + } } }); }); @@ -340,7 +456,8 @@ function runClient(port, secureProtocol, secureOptions, ciphers) { { rejectUnauthorized: false, secureProtocol: secureProtocol, - secureOptions: secureOptions + secureOptions: secureOptions, + ciphers: ciphers }, function() { diff --git a/test/simple/test-tls-cipher-list.js b/test/simple/test-tls-cipher-list.js new file mode 100644 index 000000000000..048680d740e4 --- /dev/null +++ b/test/simple/test-tls-cipher-list.js @@ -0,0 +1,273 @@ +// 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 spawn = require('child_process').spawn; +var assert = require('assert'); +var tls = require('tls'); +var crypto = process.binding('crypto'); +var common = require('../common'); +var fs = require('fs'); + +var V1038Ciphers = tls.getLegacyCiphers('v0.10.40'); + +function doTest(checklist, additional_args, env) { + var options; + if (env) options = {env:env}; + additional_args = additional_args || []; + var args = additional_args.concat([ + '-e', 'console.log(process.binding(\'crypto\').DEFAULT_CIPHER_LIST)']); + var out = ''; + spawn(process.execPath, args, options). + stdout. + on('data', function(data) { + out += data; + }). + on('end', function() { + assert.equal(out.trim(), checklist); + }); +} + +// test that the command line switches takes precedence +// over the environment variables +function doTestPrecedence() { + // test that --cipher-list takes precedence over NODE_CIPHER_LIST + doTest('ABC', ['--cipher-list=ABC'], {'NODE_CIPHER_LIST': 'XYZ'}); + + // test that --enable-legacy-cipher-list takes precedence + // over NODE_CIPHER_LIST + doTest(V1038Ciphers, + ['--enable-legacy-cipher-list=v0.10.40'], + {'NODE_CIPHER_LIST': 'XYZ'}); + + // test that --cipher-list takes precedence over NODE_LEGACY_CIPHER_LIST + doTest('ABC', + ['--cipher-list=ABC'], + {'NODE_LEGACY_CIPHER_LIST': 'v0.10.40'}); + + // test that --enable-legacy-cipher-list takes precence over both envars + // note: in this release, there's only one legal value for the legacy + // switch so this test is largely a non-op. When multiple values + // are supported, this test should be changed to test that the + // command line switch actually does override + doTest(V1038Ciphers, + ['--enable-legacy-cipher-list=v0.10.40'], + { + 'NODE_LEGACY_CIPHER_LIST': 'v0.10.40', + 'NODE_CIPHER_LIST': 'XYZ' + }); + + // test the right-most command line option takes precedence + doTest(V1038Ciphers, + [ + '--cipher-list=XYZ', + '--enable-legacy-cipher-list=v0.10.40' + ]); + + // test the right-most command line option takes precedence + doTest('XYZ', + [ + '--enable-legacy-cipher-list=v0.10.40', + '--cipher-list=XYZ' + ]); + + // test the right-most command line option takes precedence + doTest('XYZ', + [ + '--cipher-list=ABC', + '--enable-legacy-cipher-list=v0.10.40', + '--cipher-list=XYZ' + ]); + + // test that NODE_LEGACY_CIPHER_LIST takes precedence over + // NODE_CIPHER_LIST + doTest(V1038Ciphers, [], + { + 'NODE_LEGACY_CIPHER_LIST': 'v0.10.40', + 'NODE_CIPHER_LIST': 'ABC' + }); +} + +// Start running the tests... +doTest(crypto.DEFAULT_CIPHER_LIST); // test the default + +// Test the NODE_CIPHER_LIST environment variable +doTest('ABC', [], {'NODE_CIPHER_LIST':'ABC'}); + +// Test the --cipher-list command line switch +doTest('ABC', ['--cipher-list=ABC']); + +// Test the --enable-legacy-cipher-list and NODE_LEGACY_CIPHER_LIST envar +['v0.10.40'].forEach(function(arg) { + var checklist = tls.getLegacyCiphers(arg); + // command line switch + doTest(checklist, ['--enable-legacy-cipher-list=' + arg]); + // environment variable + doTest(checklist, [], {'NODE_LEGACY_CIPHER_LIST': arg}); +}); + +// Test the precedence order for the various options +doTestPrecedence(); + +// Test that we throw properly +// invalid value +assert.throws(function() {tls.getLegacyCiphers('foo');}, Error); +// no parameters +assert.throws(function() {tls.getLegacyCiphers();}, TypeError); +// not a string parameter +assert.throws(function() {tls.getLegacyCiphers(1);}, TypeError); +// too many parameters +assert.throws(function() {tls.getLegacyCiphers('abc', 'extra');}, TypeError); +// ah, just right +assert.doesNotThrow(function() {tls.getLegacyCiphers('v0.10.40');}); + +// Test to ensure default ciphers are not set when v0.10.40 legacy cipher +// switch is used. This is a bit involved... we need to first set up the +// TLS server, then spawn a second node instance using the v0.10.40 cipher, +// then connect and check to make sure the options are correct. Since there +// is no direct way of testing it, an alternate createCredentials shim is +// created that intercepts the call to createCredentials and checks the +// output. The following server code was adopted from +// test-tls-connect-simple. This spins up a server to verify that the +// connection is still able to function with the default ciphers not set +// on the client side. + +// note that the following function is written out to a string and +// passed in as an argument to a child node instance. +var fail_if_default_ciphers_set = ( + function() { + var tls = require('tls'); + var orig_createCredentials = require('crypto').createCredentials; + var used_monkey_patch = false; + require('crypto').createCredentials = function(options) { + used_monkey_patch = true; + // since node was started with the --enable-legacy-cipher-list + // switch equal to v0.10.40, the options.ciphers should be + // undefined. If it's not undefined, we have a problem and + // the test fails + if (options.ciphers !== undefined) { + console.error(options.ciphers); + process.exit(1); + } + return orig_createCredentials(options); + }; + var socket = tls.connect({ + port: 0, + rejectUnauthorized: false + }, function() { + socket.end(); + if (!used_monkey_patch) { + console.error('monkey patched createCredentials not used.'); + process.exit(1); + } + }); + } +).toString(); + +// Verifies that the default cipher list is set. +// like fail_if_default_ciphers_set, this is serialized +// out to a string and passed to a new node instance +var fail_if_default_ciphers_not_set = ( + function() { + var tls = require('tls'); + var orig_createCredentials = require('crypto').createCredentials; + var used_monkey_patch = false; + require('crypto').createCredentials = function(options) { + used_monkey_patch = true; + // node is not started with --enable-legacy-cipher-list + if (!options.ciphers) { + console.error('default ciphers are not set'); + process.exit(1); + } + return orig_createCredentials(options); + }; + var socket = tls.connect({ + port: 0, + rejectUnauthorized: false + }, function() { + socket.end(); + if (!used_monkey_patch) { + console.error('monkey patched createCredentials not used.'); + process.exit(1); + } + }); + } +).toString(); + + +var test_count = 0; + +function doDefaultCipherTest(test, additional_args, env) { + var options = {}; + if (env) options.env = env; + var err = ''; + additional_args = additional_args || []; + var args = additional_args.concat([ + '-e', require('util').format('(%s)()', test). + replace('port: 0', + 'port: ' + common.PORT) + ]); + var child = spawn(process.execPath, args, options); + // if the child process writes to stderr, report it + // as a failure. This will capture the error in the + // tls connection also, which is what we want. We + // want to be able to verify that changes to the + // default cipher list being set or not will not impact + // the actual connection being made. + child.stderr. + on('data', function(data) { + err += data; + }). + on('end', function() { + if (err !== '') { + assert.fail(err.substr(0,err.length-1)); + } + }); +} + +var options = { + key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'), + cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem') +}; +var server = tls.Server(options, function(socket) { + test_count++; + if (test_count === 4) server.close(); +}); +server.listen(common.PORT, function() { + // checks to make sure the default ciphers are *not* set + // because the --enable-legacy-cipher-list switch is set to + // v0.10.40 + doDefaultCipherTest(fail_if_default_ciphers_set, + ['--enable-legacy-cipher-list=v0.10.40']); + + // checks to make sure the default ciphers are *not* set + // because the NODE_LEGACY_CIPHER_LIST envar is set to v0.10.40 + doDefaultCipherTest(fail_if_default_ciphers_set, + [], {'NODE_LEGACY_CIPHER_LIST': 'v0.10.40'}); + + // this variant checks to ensure that the default cipher list IS set + doDefaultCipherTest(fail_if_default_ciphers_not_set, [], {}); + + // test that setting the cipher list explicitly to the v0.10.40 + // string without using the legacy cipher switch causes the + // default ciphers to be set. + doDefaultCipherTest(fail_if_default_ciphers_not_set, + ['--cipher-list=' + V1038Ciphers], {}); +}); diff --git a/test/simple/test-tls-clients-default-ciphers.js b/test/simple/test-tls-clients-default-ciphers.js new file mode 100644 index 000000000000..181c7e4a175e --- /dev/null +++ b/test/simple/test-tls-clients-default-ciphers.js @@ -0,0 +1,122 @@ +// 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. + +/* + * This is a regression test that highlights a problem where tls.connect would + * not use the default ciphers suite when no ciphers was passed. + * + * This test makes sure that when connecting to a server that uses only the RC4 + * cipher, which was removed recently from the default ciphers suite, calls to + * tls.connect that don't pass any ciphers fail to connect properly. + */ + +var common = require('../common'); +var assert = require('assert'); +var tls = require('tls'); +var https = require('https'); +var path = require('path'); +var fs = require('fs'); + +var KEYPATH = path.join(common.fixturesDir, 'keys', 'agent2-key.pem'); +var KEY = fs.readFileSync(KEYPATH).toString(); + +var CERTPATH = path.join(common.fixturesDir, 'keys', 'agent2-cert.pem'); +var CERT = fs.readFileSync(CERTPATH).toString(); + +var SERVER_OPTIONS = { + key: KEY, + cert: CERT, + // RC4 was removed recently from the default ciphers list + // due to security vulnerabilities. Set the server to only use this + // cipher so that this test can make sure that tls clients + // can't connect to it when they use default ciphers. + ciphers: 'RC4-MD5', +}; + +/* + * Uses "clientFn" with default ciphers to connect to TLS/HTTPS server + * on port "port". Calls "callback" with an object as parameter + * that has one property "allClientsFailed" that is true + * if all clients connections/requests failed, false otherwise. + */ +function testClientWithDefaultCiphers(clientFn, port, callback) { + // Use different forms of passing default ciphers to + // tls connect: no 'ciphers' property, 'ciphers' with value 'null' and + // 'ciphers' with value 'undefined'. Although we would think that these + // semantically similar ways of passing no ciphers would trigger the same + // behavior, in reality the implementation behaves differently, and + // thus we need to keep all these forms in the test. + var CLIENT_OPTIONS = [ + { + rejectUnauthorized: false, + ciphers: null + }, + { + rejectUnauthorized: false, + ciphers: undefined + }, + { + rejectUnauthorized: false, + } + ]; + + var nbClientErrors = 0; + var callbackCalled = false; + + CLIENT_OPTIONS.forEach(function(clientOptionsObject) { + clientOptionsObject.port = port; + + var conn = clientFn(clientOptionsObject, function onConnect() { + callbackCalled = true; + return callback({ allClientsFailed: false }); + }); + + conn.on('error', function onClientError(err) { + ++nbClientErrors; + if (nbClientErrors === CLIENT_OPTIONS.length && !callbackCalled) { + callbackCalled = true; + return callback({ allClientsFailed: true }); + } + }); + }); +} + +// Test that tls.connect uses default ciphers on the client properly +var tlsServer = new tls.Server(SERVER_OPTIONS); +tlsServer.listen(common.PORT, function () { + testClientWithDefaultCiphers(tls.connect, common.PORT, function onTestsDone(results) { + assert(results.allClientsFailed, + 'All TLS clients using default ciphers to server only ' + + 'supporting RC4 cipher should have failed'); + tlsServer.close(); + }); +}); + +// Test that https.get uses default ciphers on the client properly +var httpsServer = new https.Server(SERVER_OPTIONS); +httpsServer.listen(common.PORT + 1, function () { + testClientWithDefaultCiphers(https.get, common.PORT + 1, function onTestsDone(results) { + assert(results.allClientsFailed, + 'All HTTPS clients using default ciphers to server only ' + + 'supporting RC4 cipher should have failed'); + httpsServer.close(); + }); +}); diff --git a/test/simple/test-tls-getcipher.js b/test/simple/test-tls-getcipher.js index 22a280e58743..8fb9d528731d 100644 --- a/test/simple/test-tls-getcipher.js +++ b/test/simple/test-tls-getcipher.js @@ -49,7 +49,7 @@ server.listen(common.PORT, '127.0.0.1', function() { rejectUnauthorized: false }, function() { var cipher = client.getCipher(); - assert.equal(cipher.name, cipher_list[0]); + assert.equal(cipher.name, cipher_list[1]); assert(cipher_version_pattern.test(cipher.version)); client.end(); server.close();