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 { title: 'Allow both authed and unauthed connections with CA1', 60 requestCert: true, 61 rejectUnauthorized: false, 62 renegotiate: false, 63 CAs: ['ca1-cert'], 64 clients: 65 [{ name: 'agent1', shouldReject: false, shouldAuth: true }, 66 { name: 'agent2', shouldReject: false, shouldAuth: false }, 67 { name: 'agent3', shouldReject: false, shouldAuth: false }, 68 { name: 'nocert', shouldReject: false, shouldAuth: false }, 69 ] }, 70 71 { title: 'Do not request certs at connection. Do that later', 72 requestCert: false, 73 rejectUnauthorized: false, 74 renegotiate: true, 75 CAs: ['ca1-cert'], 76 clients: 77 [{ name: 'agent1', shouldReject: false, shouldAuth: true }, 78 { name: 'agent2', shouldReject: false, shouldAuth: false }, 79 { name: 'agent3', shouldReject: false, shouldAuth: false }, 80 { name: 'nocert', shouldReject: false, shouldAuth: false }, 81 ] }, 82 83 { title: 'Allow only authed connections with CA1', 84 requestCert: true, 85 rejectUnauthorized: true, 86 renegotiate: false, 87 CAs: ['ca1-cert'], 88 clients: 89 [{ name: 'agent1', shouldReject: false, shouldAuth: true }, 90 { name: 'agent2', shouldReject: true }, 91 { name: 'agent3', shouldReject: true }, 92 { name: 'nocert', shouldReject: true }, 93 ] }, 94 95 { title: 'Allow only authed connections with CA1 and CA2', 96 requestCert: true, 97 rejectUnauthorized: true, 98 renegotiate: false, 99 CAs: ['ca1-cert', 'ca2-cert'], 100 clients: 101 [{ name: 'agent1', shouldReject: false, shouldAuth: true }, 102 { name: 'agent2', shouldReject: true }, 103 { name: 'agent3', shouldReject: false, shouldAuth: true }, 104 { name: 'nocert', shouldReject: true }, 105 ] }, 106 107 108 { title: 'Allow only certs signed by CA2 but not in the CRL', 109 requestCert: true, 110 rejectUnauthorized: true, 111 renegotiate: false, 112 CAs: ['ca2-cert'], 113 crl: 'ca2-crl', 114 clients: [ 115 { name: 'agent1', shouldReject: true, shouldAuth: false }, 116 { name: 'agent2', shouldReject: true, shouldAuth: false }, 117 { name: 'agent3', shouldReject: false, shouldAuth: true }, 118 // Agent4 has a cert in the CRL. 119 { name: 'agent4', shouldReject: true, shouldAuth: false }, 120 { name: 'nocert', shouldReject: true }, 121 ] }, 122 ]; 123 124function filenamePEM(n) { 125 return fixtures.path('keys', `${n}.pem`); 126} 127 128function loadPEM(n) { 129 return fixtures.readKey(`${n}.pem`); 130} 131 132 133const serverKey = loadPEM('agent2-key'); 134const serverCert = loadPEM('agent2-cert'); 135 136 137function runClient(prefix, port, options, cb) { 138 139 // Client can connect in three ways: 140 // - Self-signed cert 141 // - Certificate, but not signed by CA. 142 // - Certificate signed by CA. 143 144 const args = ['s_client', '-connect', `127.0.0.1:${port}`]; 145 146 console.log(`${prefix} connecting with`, options.name); 147 148 switch (options.name) { 149 case 'agent1': 150 // Signed by CA1 151 args.push('-key'); 152 args.push(filenamePEM('agent1-key')); 153 args.push('-cert'); 154 args.push(filenamePEM('agent1-cert')); 155 break; 156 157 case 'agent2': 158 // Self-signed 159 // This is also the key-cert pair that the server will use. 160 args.push('-key'); 161 args.push(filenamePEM('agent2-key')); 162 args.push('-cert'); 163 args.push(filenamePEM('agent2-cert')); 164 break; 165 166 case 'agent3': 167 // Signed by CA2 168 args.push('-key'); 169 args.push(filenamePEM('agent3-key')); 170 args.push('-cert'); 171 args.push(filenamePEM('agent3-cert')); 172 break; 173 174 case 'agent4': 175 // Signed by CA2 (rejected by ca2-crl) 176 args.push('-key'); 177 args.push(filenamePEM('agent4-key')); 178 args.push('-cert'); 179 args.push(filenamePEM('agent4-cert')); 180 break; 181 182 case 'nocert': 183 // Do not send certificate 184 break; 185 186 default: 187 throw new Error(`${prefix}Unknown agent name`); 188 } 189 190 // To test use: openssl s_client -connect localhost:8000 191 const client = spawn(common.opensslCli, args); 192 193 let out = ''; 194 195 let rejected = true; 196 let authed = false; 197 let goodbye = false; 198 199 client.stdout.setEncoding('utf8'); 200 client.stdout.on('data', function(d) { 201 out += d; 202 203 if (!goodbye && /_unauthed/.test(out)) { 204 console.error(`${prefix} * unauthed`); 205 goodbye = true; 206 client.kill(); 207 authed = false; 208 rejected = false; 209 } 210 211 if (!goodbye && /_authed/.test(out)) { 212 console.error(`${prefix} * authed`); 213 goodbye = true; 214 client.kill(); 215 authed = true; 216 rejected = false; 217 } 218 }); 219 220 client.on('exit', function(code) { 221 if (options.shouldReject) { 222 assert.strictEqual( 223 rejected, true, 224 `${prefix}${options.name} NOT rejected, but should have been`); 225 } else { 226 assert.strictEqual( 227 rejected, false, 228 `${prefix}${options.name} rejected, but should NOT have been`); 229 assert.strictEqual( 230 authed, options.shouldAuth, 231 `${prefix}${options.name} authed is ${authed} but should have been ${ 232 options.shouldAuth}`); 233 } 234 235 cb(); 236 }); 237} 238 239 240// Run the tests 241let successfulTests = 0; 242function runTest(port, testIndex) { 243 const prefix = `${testIndex} `; 244 const tcase = testCases[testIndex]; 245 if (!tcase) return; 246 247 console.error(`${prefix}Running '${tcase.title}'`); 248 249 const cas = tcase.CAs.map(loadPEM); 250 251 const crl = tcase.crl ? loadPEM(tcase.crl) : null; 252 253 const serverOptions = { 254 key: serverKey, 255 cert: serverCert, 256 ca: cas, 257 crl: crl, 258 requestCert: tcase.requestCert, 259 rejectUnauthorized: tcase.rejectUnauthorized 260 }; 261 262 // If renegotiating - session might be resumed and openssl won't request 263 // client's certificate (probably because of bug in the openssl) 264 if (tcase.renegotiate) { 265 serverOptions.secureOptions = 266 SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; 267 // Renegotiation as a protocol feature was dropped after TLS1.2. 268 serverOptions.maxVersion = 'TLSv1.2'; 269 } 270 271 let renegotiated = false; 272 const server = tls.Server(serverOptions, function handleConnection(c) { 273 c.on('error', function(e) { 274 // child.kill() leads ECONNRESET error in the TLS connection of 275 // openssl s_client via spawn(). A test result is already 276 // checked by the data of client.stdout before child.kill() so 277 // these tls errors can be ignored. 278 }); 279 if (tcase.renegotiate && !renegotiated) { 280 renegotiated = true; 281 setTimeout(function() { 282 console.error(`${prefix}- connected, renegotiating`); 283 c.write('\n_renegotiating\n'); 284 return c.renegotiate({ 285 requestCert: true, 286 rejectUnauthorized: false 287 }, function(err) { 288 assert.ifError(err); 289 c.write('\n_renegotiated\n'); 290 handleConnection(c); 291 }); 292 }, 200); 293 return; 294 } 295 296 if (c.authorized) { 297 console.error(`${prefix}- authed connection: ${ 298 c.getPeerCertificate().subject.CN}`); 299 c.write('\n_authed\n'); 300 } else { 301 console.error(`${prefix}- unauthed connection: %s`, c.authorizationError); 302 c.write('\n_unauthed\n'); 303 } 304 }); 305 306 function runNextClient(clientIndex) { 307 const options = tcase.clients[clientIndex]; 308 if (options) { 309 runClient(`${prefix}${clientIndex} `, port, options, function() { 310 runNextClient(clientIndex + 1); 311 }); 312 } else { 313 server.close(); 314 successfulTests++; 315 runTest(0, nextTest++); 316 } 317 } 318 319 server.listen(port, function() { 320 port = server.address().port; 321 if (tcase.debug) { 322 console.error(`${prefix}TLS server running on port ${port}`); 323 } else if (tcase.renegotiate) { 324 runNextClient(0); 325 } else { 326 let clientsCompleted = 0; 327 for (let i = 0; i < tcase.clients.length; i++) { 328 runClient(`${prefix}${i} `, port, tcase.clients[i], function() { 329 clientsCompleted++; 330 if (clientsCompleted === tcase.clients.length) { 331 server.close(); 332 successfulTests++; 333 runTest(0, nextTest++); 334 } 335 }); 336 } 337 } 338 }); 339} 340 341 342let nextTest = 0; 343runTest(0, nextTest++); 344 345 346process.on('exit', function() { 347 assert.strictEqual(successfulTests, testCases.length); 348}); 349