• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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