summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/tests/mochitest/browser/browser_clientAuth_connection.js
blob: d8f57c3e8b2fecd3f30e9cb8c9f2946196bb2253 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/publicdomain/zero/1.0/
"use strict";

// Tests various scenarios connecting to a server that requires client cert
// authentication. Also tests that nsIClientAuthDialogs.chooseCertificate
// is called at the appropriate times and with the correct arguments.

const { MockRegistrar } = ChromeUtils.importESModule(
  "resource://testing-common/MockRegistrar.sys.mjs"
);

const DialogState = {
  // Assert that chooseCertificate() is never called.
  ASSERT_NOT_CALLED: "ASSERT_NOT_CALLED",
  // Return that the user selected the first given cert.
  RETURN_CERT_SELECTED: "RETURN_CERT_SELECTED",
  // Return that the user canceled.
  RETURN_CERT_NOT_SELECTED: "RETURN_CERT_NOT_SELECTED",
};

var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing);
let cars = Cc["@mozilla.org/security/clientAuthRememberService;1"].getService(
  Ci.nsIClientAuthRememberService
);

var gExpectedClientCertificateChoices;

// Mock implementation of nsIClientAuthDialogs.
const gClientAuthDialogs = {
  _state: DialogState.ASSERT_NOT_CALLED,
  _rememberClientAuthCertificate: false,
  _chooseCertificateCalled: false,

  set state(newState) {
    info(`old state: ${this._state}`);
    this._state = newState;
    info(`new state: ${this._state}`);
  },

  get state() {
    return this._state;
  },

  set rememberClientAuthCertificate(value) {
    this._rememberClientAuthCertificate = value;
  },

  get rememberClientAuthCertificate() {
    return this._rememberClientAuthCertificate;
  },

  get chooseCertificateCalled() {
    return this._chooseCertificateCalled;
  },

  set chooseCertificateCalled(value) {
    this._chooseCertificateCalled = value;
  },

  chooseCertificate(
    hostname,
    port,
    organization,
    issuerOrg,
    certList,
    selectedIndex,
    rememberClientAuthCertificate
  ) {
    this.chooseCertificateCalled = true;
    Assert.notEqual(
      this.state,
      DialogState.ASSERT_NOT_CALLED,
      "chooseCertificate() should be called only when expected"
    );

    rememberClientAuthCertificate.value = this.rememberClientAuthCertificate;

    Assert.equal(
      hostname,
      "requireclientcert.example.com",
      "Hostname should be 'requireclientcert.example.com'"
    );
    Assert.equal(port, 443, "Port should be 443");
    Assert.equal(
      organization,
      "",
      "Server cert Organization should be empty/not present"
    );
    Assert.equal(
      issuerOrg,
      "Mozilla Testing",
      "Server cert issuer Organization should be 'Mozilla Testing'"
    );

    // For mochitests, the cert at build/pgo/certs/mochitest.client should be
    // selectable as well as one of the PGO certs we loaded in `setup`, so we do
    // some brief checks to confirm this.
    Assert.notEqual(certList, null, "Cert list should not be null");
    Assert.equal(
      certList.length,
      gExpectedClientCertificateChoices,
      `${gExpectedClientCertificateChoices} certificates should be available`
    );

    for (let cert of certList.enumerate(Ci.nsIX509Cert)) {
      Assert.notEqual(cert, null, "Cert list should contain nsIX509Certs");
      Assert.equal(
        cert.issuerCommonName,
        "Temporary Certificate Authority",
        "cert should have expected issuer CN"
      );
    }

    if (this.state == DialogState.RETURN_CERT_SELECTED) {
      selectedIndex.value = 0;
      return true;
    }
    return false;
  },

  QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogs"]),
};

add_setup(async function () {
  let clientAuthDialogsCID = MockRegistrar.register(
    "@mozilla.org/nsClientAuthDialogs;1",
    gClientAuthDialogs
  );
  registerCleanupFunction(() => {
    MockRegistrar.unregister(clientAuthDialogsCID);
  });

  // This CA has the expected keyCertSign and cRLSign usages. It should not be
  // presented for use as a client certificate.
  await readCertificate("pgo-ca-regular-usages.pem", "CTu,CTu,CTu");
  // This CA has all keyUsages. For compatibility with preexisting behavior, it
  // will be presented for use as a client certificate.
  await readCertificate("pgo-ca-all-usages.pem", "CTu,CTu,CTu");
  // This client certificate was issued by an intermediate that was issued by
  // the test CA. The server only lists the test CA's subject distinguished name
  // as an acceptible issuer name for client certificates. If the implementation
  // can determine that the test CA is a root CA for the client certificate and
  // thus is acceptible to use, it should be included in the chooseCertificate
  // callback. At the beginning of this test (speaking of this file as a whole),
  // the client is not aware of the intermediate, and so it is not available in
  // the callback.
  await readCertificate("client-cert-via-intermediate.pem", ",,");
  // This certificate has an id-kp-OCSPSigning EKU. Client certificates
  // shouldn't have this EKU, but there is at least one private PKI where they
  // do. For interoperability, such certificates will be presented for use.
  await readCertificate("client-cert-with-ocsp-signing.pem", ",,");
  gExpectedClientCertificateChoices = 3;
});

/**
 * Test helper for the tests below.
 *
 * @param {string} prefValue
 *        Value to set the "security.default_personal_cert" pref to.
 * @param {string} urlToNavigate
 *        The URL to navigate to.
 * @param {string} expectedURL
 *        If the connection is expected to load successfully, the URL that
 *        should load. If the connection is expected to fail and result in an
 *        error page, |undefined|.
 * @param {boolean} expectCallingChooseCertificate
 *        Determines whether we expect chooseCertificate to be called.
 * @param {object} options
 *        Optional options object to pass on to the window that gets opened.
 * @param {string} expectStringInPage
 *        Optional string that is expected to be in the content of the page
 *        once it loads.
 */
async function testHelper(
  prefValue,
  urlToNavigate,
  expectedURL,
  expectCallingChooseCertificate,
  options = undefined,
  expectStringInPage = undefined
) {
  gClientAuthDialogs.chooseCertificateCalled = false;
  await SpecialPowers.pushPrefEnv({
    set: [["security.default_personal_cert", prefValue]],
  });

  let win = await BrowserTestUtils.openNewBrowserWindow(options);

  BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, urlToNavigate);
  if (expectedURL) {
    await BrowserTestUtils.browserLoaded(
      win.gBrowser.selectedBrowser,
      false,
      "https://requireclientcert.example.com/",
      true
    );
    let loadedURL = win.gBrowser.selectedBrowser.documentURI.spec;
    Assert.ok(
      loadedURL.startsWith(expectedURL),
      `Expected and actual URLs should match (got '${loadedURL}', expected '${expectedURL}')`
    );
  } else {
    await new Promise(resolve => {
      let removeEventListener = BrowserTestUtils.addContentEventListener(
        win.gBrowser.selectedBrowser,
        "AboutNetErrorLoad",
        () => {
          removeEventListener();
          resolve();
        },
        { capture: false, wantUntrusted: true }
      );
    });
  }

  Assert.equal(
    gClientAuthDialogs.chooseCertificateCalled,
    expectCallingChooseCertificate,
    "chooseCertificate should have been called if we were expecting it to be called"
  );

  if (expectStringInPage) {
    let pageContent = await SpecialPowers.spawn(
      win.gBrowser.selectedBrowser,
      [],
      async function () {
        return content.document.body.textContent;
      }
    );
    Assert.ok(
      pageContent.includes(expectStringInPage),
      `page should contain the string '${expectStringInPage}' (was '${pageContent}')`
    );
  }

  await win.close();

  // This clears the TLS session cache so we don't use a previously-established
  // ticket to connect and bypass selecting a client auth certificate in
  // subsequent tests.
  sdr.logout();
}

// Test that if a certificate is chosen automatically the connection succeeds,
// and that nsIClientAuthDialogs.chooseCertificate() is never called.
add_task(async function testCertChosenAutomatically() {
  gClientAuthDialogs.state = DialogState.ASSERT_NOT_CALLED;
  await testHelper(
    "Select Automatically",
    "https://requireclientcert.example.com/",
    "https://requireclientcert.example.com/",
    false
  );
  // This clears all saved client auth certificate state so we don't influence
  // subsequent tests.
  cars.clearRememberedDecisions();
});

// Test that if the user doesn't choose a certificate, the connection fails and
// an error page is displayed.
add_task(async function testCertNotChosenByUser() {
  gClientAuthDialogs.state = DialogState.RETURN_CERT_NOT_SELECTED;
  await testHelper(
    "Ask Every Time",
    "https://requireclientcert.example.com/",
    undefined,
    true,
    undefined,
    // bug 1818556: ssltunnel doesn't behave as expected here on Windows
    AppConstants.platform != "win"
      ? "SSL_ERROR_RX_CERTIFICATE_REQUIRED_ALERT"
      : undefined
  );
  cars.clearRememberedDecisions();
});

// Test that if the user chooses a certificate the connection suceeeds.
add_task(async function testCertChosenByUser() {
  gClientAuthDialogs.state = DialogState.RETURN_CERT_SELECTED;
  await testHelper(
    "Ask Every Time",
    "https://requireclientcert.example.com/",
    "https://requireclientcert.example.com/",
    true
  );
  cars.clearRememberedDecisions();
});

// Test that the cancel decision is remembered correctly
add_task(async function testEmptyCertChosenByUser() {
  gClientAuthDialogs.state = DialogState.RETURN_CERT_NOT_SELECTED;
  gClientAuthDialogs.rememberClientAuthCertificate = true;
  await testHelper(
    "Ask Every Time",
    "https://requireclientcert.example.com/",
    undefined,
    true
  );
  await testHelper(
    "Ask Every Time",
    "https://requireclientcert.example.com/",
    undefined,
    false
  );
  cars.clearRememberedDecisions();
});

// Test that if the user chooses a certificate in a private browsing window,
// configures Firefox to remember this certificate for the duration of the
// session, closes that window (and thus all private windows), reopens a private
// window, and visits that site again, they are re-asked for a certificate (i.e.
// any state from the previous private session should be gone). Similarly, after
// closing that private window, if the user opens a non-private window, they
// again should be asked to choose a certificate (i.e. private state should not
// be remembered/used in non-private contexts).
add_task(async function testClearPrivateBrowsingState() {
  gClientAuthDialogs.rememberClientAuthCertificate = true;
  gClientAuthDialogs.state = DialogState.RETURN_CERT_SELECTED;
  await testHelper(
    "Ask Every Time",
    "https://requireclientcert.example.com/",
    "https://requireclientcert.example.com/",
    true,
    {
      private: true,
    }
  );
  await testHelper(
    "Ask Every Time",
    "https://requireclientcert.example.com/",
    "https://requireclientcert.example.com/",
    true,
    {
      private: true,
    }
  );
  await testHelper(
    "Ask Every Time",
    "https://requireclientcert.example.com/",
    "https://requireclientcert.example.com/",
    true
  );
  // NB: we don't `cars.clearRememberedDecisions()` in between the two calls to
  // `testHelper` because that would clear all client auth certificate state and
  // obscure what we're testing (that Firefox properly clears the relevant state
  // when the last private window closes).
  cars.clearRememberedDecisions();
});

// Test that 3rd party certificates are taken into account when filtering client
// certificates based on the acceptible CA list sent by the server.
add_task(async function testCertFilteringWithIntermediate() {
  let intermediateBytes = await IOUtils.readUTF8(
    getTestFilePath("intermediate.pem")
  ).then(
    pem => {
      let base64 = pemToBase64(pem);
      let bin = atob(base64);
      let bytes = [];
      for (let i = 0; i < bin.length; i++) {
        bytes.push(bin.charCodeAt(i));
      }
      return bytes;
    },
    error => {
      throw error;
    }
  );
  let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent);
  nssComponent.addEnterpriseIntermediate(intermediateBytes);
  gExpectedClientCertificateChoices = 4;
  gClientAuthDialogs.state = DialogState.RETURN_CERT_SELECTED;
  await testHelper(
    "Ask Every Time",
    "https://requireclientcert.example.com/",
    "https://requireclientcert.example.com/",
    true
  );
  cars.clearRememberedDecisions();
  // This will reset the added intermediate.
  await SpecialPowers.pushPrefEnv({
    set: [["security.enterprise_roots.enabled", true]],
  });
});

// Test that if the server certificate does not validate successfully,
// nsIClientAuthDialogs.chooseCertificate() is never called.
add_task(async function testNoDialogForUntrustedServerCertificate() {
  gClientAuthDialogs.state = DialogState.ASSERT_NOT_CALLED;
  await testHelper(
    "Ask Every Time",
    "https://requireclientcert-untrusted.example.com/",
    undefined,
    false
  );
  // This clears all saved client auth certificate state so we don't influence
  // subsequent tests.
  cars.clearRememberedDecisions();
});