1 // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2 // Any copyright is dedicated to the Public Domain.
3 // http://creativecommons.org/publicdomain/zero/1.0/
6 // Tests various scenarios connecting to a server that requires client cert
7 // authentication. Also tests that nsIClientAuthDialogService.chooseCertificate
8 // is called at the appropriate times and with the correct arguments.
10 const { MockRegistrar } = ChromeUtils.importESModule(
11 "resource://testing-common/MockRegistrar.sys.mjs"
15 // Assert that chooseCertificate() is never called.
16 ASSERT_NOT_CALLED: "ASSERT_NOT_CALLED",
17 // Return that the user selected the first given cert.
18 RETURN_CERT_SELECTED: "RETURN_CERT_SELECTED",
19 // Return that the user canceled.
20 RETURN_CERT_NOT_SELECTED: "RETURN_CERT_NOT_SELECTED",
23 var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing);
24 let cars = Cc["@mozilla.org/security/clientAuthRememberService;1"].getService(
25 Ci.nsIClientAuthRememberService
28 var gExpectedClientCertificateChoices;
30 // Mock implementation of nsIClientAuthDialogService.
31 const gClientAuthDialogService = {
32 _state: DialogState.ASSERT_NOT_CALLED,
33 _rememberClientAuthCertificate: false,
34 _chooseCertificateCalled: false,
37 info(`old state: ${this._state}`);
38 this._state = newState;
39 info(`new state: ${this._state}`);
46 set rememberClientAuthCertificate(value) {
47 this._rememberClientAuthCertificate = value;
50 get rememberClientAuthCertificate() {
51 return this._rememberClientAuthCertificate;
54 get chooseCertificateCalled() {
55 return this._chooseCertificateCalled;
58 set chooseCertificateCalled(value) {
59 this._chooseCertificateCalled = value;
62 chooseCertificate(hostname, certArray, loadContext, callback) {
63 this.chooseCertificateCalled = true;
66 DialogState.ASSERT_NOT_CALLED,
67 "chooseCertificate() should be called only when expected"
71 "requireclientcert.example.com",
72 "Hostname should be 'requireclientcert.example.com'"
75 // For mochitests, the cert at build/pgo/certs/mochitest.client should be
76 // selectable as well as one of the PGO certs we loaded in `setup`, so we do
77 // some brief checks to confirm this.
78 Assert.notEqual(certArray, null, "Cert list should not be null");
81 gExpectedClientCertificateChoices,
82 `${gExpectedClientCertificateChoices} certificates should be available`
85 for (let cert of certArray) {
86 Assert.notEqual(cert, null, "Cert list should contain nsIX509Certs");
88 cert.issuerCommonName,
89 "Temporary Certificate Authority",
90 "cert should have expected issuer CN"
94 if (this.state == DialogState.RETURN_CERT_SELECTED) {
95 callback.certificateChosen(
97 this.rememberClientAuthCertificate
100 callback.certificateChosen(null, this.rememberClientAuthCertificate);
104 QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogService"]),
107 add_setup(async function () {
108 let clientAuthDialogServiceCID = MockRegistrar.register(
109 "@mozilla.org/security/ClientAuthDialogService;1",
110 gClientAuthDialogService
112 registerCleanupFunction(() => {
113 MockRegistrar.unregister(clientAuthDialogServiceCID);
116 // This CA has the expected keyCertSign and cRLSign usages. It should not be
117 // presented for use as a client certificate.
118 await readCertificate("pgo-ca-regular-usages.pem", "CTu,CTu,CTu");
119 // This CA has all keyUsages. For compatibility with preexisting behavior, it
120 // will be presented for use as a client certificate.
121 await readCertificate("pgo-ca-all-usages.pem", "CTu,CTu,CTu");
122 // This client certificate was issued by an intermediate that was issued by
123 // the test CA. The server only lists the test CA's subject distinguished name
124 // as an acceptible issuer name for client certificates. If the implementation
125 // can determine that the test CA is a root CA for the client certificate and
126 // thus is acceptible to use, it should be included in the chooseCertificate
127 // callback. At the beginning of this test (speaking of this file as a whole),
128 // the client is not aware of the intermediate, and so it is not available in
130 await readCertificate("client-cert-via-intermediate.pem", ",,");
131 // This certificate has an id-kp-OCSPSigning EKU. Client certificates
132 // shouldn't have this EKU, but there is at least one private PKI where they
133 // do. For interoperability, such certificates will be presented for use.
134 await readCertificate("client-cert-with-ocsp-signing.pem", ",,");
135 gExpectedClientCertificateChoices = 3;
139 * Test helper for the tests below.
141 * @param {string} prefValue
142 * Value to set the "security.default_personal_cert" pref to.
143 * @param {string} urlToNavigate
144 * The URL to navigate to.
145 * @param {string} expectedURL
146 * If the connection is expected to load successfully, the URL that
147 * should load. If the connection is expected to fail and result in an
148 * error page, |undefined|.
149 * @param {boolean} expectCallingChooseCertificate
150 * Determines whether we expect chooseCertificate to be called.
151 * @param {object} options
152 * Optional options object to pass on to the window that gets opened.
153 * @param {string} expectStringInPage
154 * Optional string that is expected to be in the content of the page
157 async function testHelper(
161 expectCallingChooseCertificate,
163 expectStringInPage = undefined
165 gClientAuthDialogService.chooseCertificateCalled = false;
166 await SpecialPowers.pushPrefEnv({
167 set: [["security.default_personal_cert", prefValue]],
170 let win = await BrowserTestUtils.openNewBrowserWindow(options);
172 BrowserTestUtils.startLoadingURIString(
173 win.gBrowser.selectedBrowser,
177 await BrowserTestUtils.browserLoaded(
178 win.gBrowser.selectedBrowser,
180 "https://requireclientcert.example.com/",
183 let loadedURL = win.gBrowser.selectedBrowser.documentURI.spec;
185 loadedURL.startsWith(expectedURL),
186 `Expected and actual URLs should match (got '${loadedURL}', expected '${expectedURL}')`
189 await new Promise(resolve => {
190 let removeEventListener = BrowserTestUtils.addContentEventListener(
191 win.gBrowser.selectedBrowser,
194 removeEventListener();
197 { capture: false, wantUntrusted: true }
203 gClientAuthDialogService.chooseCertificateCalled,
204 expectCallingChooseCertificate,
205 "chooseCertificate should have been called if we were expecting it to be called"
208 if (expectStringInPage) {
209 let pageContent = await SpecialPowers.spawn(
210 win.gBrowser.selectedBrowser,
213 return content.document.body.textContent;
217 pageContent.includes(expectStringInPage),
218 `page should contain the string '${expectStringInPage}' (was '${pageContent}')`
224 // This clears the TLS session cache so we don't use a previously-established
225 // ticket to connect and bypass selecting a client auth certificate in
230 // Test that if a certificate is chosen automatically the connection succeeds,
231 // and that nsIClientAuthDialogService.chooseCertificate() is never called.
232 add_task(async function testCertChosenAutomatically() {
233 gClientAuthDialogService.state = DialogState.ASSERT_NOT_CALLED;
235 "Select Automatically",
236 "https://requireclientcert.example.com/",
237 "https://requireclientcert.example.com/",
240 // This clears all saved client auth certificate state so we don't influence
242 cars.clearRememberedDecisions();
245 // Test that if the user doesn't choose a certificate, the connection fails and
246 // an error page is displayed.
247 add_task(async function testCertNotChosenByUser() {
248 gClientAuthDialogService.state = DialogState.RETURN_CERT_NOT_SELECTED;
251 "https://requireclientcert.example.com/",
255 // bug 1818556: ssltunnel doesn't behave as expected here on Windows
256 AppConstants.platform != "win"
257 ? "SSL_ERROR_RX_CERTIFICATE_REQUIRED_ALERT"
260 cars.clearRememberedDecisions();
263 // Test that if the user chooses a certificate the connection suceeeds.
264 add_task(async function testCertChosenByUser() {
265 gClientAuthDialogService.state = DialogState.RETURN_CERT_SELECTED;
268 "https://requireclientcert.example.com/",
269 "https://requireclientcert.example.com/",
272 cars.clearRememberedDecisions();
275 // Test that the cancel decision is remembered correctly
276 add_task(async function testEmptyCertChosenByUser() {
277 gClientAuthDialogService.state = DialogState.RETURN_CERT_NOT_SELECTED;
278 gClientAuthDialogService.rememberClientAuthCertificate = true;
281 "https://requireclientcert.example.com/",
287 "https://requireclientcert.example.com/",
291 cars.clearRememberedDecisions();
294 // Test that if the user chooses a certificate in a private browsing window,
295 // configures Firefox to remember this certificate for the duration of the
296 // session, closes that window (and thus all private windows), reopens a private
297 // window, and visits that site again, they are re-asked for a certificate (i.e.
298 // any state from the previous private session should be gone). Similarly, after
299 // closing that private window, if the user opens a non-private window, they
300 // again should be asked to choose a certificate (i.e. private state should not
301 // be remembered/used in non-private contexts).
302 add_task(async function testClearPrivateBrowsingState() {
303 gClientAuthDialogService.rememberClientAuthCertificate = true;
304 gClientAuthDialogService.state = DialogState.RETURN_CERT_SELECTED;
307 "https://requireclientcert.example.com/",
308 "https://requireclientcert.example.com/",
316 "https://requireclientcert.example.com/",
317 "https://requireclientcert.example.com/",
325 "https://requireclientcert.example.com/",
326 "https://requireclientcert.example.com/",
329 // NB: we don't `cars.clearRememberedDecisions()` in between the two calls to
330 // `testHelper` because that would clear all client auth certificate state and
331 // obscure what we're testing (that Firefox properly clears the relevant state
332 // when the last private window closes).
333 cars.clearRememberedDecisions();
336 // Test that 3rd party certificates are taken into account when filtering client
337 // certificates based on the acceptible CA list sent by the server.
338 add_task(async function testCertFilteringWithIntermediate() {
339 let intermediateBytes = await IOUtils.readUTF8(
340 getTestFilePath("intermediate.pem")
343 let base64 = pemToBase64(pem);
344 let bin = atob(base64);
346 for (let i = 0; i < bin.length; i++) {
347 bytes.push(bin.charCodeAt(i));
355 let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent);
356 nssComponent.addEnterpriseIntermediate(intermediateBytes);
357 gExpectedClientCertificateChoices = 4;
358 gClientAuthDialogService.state = DialogState.RETURN_CERT_SELECTED;
361 "https://requireclientcert.example.com/",
362 "https://requireclientcert.example.com/",
365 cars.clearRememberedDecisions();
366 // This will reset the added intermediate.
367 await SpecialPowers.pushPrefEnv({
368 set: [["security.enterprise_roots.enabled", true]],
372 // Test that if the server certificate does not validate successfully,
373 // nsIClientAuthDialogService.chooseCertificate() is never called.
374 add_task(async function testNoDialogForUntrustedServerCertificate() {
375 gClientAuthDialogService.state = DialogState.ASSERT_NOT_CALLED;
378 "https://requireclientcert-untrusted.example.com/",
382 // This clears all saved client auth certificate state so we don't influence
384 cars.clearRememberedDecisions();