diff options
author | James M Snell <jasnell@gmail.com> | 2015-04-02 15:16:40 -0700 |
---|---|---|
committer | James M Snell <jasnell@gmail.com> | 2015-04-08 12:00:18 -0700 |
commit | 67d9a56251c4491beacb666ba5833574d0cf0d12 (patch) | |
tree | b3c2ea3cb29a6153c4c370fa28a4ed0c3b8132c2 | |
parent | 7edfd5f6d11f7a219408bc1edf265dbfe2b90900 (diff) | |
download | node-67d9a56251c4491beacb666ba5833574d0cf0d12.tar.gz |
tls: disable RC4, add --cipher-list command line switch
Disable RC4 in the default cipher list
Add the `--cipher-list` command line switch and `NODE_CIPHER_LIST`
environment variable to completely override the default cipher list.
Add the `--enable-legacy-cipher-list` and `NODE_LEGACY_CIPHER_LIST`
environment variable to selectively enable the default cipher list from
previous node.js releases.
Reviewed-By: James M Snell <jasnell@gmail.com>
PR-URL: https://github.com/joyent/node/pull/14413
-rw-r--r-- | doc/api/tls.markdown | 66 | ||||
-rw-r--r-- | lib/tls.js | 14 | ||||
-rw-r--r-- | src/node.cc | 41 | ||||
-rw-r--r-- | src/node_crypto.cc | 25 | ||||
-rw-r--r-- | src/node_crypto.h | 20 | ||||
-rw-r--r-- | test/simple/test-tls-cipher-list.js | 70 | ||||
-rw-r--r-- | test/simple/test-tls-getcipher.js | 2 |
7 files changed, 226 insertions, 12 deletions
diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index fbd97e88a..49b37106e 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -109,6 +109,60 @@ 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.38. 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.38 defaults + node --enable-legacy-cipher-list=v0.10.38 + // or + NODE_LEGACY_CIPHER_LIST=v0.10.38 + +Currently, the values supported for the `enable-legacy-cipher-list` switch and +`NODE_LEGACY_CIPHER_LIST` environment variable include: + + v0.10.38 - To enable the default cipher suite used in v0.10.38 + + 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.38')); + +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. ## tls.getCiphers() @@ -151,13 +205,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 +387,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 +579,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 +604,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/tls.js b/lib/tls.js index e3b908322..9f53ad82a 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,9 @@ exports.connect = function(/* [port, host], options, cb */) { var defaults = { rejectUnauthorized: '0' !== process.env.NODE_TLS_REJECT_UNAUTHORIZED }; + if (DEFAULT_CIPHERS != _crypto.getLegacyCiphers('v0.10.38')) { + 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 e80c1a573..4ba39b111 100644 --- a/src/node.cc +++ b/src/node.cc @@ -2566,6 +2566,9 @@ 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=val \n" + " set to v0.10.38 to use the v0.10.38 list\n" "\n" "Environment variables:\n" #ifdef _WIN32 @@ -2577,6 +2580,9 @@ 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\n" + " Set to v0.10.38 to use the v0.10.38 list\n" "\n" "Documentation can be found at http://nodejs.org/\n"); } @@ -2584,6 +2590,7 @@ static void PrintHelp() { // Parse node command line arguments. static void ParseArgs(int argc, char **argv) { int i; + bool using_legacy_cipher_list = false; // TODO use parse opts for (i = 1; i < argc; i++) { @@ -2652,6 +2659,21 @@ static void ParseArgs(int argc, char **argv) { } else if (strcmp(arg, "--throw-deprecation") == 0) { argv[i] = const_cast<char*>(""); throw_deprecation = true; + } else if (strncmp(arg, "--cipher-list=", 14) == 0) { + if (!using_legacy_cipher_list) { + DEFAULT_CIPHER_LIST = arg + 14; + } + argv[i] = const_cast<char*>(""); + } else if (strncmp(arg, "--enable-legacy-cipher-list=", 28) == 0) { + const char * legacy_list = legacy_cipher_list(arg+28); + if (legacy_list != NULL) { + using_legacy_cipher_list = true; + DEFAULT_CIPHER_LIST = legacy_list; + } else { + fprintf(stderr, "Error: An unknown legacy cipher list was specified\n"); + exit(9); + } + argv[i] = const_cast<char*>(""); } else if (argv[i][0] != '-') { break; } @@ -2946,6 +2968,25 @@ char** Init(int argc, char *argv[]) { v8argv[option_end_index + 1] = const_cast<char*>("v8debug"); } + const char * cipher_list = getenv("NODE_CIPHER_LIST"); + if (cipher_list != NULL) { + DEFAULT_CIPHER_LIST = cipher_list; + } + // Allow the NODE_LEGACY_CIPHER_LIST envar to override the other + // cipher list options. NODE_LEGACY_CIPHER_LIST=v0.10.38 will use + // the cipher list from v0.10.38 + const char * leg_cipher_id = getenv("NODE_LEGACY_CIPHER_LIST"); + if (leg_cipher_id != NULL) { + const char * leg_cipher_list = + legacy_cipher_list(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); + } + } + // For the normal stack which moves from high to low addresses when frames // are pushed, we can compute the limit as stack_size bytes below the // the address of a stack variable (e.g. &stack_var) as an approximation diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 7a3922a79..c1e943fef 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -71,6 +71,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 +803,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 +4191,21 @@ 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 : "<string conversion failed>"; +} + +Handle<Value> DefaultCiphers(const Arguments& args) { + HandleScope scope; + node::Utf8Value key(args[0]); + const char * list = legacy_cipher_list(ToCString(key)); + if (list == NULL) { + list = DEFAULT_CIPHER_LIST_HEAD; + } + return scope.Close(v8::String::New(list)); +} Handle<Value> GetCiphers(const Arguments& args) { HandleScope scope; @@ -4264,6 +4280,13 @@ void InitCrypto(Handle<Object> 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::PropertyAttribute>(v8::ReadOnly | v8::DontDelete)); + + NODE_SET_METHOD(target, "getLegacyCiphers", DefaultCiphers); } } // namespace crypto diff --git a/src/node_crypto.h b/src/node_crypto.h index 54b9b88e4..0b360cfa3 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -27,6 +27,7 @@ #include "node_object_wrap.h" #include "v8.h" +#include <string.h> #include <openssl/ssl.h> #include <openssl/err.h> #include <openssl/evp.h> @@ -43,10 +44,29 @@ #define EVP_F_EVP_DECRYPTFINAL 101 +#define DEFAULT_CIPHER_LIST_V10_38 "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" + +static inline const char * legacy_cipher_list(const char * ver) { + if (ver == NULL) { + return NULL; + } + if (strncmp(ver, "v0.10.38", 8) == 0) { + return DEFAULT_CIPHER_LIST_V10_38; + } else { + return NULL; + } +} + + namespace node { extern bool SSL2_ENABLE; extern bool SSL3_ENABLE; +extern const char * DEFAULT_CIPHER_LIST; namespace crypto { diff --git a/test/simple/test-tls-cipher-list.js b/test/simple/test-tls-cipher-list.js new file mode 100644 index 000000000..ac2169537 --- /dev/null +++ b/test/simple/test-tls-cipher-list.js @@ -0,0 +1,70 @@ +// 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'); + +function doTest(checklist, env, useswitch) { + var options; + if (env && useswitch === 1) { + options = {env:env}; + } + var args = ['-e', 'console.log(process.binding(\'crypto\').DEFAULT_CIPHER_LIST)']; + + switch(useswitch) { + case 1: + // Test --cipher-test + args.unshift('--cipher-list=' + env); + break; + case 2: + // Test --enable-legacy-cipher-list + args.unshift('--enable-legacy-cipher-list=' + env); + break; + case 3: + // Test NODE_LEGACY_CIPHER_LIST + if (env) options = {env:{"NODE_LEGACY_CIPHER_LIST": env}}; + break; + default: + // Test NODE_CIPHER_LIST + if (env) options = {env:env}; + } + + var out = ''; + spawn(process.execPath, args, options). + stdout. + on('data', function(data) { + out += data; + }). + on('end', function() { + assert.equal(out.trim(), checklist); + }); +} + +doTest(crypto.DEFAULT_CIPHER_LIST); // test the default +doTest('ABC', {'NODE_CIPHER_LIST':'ABC'}); // test the envar +doTest('ABC', 'ABC', 1); // test the --cipher-list switch + +['v0.10.38'].forEach(function(ver) { + doTest(tls.getLegacyCiphers(ver), ver, 2); + doTest(tls.getLegacyCiphers(ver), ver, 3); +}); diff --git a/test/simple/test-tls-getcipher.js b/test/simple/test-tls-getcipher.js index 22a280e58..8fb9d5287 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(); |