// 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. 'use strict'; const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); if (!common.opensslCli) common.skip('node compiled without OpenSSL CLI.'); // This is a rather complex test which sets up various TLS servers with node // and connects to them using the 'openssl s_client' command line utility // with various keys. Depending on the certificate authority and other // parameters given to the server, the various clients are // - rejected, // - accepted and "unauthorized", or // - accepted and "authorized". const assert = require('assert'); const { spawn } = require('child_process'); const { SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION } = require('crypto').constants; const tls = require('tls'); const fixtures = require('../common/fixtures'); const testCases = [{ title: 'Do not request certs. Everyone is unauthorized.', requestCert: false, rejectUnauthorized: false, renegotiate: false, CAs: ['ca1-cert'], clients: [{ name: 'agent1', shouldReject: false, shouldAuth: false }, { name: 'agent2', shouldReject: false, shouldAuth: false }, { name: 'agent3', shouldReject: false, shouldAuth: false }, { name: 'nocert', shouldReject: false, shouldAuth: false } ] }, { title: 'Allow both authed and unauthed connections with CA1', requestCert: true, rejectUnauthorized: false, renegotiate: false, CAs: ['ca1-cert'], clients: [{ name: 'agent1', shouldReject: false, shouldAuth: true }, { name: 'agent2', shouldReject: false, shouldAuth: false }, { name: 'agent3', shouldReject: false, shouldAuth: false }, { name: 'nocert', shouldReject: false, shouldAuth: false } ] }, { title: 'Do not request certs at connection. Do that later', requestCert: false, rejectUnauthorized: false, renegotiate: true, CAs: ['ca1-cert'], clients: [{ name: 'agent1', shouldReject: false, shouldAuth: true }, { name: 'agent2', shouldReject: false, shouldAuth: false }, { name: 'agent3', shouldReject: false, shouldAuth: false }, { name: 'nocert', shouldReject: false, shouldAuth: false } ] }, { title: 'Allow only authed connections with CA1', requestCert: true, rejectUnauthorized: true, renegotiate: false, CAs: ['ca1-cert'], clients: [{ name: 'agent1', shouldReject: false, shouldAuth: true }, { name: 'agent2', shouldReject: true }, { name: 'agent3', shouldReject: true }, { name: 'nocert', shouldReject: true } ] }, { title: 'Allow only authed connections with CA1 and CA2', requestCert: true, rejectUnauthorized: true, renegotiate: false, CAs: ['ca1-cert', 'ca2-cert'], clients: [{ name: 'agent1', shouldReject: false, shouldAuth: true }, { name: 'agent2', shouldReject: true }, { name: 'agent3', shouldReject: false, shouldAuth: true }, { name: 'nocert', shouldReject: true } ] }, { title: 'Allow only certs signed by CA2 but not in the CRL', requestCert: true, rejectUnauthorized: true, renegotiate: false, CAs: ['ca2-cert'], crl: 'ca2-crl', clients: [ { name: 'agent1', shouldReject: true, shouldAuth: false }, { name: 'agent2', shouldReject: true, shouldAuth: false }, { name: 'agent3', shouldReject: false, shouldAuth: true }, // Agent4 has a cert in the CRL. { name: 'agent4', shouldReject: true, shouldAuth: false }, { name: 'nocert', shouldReject: true } ] } ]; function filenamePEM(n) { return fixtures.path('keys', `${n}.pem`); } function loadPEM(n) { return fixtures.readKey(`${n}.pem`); } const serverKey = loadPEM('agent2-key'); const serverCert = loadPEM('agent2-cert'); function runClient(prefix, port, options, cb) { // Client can connect in three ways: // - Self-signed cert // - Certificate, but not signed by CA. // - Certificate signed by CA. const args = ['s_client', '-connect', `127.0.0.1:${port}`]; console.log(`${prefix} connecting with`, options.name); switch (options.name) { case 'agent1': // Signed by CA1 args.push('-key'); args.push(filenamePEM('agent1-key')); args.push('-cert'); args.push(filenamePEM('agent1-cert')); break; case 'agent2': // Self-signed // This is also the key-cert pair that the server will use. args.push('-key'); args.push(filenamePEM('agent2-key')); args.push('-cert'); args.push(filenamePEM('agent2-cert')); break; case 'agent3': // Signed by CA2 args.push('-key'); args.push(filenamePEM('agent3-key')); args.push('-cert'); args.push(filenamePEM('agent3-cert')); break; case 'agent4': // Signed by CA2 (rejected by ca2-crl) args.push('-key'); args.push(filenamePEM('agent4-key')); args.push('-cert'); args.push(filenamePEM('agent4-cert')); break; case 'nocert': // Do not send certificate break; default: throw new Error(`${prefix}Unknown agent name`); } // To test use: openssl s_client -connect localhost:8000 const client = spawn(common.opensslCli, args); let out = ''; let rejected = true; let authed = false; let goodbye = false; client.stdout.setEncoding('utf8'); client.stdout.on('data', function(d) { out += d; if (!goodbye && /_unauthed/.test(out)) { console.error(`${prefix} * unauthed`); goodbye = true; client.kill(); authed = false; rejected = false; } if (!goodbye && /_authed/.test(out)) { console.error(`${prefix} * authed`); goodbye = true; client.kill(); authed = true; rejected = false; } }); client.on('exit', function(code) { if (options.shouldReject) { assert.strictEqual( rejected, true, `${prefix}${options.name} NOT rejected, but should have been`); } else { assert.strictEqual( rejected, false, `${prefix}${options.name} rejected, but should NOT have been`); assert.strictEqual( authed, options.shouldAuth, `${prefix}${options.name} authed is ${authed} but should have been ${ options.shouldAuth}`); } cb(); }); } // Run the tests let successfulTests = 0; function runTest(port, testIndex) { const prefix = `${testIndex} `; const tcase = testCases[testIndex]; if (!tcase) return; console.error(`${prefix}Running '${tcase.title}'`); const cas = tcase.CAs.map(loadPEM); const crl = tcase.crl ? loadPEM(tcase.crl) : null; const serverOptions = { key: serverKey, cert: serverCert, ca: cas, crl: crl, requestCert: tcase.requestCert, rejectUnauthorized: tcase.rejectUnauthorized }; /* * If renegotiating - session might be resumed and openssl won't request * client's certificate (probably because of bug in the openssl) */ if (tcase.renegotiate) { serverOptions.secureOptions = SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; // Renegotiation as a protocol feature was dropped after TLS1.2. serverOptions.maxVersion = 'TLSv1.2'; } let renegotiated = false; const server = tls.Server(serverOptions, function handleConnection(c) { c.on('error', function(e) { // child.kill() leads ECONNRESET error in the TLS connection of // openssl s_client via spawn(). A test result is already // checked by the data of client.stdout before child.kill() so // these tls errors can be ignored. }); if (tcase.renegotiate && !renegotiated) { renegotiated = true; setTimeout(function() { console.error(`${prefix}- connected, renegotiating`); c.write('\n_renegotiating\n'); return c.renegotiate({ requestCert: true, rejectUnauthorized: false }, function(err) { assert.ifError(err); c.write('\n_renegotiated\n'); handleConnection(c); }); }, 200); return; } if (c.authorized) { console.error(`${prefix}- authed connection: ${ c.getPeerCertificate().subject.CN}`); c.write('\n_authed\n'); } else { console.error(`${prefix}- unauthed connection: %s`, c.authorizationError); c.write('\n_unauthed\n'); } }); function runNextClient(clientIndex) { const options = tcase.clients[clientIndex]; if (options) { runClient(`${prefix}${clientIndex} `, port, options, function() { runNextClient(clientIndex + 1); }); } else { server.close(); successfulTests++; runTest(0, nextTest++); } } server.listen(port, function() { port = server.address().port; if (tcase.debug) { console.error(`${prefix}TLS server running on port ${port}`); } else if (tcase.renegotiate) { runNextClient(0); } else { let clientsCompleted = 0; for (let i = 0; i < tcase.clients.length; i++) { runClient(`${prefix}${i} `, port, tcase.clients[i], function() { clientsCompleted++; if (clientsCompleted === tcase.clients.length) { server.close(); successfulTests++; runTest(0, nextTest++); } }); } } }); } let nextTest = 0; runTest(0, nextTest++); process.on('exit', function() { assert.strictEqual(successfulTests, testCases.length); });