• 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   { 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