1// Copyright Joyent, Inc. and other Node contributors. 2// 3// Permission is hereby granted, free of charge, to any person obtaining a 4// copy of this software and associated documentation files (the 5// "Software"), to deal in the Software without restriction, including 6// without limitation the rights to use, copy, modify, merge, publish, 7// distribute, sublicense, and/or sell copies of the Software, and to permit 8// persons to whom the Software is furnished to do so, subject to the 9// following conditions: 10// 11// The above copyright notice and this permission notice shall be included 12// in all copies or substantial portions of the Software. 13// 14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20// USE OR OTHER DEALINGS IN THE SOFTWARE. 21 22'use strict'; 23const common = require('../common'); 24 25if (!common.hasCrypto) 26 common.skip('missing crypto'); 27 28if (!common.opensslCli) 29 common.skip('node compiled without OpenSSL CLI.'); 30 31// This is a rather complex test which sets up various TLS servers with node 32// and connects to them using the 'openssl s_client' command line utility 33// with various keys. Depending on the certificate authority and other 34// parameters given to the server, the various clients are 35// - rejected, 36// - accepted and "unauthorized", or 37// - accepted and "authorized". 38 39const assert = require('assert'); 40const { spawn } = require('child_process'); 41const { SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION } = 42 require('crypto').constants; 43const tls = require('tls'); 44const fixtures = require('../common/fixtures'); 45 46const testCases = 47 [{ title: 'Do not request certs. Everyone is unauthorized.', 48 requestCert: false, 49 rejectUnauthorized: false, 50 renegotiate: false, 51 CAs: ['ca1-cert'], 52 clients: 53 [{ name: 'agent1', shouldReject: false, shouldAuth: false }, 54 { name: 'agent2', shouldReject: false, shouldAuth: false }, 55 { name: 'agent3', shouldReject: false, shouldAuth: false }, 56 { name: 'nocert', shouldReject: false, shouldAuth: false } 57 ] 58 }, 59 60 { title: 'Allow both authed and unauthed connections with CA1', 61 requestCert: true, 62 rejectUnauthorized: false, 63 renegotiate: false, 64 CAs: ['ca1-cert'], 65 clients: 66 [{ name: 'agent1', shouldReject: false, shouldAuth: true }, 67 { name: 'agent2', shouldReject: false, shouldAuth: false }, 68 { name: 'agent3', shouldReject: false, shouldAuth: false }, 69 { name: 'nocert', shouldReject: false, shouldAuth: false } 70 ] 71 }, 72 73 { title: 'Do not request certs at connection. Do that later', 74 requestCert: false, 75 rejectUnauthorized: false, 76 renegotiate: true, 77 CAs: ['ca1-cert'], 78 clients: 79 [{ name: 'agent1', shouldReject: false, shouldAuth: true }, 80 { name: 'agent2', shouldReject: false, shouldAuth: false }, 81 { name: 'agent3', shouldReject: false, shouldAuth: false }, 82 { name: 'nocert', shouldReject: false, shouldAuth: false } 83 ] 84 }, 85 86 { title: 'Allow only authed connections with CA1', 87 requestCert: true, 88 rejectUnauthorized: true, 89 renegotiate: false, 90 CAs: ['ca1-cert'], 91 clients: 92 [{ name: 'agent1', shouldReject: false, shouldAuth: true }, 93 { name: 'agent2', shouldReject: true }, 94 { name: 'agent3', shouldReject: true }, 95 { name: 'nocert', shouldReject: true } 96 ] 97 }, 98 99 { title: 'Allow only authed connections with CA1 and CA2', 100 requestCert: true, 101 rejectUnauthorized: true, 102 renegotiate: false, 103 CAs: ['ca1-cert', 'ca2-cert'], 104 clients: 105 [{ name: 'agent1', shouldReject: false, shouldAuth: true }, 106 { name: 'agent2', shouldReject: true }, 107 { name: 'agent3', shouldReject: false, shouldAuth: true }, 108 { name: 'nocert', shouldReject: true } 109 ] 110 }, 111 112 113 { title: 'Allow only certs signed by CA2 but not in the CRL', 114 requestCert: true, 115 rejectUnauthorized: true, 116 renegotiate: false, 117 CAs: ['ca2-cert'], 118 crl: 'ca2-crl', 119 clients: [ 120 { name: 'agent1', shouldReject: true, shouldAuth: false }, 121 { name: 'agent2', shouldReject: true, shouldAuth: false }, 122 { name: 'agent3', shouldReject: false, shouldAuth: true }, 123 // Agent4 has a cert in the CRL. 124 { name: 'agent4', shouldReject: true, shouldAuth: false }, 125 { name: 'nocert', shouldReject: true } 126 ] 127 } 128 ]; 129 130function filenamePEM(n) { 131 return fixtures.path('keys', `${n}.pem`); 132} 133 134function loadPEM(n) { 135 return fixtures.readKey(`${n}.pem`); 136} 137 138 139const serverKey = loadPEM('agent2-key'); 140const serverCert = loadPEM('agent2-cert'); 141 142 143function runClient(prefix, port, options, cb) { 144 145 // Client can connect in three ways: 146 // - Self-signed cert 147 // - Certificate, but not signed by CA. 148 // - Certificate signed by CA. 149 150 const args = ['s_client', '-connect', `127.0.0.1:${port}`]; 151 152 console.log(`${prefix} connecting with`, options.name); 153 154 switch (options.name) { 155 case 'agent1': 156 // Signed by CA1 157 args.push('-key'); 158 args.push(filenamePEM('agent1-key')); 159 args.push('-cert'); 160 args.push(filenamePEM('agent1-cert')); 161 break; 162 163 case 'agent2': 164 // Self-signed 165 // This is also the key-cert pair that the server will use. 166 args.push('-key'); 167 args.push(filenamePEM('agent2-key')); 168 args.push('-cert'); 169 args.push(filenamePEM('agent2-cert')); 170 break; 171 172 case 'agent3': 173 // Signed by CA2 174 args.push('-key'); 175 args.push(filenamePEM('agent3-key')); 176 args.push('-cert'); 177 args.push(filenamePEM('agent3-cert')); 178 break; 179 180 case 'agent4': 181 // Signed by CA2 (rejected by ca2-crl) 182 args.push('-key'); 183 args.push(filenamePEM('agent4-key')); 184 args.push('-cert'); 185 args.push(filenamePEM('agent4-cert')); 186 break; 187 188 case 'nocert': 189 // Do not send certificate 190 break; 191 192 default: 193 throw new Error(`${prefix}Unknown agent name`); 194 } 195 196 // To test use: openssl s_client -connect localhost:8000 197 const client = spawn(common.opensslCli, args); 198 199 let out = ''; 200 201 let rejected = true; 202 let authed = false; 203 let goodbye = false; 204 205 client.stdout.setEncoding('utf8'); 206 client.stdout.on('data', function(d) { 207 out += d; 208 209 if (!goodbye && /_unauthed/.test(out)) { 210 console.error(`${prefix} * unauthed`); 211 goodbye = true; 212 client.kill(); 213 authed = false; 214 rejected = false; 215 } 216 217 if (!goodbye && /_authed/.test(out)) { 218 console.error(`${prefix} * authed`); 219 goodbye = true; 220 client.kill(); 221 authed = true; 222 rejected = false; 223 } 224 }); 225 226 client.on('exit', function(code) { 227 if (options.shouldReject) { 228 assert.strictEqual( 229 rejected, true, 230 `${prefix}${options.name} NOT rejected, but should have been`); 231 } else { 232 assert.strictEqual( 233 rejected, false, 234 `${prefix}${options.name} rejected, but should NOT have been`); 235 assert.strictEqual( 236 authed, options.shouldAuth, 237 `${prefix}${options.name} authed is ${authed} but should have been ${ 238 options.shouldAuth}`); 239 } 240 241 cb(); 242 }); 243} 244 245 246// Run the tests 247let successfulTests = 0; 248function runTest(port, testIndex) { 249 const prefix = `${testIndex} `; 250 const tcase = testCases[testIndex]; 251 if (!tcase) return; 252 253 console.error(`${prefix}Running '${tcase.title}'`); 254 255 const cas = tcase.CAs.map(loadPEM); 256 257 const crl = tcase.crl ? loadPEM(tcase.crl) : null; 258 259 const serverOptions = { 260 key: serverKey, 261 cert: serverCert, 262 ca: cas, 263 crl: crl, 264 requestCert: tcase.requestCert, 265 rejectUnauthorized: tcase.rejectUnauthorized 266 }; 267 268 /* 269 * If renegotiating - session might be resumed and openssl won't request 270 * client's certificate (probably because of bug in the openssl) 271 */ 272 if (tcase.renegotiate) { 273 serverOptions.secureOptions = 274 SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; 275 // Renegotiation as a protocol feature was dropped after TLS1.2. 276 serverOptions.maxVersion = 'TLSv1.2'; 277 } 278 279 let renegotiated = false; 280 const server = tls.Server(serverOptions, function handleConnection(c) { 281 c.on('error', function(e) { 282 // child.kill() leads ECONNRESET error in the TLS connection of 283 // openssl s_client via spawn(). A test result is already 284 // checked by the data of client.stdout before child.kill() so 285 // these tls errors can be ignored. 286 }); 287 if (tcase.renegotiate && !renegotiated) { 288 renegotiated = true; 289 setTimeout(function() { 290 console.error(`${prefix}- connected, renegotiating`); 291 c.write('\n_renegotiating\n'); 292 return c.renegotiate({ 293 requestCert: true, 294 rejectUnauthorized: false 295 }, function(err) { 296 assert.ifError(err); 297 c.write('\n_renegotiated\n'); 298 handleConnection(c); 299 }); 300 }, 200); 301 return; 302 } 303 304 if (c.authorized) { 305 console.error(`${prefix}- authed connection: ${ 306 c.getPeerCertificate().subject.CN}`); 307 c.write('\n_authed\n'); 308 } else { 309 console.error(`${prefix}- unauthed connection: %s`, c.authorizationError); 310 c.write('\n_unauthed\n'); 311 } 312 }); 313 314 function runNextClient(clientIndex) { 315 const options = tcase.clients[clientIndex]; 316 if (options) { 317 runClient(`${prefix}${clientIndex} `, port, options, function() { 318 runNextClient(clientIndex + 1); 319 }); 320 } else { 321 server.close(); 322 successfulTests++; 323 runTest(0, nextTest++); 324 } 325 } 326 327 server.listen(port, function() { 328 port = server.address().port; 329 if (tcase.debug) { 330 console.error(`${prefix}TLS server running on port ${port}`); 331 } else if (tcase.renegotiate) { 332 runNextClient(0); 333 } else { 334 let clientsCompleted = 0; 335 for (let i = 0; i < tcase.clients.length; i++) { 336 runClient(`${prefix}${i} `, port, tcase.clients[i], function() { 337 clientsCompleted++; 338 if (clientsCompleted === tcase.clients.length) { 339 server.close(); 340 successfulTests++; 341 runTest(0, nextTest++); 342 } 343 }); 344 } 345 } 346 }); 347} 348 349 350let nextTest = 0; 351runTest(0, nextTest++); 352 353 354process.on('exit', function() { 355 assert.strictEqual(successfulTests, testCases.length); 356}); 357