Skip to content
This repository was archived by the owner on Apr 22, 2023. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
tls: refactor test-tls-cipher-list and v0.10.38 legacy test
Per the feedback from Julien, refactor the test-tls-cipher-list
test to avoid using the confusing switch statement. Add tests
to ensure that using `--enable-legacy-cipher-list=v0.10.38`
or `NODE_LEGACY_CIPHER_LIST=v0.10.38` disables the use of the
default cipher list but setting the `--cipher-list` equal to
the v0.10.38 list explicitly does not.

Minor refactor to tls.js to ensure the above behavior.
  • Loading branch information
jasnell committed May 1, 2015
commit 14a26f9491aab4f95335417f1f96000091a58763
17 changes: 16 additions & 1 deletion lib/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,20 @@ function normalizeConnectArgs(listArgs) {
return (cb) ? [options, cb] : [options];
}

// return true if the --enable-legacy-cipher-list command line
// switch, or the NODE_LEGACY_CIPHER_LIST environment variable
// are set to v0.10.38 and the DEFAULT_CIPHERS equal the v0.10.38
// list.
function usingV1038Ciphers() {
var argv = process.execArgv;
if ((argv.indexOf('--enable-legacy-cipher-list=v0.10.38') > -1 ||
process.env['NODE_LEGACY_CIPHER_LIST'] === 'v0.10.38') &&
DEFAULT_CIPHERS === _crypto.getLegacyCiphers('v0.10.38')) {
return true;
}
return false;
}

exports.connect = function(/* [port, host], options, cb */) {
var args = normalizeConnectArgs(arguments);
var options = args[0];
Expand All @@ -1338,7 +1352,8 @@ 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')) {
if (!usingV1038Ciphers()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment to clarify why there's an exception for v0.10.38 here? Otherwise I'm concerned it's going to be difficult to understand a few months/years from now.

// only set the default ciphers
defaults.ciphers = DEFAULT_CIPHERS;
}
options = util._extend(defaults, options || {});
Expand Down
270 changes: 179 additions & 91 deletions test/simple/test-tls-cipher-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,84 +19,21 @@
// 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 spawn = require('child_process').spawn;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: the current style used does not align the = operator for assignment statements.

var assert = require('assert');
var tls = require('tls');
var tls = require('tls');
var crypto = process.binding('crypto');
var common = require('../common');
var fs = require('fs');

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;
case 4:
// Test command line switch takes precedence over environment variable
args.unshift('--cipher-list=' + env);
if (env) options = {env:{"NODE_CIPHER_LIST": "XYZ"}};
break;
case 5:
// Test command line switch takes precedence over environment variable
args.unshift('--enable-legacy-cipher-list=' + env);
if (env) options = {env:{"NODE_CIPHER_LIST": "XYZ"}};
break;
case 6:
// Test right-most option takes precedence
args.unshift('--enable-legacy-cipher-list=' + env);
args.unshift('--cipher-list=XYZ');
break;
case 7:
// Test right-most option takes precedence
args.unshift('--cipher-list=' + env);
args.unshift('--enable-legacy-cipher-list=v0.10.38');
break;
case 8:
// Test NODE_LEGACY_CIPHER_LIST takes precendence over NODE_CIPHER_LIST
options = {env:{
"NODE_LEGACY_CIPHER_LIST": env,
"NODE_CIPHER_LIST": "ABC"
}};
break;
case 9:
// Test command line switch takes precedence over environment variable
args.unshift('--cipher-list=' + env);
if (env) options = {env:{"NODE_LEGACY_CIPHER_LIST": "v0.10.38"}};
break;
case 10:
// Test command line switch takes precedence over environment variable
args.unshift('--enable-legacy-cipher-list=' + env);
if (env) options = {
env:{
"NODE_LEGACY_CIPHER_LIST": "v0.10.38",
"NODE_CIPHER_LIST": "XYZ"
}
};
break;
case 11:
// Test right-most option takes precedence, multiple of same option
args.unshift('--cipher-list=' + env);
args.unshift('--enable-legacy-cipher-list=v0.10.38');
args.unshift('--cipher-list=' + 'XYZ');
break;
default:
// Test NODE_CIPHER_LIST
if (env) options = {env:env};
}
var V1038Ciphers = tls.getLegacyCiphers('v0.10.38');

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.
Expand All @@ -108,27 +45,88 @@ function doTest(checklist, env, useswitch) {
});
}

var V1038Ciphers = tls.getLegacyCiphers('v0.10.38');
// test that the command line switchs takes precedence

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switchs -> switches.

// 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.38'],
{'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.38'});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: unnecessary blank line.


// test that --enable-legacy-cipher-list takes precence over both envars
doTest(V1038Ciphers,
['--enable-legacy-cipher-list=v0.10.38'],
{
'NODE_LEGACY_CIPHER_LIST': 'v0.10.38',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want another value for NODE_LEGACY_CIPHER_LIST (e.g ABC), otherwise how do we know that the value for legacy cipher list was taken from the command line switch and not from the environment variable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In v0.10.39 there is only one legal value for this. Passing in anything but
v0.10.38 throws, even if it's overridden by the command line switch. For
v0.12.3 and master I specify different values.
On May 4, 2015 5:19 PM, "Julien Gilli" [email protected] wrote:

In test/simple/test-tls-cipher-list.js
#14572 (comment):

  • // over NODE_CIPHER_LIST
  • doTest(V1038Ciphers,
  •     ['--enable-legacy-cipher-list=v0.10.38'],
    
  •     {'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.38'});
    
  • // test that --enable-legacy-cipher-list takes precence over both envars
  • doTest(V1038Ciphers,
  •     ['--enable-legacy-cipher-list=v0.10.38'],
    
  •     {
    
  •       'NODE_LEGACY_CIPHER_LIST': 'v0.10.38',
    

Don't we want another value for NODE_LEGACY_CIPHER_LIST (e.g ABC),
otherwise how do we know that the value for legacy cipher list was taken
from the command line switch and not from the environment variable?


Reply to this email directly or view it on GitHub
https://github.com/joyent/node/pull/14572/files#r29636669.

'NODE_CIPHER_LIST': 'XYZ'
});

// test the right-most command line option takes precedence
doTest(V1038Ciphers,
[
'--cipher-list=XYZ',
'--enable-legacy-cipher-list=v0.10.38'
]);

// test the right-most command line option takes precedence
doTest('XYZ',
[
'--enable-legacy-cipher-list=v0.10.38',
'--cipher-list=XYZ'
]);

// test the right-most command line option takes precedence
doTest('XYZ',
[
'--cipher-list=XYZ',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want another value for the first --cipher-list command line switch (e.g ABC)? Otherwise how do we know that the value for ciphers list was taken from the second --cipher-list and not the first one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, actually that's just a typo... it should have been different

'--enable-legacy-cipher-list=v0.10.38',
'--cipher-list=XYZ'
]);

// test that NODE_LEGACY_CIPHER_LIST takes precedence over
// NODE_CIPHER_LIST

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: unnecessary blank line.

doTest(V1038Ciphers, [],
{
'NODE_LEGACY_CIPHER_LIST': 'v0.10.38',
'NODE_CIPHER_LIST': 'ABC'
});

}

// Start running the tests...

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

// test precedence order
doTest('ABC', 'ABC', 4);
doTest(V1038Ciphers, 'v0.10.38', 5);
doTest(V1038Ciphers, 'v0.10.38', 6);
doTest('ABC', 'ABC', 7);
doTest(V1038Ciphers, 'v0.10.38', 8);
doTest('ABC', 'ABC', 9);
doTest(V1038Ciphers, 'v0.10.38', 10);
doTest('YYY', 'YYY', 11);

['v0.10.38'].forEach(function(ver) {
doTest(tls.getLegacyCiphers(ver), ver, 2);
doTest(tls.getLegacyCiphers(ver), ver, 3);

// 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.38'].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
Expand All @@ -139,3 +137,93 @@ assert.throws(function() {tls.getLegacyCiphers(1);}, TypeError);
assert.throws(function() {tls.getLegacyCiphers('abc', 'extra');}, TypeError);
// ah, just right
assert.doesNotThrow(function() {tls.getLegacyCiphers('v0.10.38');});



// Test to ensure default ciphers are not set when v0.10.38 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.38 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.

// note that the following function is written out to a string and
// passed in as an argument to a child node instance.
var script = (
function() {
var tls = require('tls');
var orig_createCredentials = require('crypto').createCredentials;
require('crypto').createCredentials = function(options) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of monkey patching crypto.createCredentials, what do you think about starting a server that sets its ciphers list to RC4-MD5 only?

Since it's explicitly excluded from the ciphers list after node v0.10.38, the test could make sure that the client connection can only be established when passing --enable-legacy-cipher-list=v0.10.38 or setting equivalent environment variables.

I believe it would make the implementation of this test much simpler.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not quite what this is testing. This tests to make sure the default
ciphers are not set on the options when v0.10.38 legacy is set. The monkey
patch allows us to intercept the call and verify that the ciphers property
is indeed not set.
On May 4, 2015 6:05 PM, "Julien Gilli" [email protected] wrote:

In test/simple/test-tls-cipher-list.js
#14572 (comment):

+// Test to ensure default ciphers are not set when v0.10.38 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.38 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.
+
+// note that the following function is written out to a string and
+// passed in as an argument to a child node instance.
+var script = (

  • function() {
  • var tls = require('tls');
  • var orig_createCredentials = require('crypto').createCredentials;
  • require('crypto').createCredentials = function(options) {

Instead of monkey patching crypto.createCredentials, what do you think
about starting a server that sets its ciphers list to RC4-MD5 only?

Since it's explicitly excluded from the ciphers list after node v0.10.38,
the test could make sure that the client connection can only be established
when passing --enable-legacy-cipher-list=v0.10.38 or setting equivalent
environment variables.

I believe it would make the implementation of this test much simpler.


Reply to this email directly or view it on GitHub
https://github.com/joyent/node/pull/14572/files#r29638490.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thank you for the clarification. I would suggest also asserting that the monkey-patched version of crypto.createCredentials was called to avoid missing potential future regressions if crypto.createCredentials ends up being refactored and is not called anymore (like in v0.12).

Also, do we really need to spawn a server to run that test? If I understand correctly, crypto.createCredentials is called before any connection is initiated, so I would expect that we can perform the same test without starting a server.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spawn the server purposefully to do a full roundtrip to make sure that the connection still works without those defaults being set on the client side.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasnell OK, I understand now. Generally, testing a single characteristic/behavior in a test helps make it clearer and avoid false negatives.

In this case, the test doesn't make a difference between the child process writing to stderr because the client could not connect or because the options.ciphers value passed to crypto.createCredentials was not expected. See my comment here: https://github.com/joyent/node/pull/14572/files#r30068923.

If I understand correctly, doDefaultCipherTest seems to test more than one thing. It tests that crypto.createCredentials is called with the proper value for its options.ciphers argument, and that a client that uses --enable-legacy-cipher-list can connect to a tls.Server instance started with the default options.

I think splitting that in two different simpler tests would make the tests more reliable and easier to understand.

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();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the client cannot connect, an 'error' event would be emitted and node would write to stderr. The parent process would check that some data was read from the child's stderr, and it would make the test pass, but it shouldn't.

});
}
).toString();

var test_count = 0;

function doDefaultCipherTest(additional_args, env, failexpected) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: instead of passing booleans, I find passing options objects easier to read. In this specific case:

doDefaultCipherTest([], {}, test_uses_default_cipher_list);

turns into

doDefaultCipherTest([], {},  { failExpected: true });

var options = {};
if (env) options.env = env;
var out = '', err = '';
additional_args = additional_args || [];
var args = additional_args.concat([

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another way to achieve the same thing would be to use fork and to pass an argument to the child process that allows to easily identify what code to run in the parent and in the child processes. Something similar to what is done in https://github.com/joyent/node/blob/v0.10/test/simple/test-domain-top-level-error-handler-throw.js. A search with grep -rHn "'child'" test/simple will show you many tests where this pattern is used.

That would require to have this test in a separate file, and I think this would make things simpler and cleaner overall.

'-e', require('util').format('(%s)()', script).replace(
'port: 0', 'port: ' + common.PORT)
]);
var child = spawn(process.execPath, args, options);
child.stdout.
on('data', function(data) {
out += data;
}).
on('end', function() {
if (failexpected && err === '') {
// if we get here, there's a problem because the default cipher
// list was not set when it should have been
assert.fail('options.cipher list was not set');
}
});
child.stderr.
on('data', function(data) {
err += data;
}).
on('end', function() {
if (err !== '') {
if (!failexpected) {
assert.fail(err.substr(0,err.length-1));
}
}
});
child.on('close', function() {
test_count++;
if (test_count === 4) server.close();
});
}

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) {});
server.listen(common.PORT, function() {
doDefaultCipherTest(['--enable-legacy-cipher-list=v0.10.38']);
doDefaultCipherTest([], {'NODE_LEGACY_CIPHER_LIST': 'v0.10.38'});
// this variant checks to ensure that the default cipher list IS set
var test_uses_default_cipher_list = true;
doDefaultCipherTest([], {}, test_uses_default_cipher_list);
// test that setting the cipher list explicitly to the v0.10.38
// string without using the legacy cipher switch causes the
// default ciphers to be set.
doDefaultCipherTest(['--cipher-list=' + V1038Ciphers], {},
test_uses_default_cipher_list);
});