Bug 1938475 [Wayland] Fallback to monitor screen scale if we're missing wayland surfa...
[gecko.git] / security / manager / ssl / tests / mochitest / browser / browser_clientAuth_connection.js
blob2eed2b620a5bef9d9c7ef1945ff01ae47751fed9
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/
4 "use strict";
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"
14 const DialogState = {
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,
36   set state(newState) {
37     info(`old state: ${this._state}`);
38     this._state = newState;
39     info(`new state: ${this._state}`);
40   },
42   get state() {
43     return this._state;
44   },
46   set rememberClientAuthCertificate(value) {
47     this._rememberClientAuthCertificate = value;
48   },
50   get rememberClientAuthCertificate() {
51     return this._rememberClientAuthCertificate;
52   },
54   get chooseCertificateCalled() {
55     return this._chooseCertificateCalled;
56   },
58   set chooseCertificateCalled(value) {
59     this._chooseCertificateCalled = value;
60   },
62   chooseCertificate(hostname, certArray, loadContext, callback) {
63     this.chooseCertificateCalled = true;
64     Assert.notEqual(
65       this.state,
66       DialogState.ASSERT_NOT_CALLED,
67       "chooseCertificate() should be called only when expected"
68     );
69     Assert.equal(
70       hostname,
71       "requireclientcert.example.com",
72       "Hostname should be 'requireclientcert.example.com'"
73     );
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");
79     Assert.equal(
80       certArray.length,
81       gExpectedClientCertificateChoices,
82       `${gExpectedClientCertificateChoices} certificates should be available`
83     );
85     for (let cert of certArray) {
86       Assert.notEqual(cert, null, "Cert list should contain nsIX509Certs");
87       Assert.equal(
88         cert.issuerCommonName,
89         "Temporary Certificate Authority",
90         "cert should have expected issuer CN"
91       );
92     }
94     if (this.state == DialogState.RETURN_CERT_SELECTED) {
95       callback.certificateChosen(
96         certArray[0],
97         this.rememberClientAuthCertificate
98       );
99     } else {
100       callback.certificateChosen(null, this.rememberClientAuthCertificate);
101     }
102   },
104   QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogService"]),
107 add_setup(async function () {
108   let clientAuthDialogServiceCID = MockRegistrar.register(
109     "@mozilla.org/security/ClientAuthDialogService;1",
110     gClientAuthDialogService
111   );
112   registerCleanupFunction(() => {
113     MockRegistrar.unregister(clientAuthDialogServiceCID);
114   });
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
129   // the callback.
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
155  *        once it loads.
156  */
157 async function testHelper(
158   prefValue,
159   urlToNavigate,
160   expectedURL,
161   expectCallingChooseCertificate,
162   options = undefined,
163   expectStringInPage = undefined
164 ) {
165   gClientAuthDialogService.chooseCertificateCalled = false;
166   await SpecialPowers.pushPrefEnv({
167     set: [["security.default_personal_cert", prefValue]],
168   });
170   let win = await BrowserTestUtils.openNewBrowserWindow(options);
172   BrowserTestUtils.startLoadingURIString(
173     win.gBrowser.selectedBrowser,
174     urlToNavigate
175   );
176   if (expectedURL) {
177     await BrowserTestUtils.browserLoaded(
178       win.gBrowser.selectedBrowser,
179       false,
180       "https://requireclientcert.example.com/",
181       true
182     );
183     let loadedURL = win.gBrowser.selectedBrowser.documentURI.spec;
184     Assert.ok(
185       loadedURL.startsWith(expectedURL),
186       `Expected and actual URLs should match (got '${loadedURL}', expected '${expectedURL}')`
187     );
188   } else {
189     await new Promise(resolve => {
190       let removeEventListener = BrowserTestUtils.addContentEventListener(
191         win.gBrowser.selectedBrowser,
192         "AboutNetErrorLoad",
193         () => {
194           removeEventListener();
195           resolve();
196         },
197         { capture: false, wantUntrusted: true }
198       );
199     });
200   }
202   Assert.equal(
203     gClientAuthDialogService.chooseCertificateCalled,
204     expectCallingChooseCertificate,
205     "chooseCertificate should have been called if we were expecting it to be called"
206   );
208   if (expectStringInPage) {
209     let pageContent = await SpecialPowers.spawn(
210       win.gBrowser.selectedBrowser,
211       [],
212       async function () {
213         return content.document.body.textContent;
214       }
215     );
216     Assert.ok(
217       pageContent.includes(expectStringInPage),
218       `page should contain the string '${expectStringInPage}' (was '${pageContent}')`
219     );
220   }
222   await win.close();
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
226   // subsequent tests.
227   sdr.logout();
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;
234   await testHelper(
235     "Select Automatically",
236     "https://requireclientcert.example.com/",
237     "https://requireclientcert.example.com/",
238     false
239   );
240   // This clears all saved client auth certificate state so we don't influence
241   // subsequent tests.
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;
249   await testHelper(
250     "Ask Every Time",
251     "https://requireclientcert.example.com/",
252     undefined,
253     true,
254     undefined,
255     // bug 1818556: ssltunnel doesn't behave as expected here on Windows
256     AppConstants.platform != "win"
257       ? "SSL_ERROR_RX_CERTIFICATE_REQUIRED_ALERT"
258       : undefined
259   );
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;
266   await testHelper(
267     "Ask Every Time",
268     "https://requireclientcert.example.com/",
269     "https://requireclientcert.example.com/",
270     true
271   );
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;
279   await testHelper(
280     "Ask Every Time",
281     "https://requireclientcert.example.com/",
282     undefined,
283     true
284   );
285   await testHelper(
286     "Ask Every Time",
287     "https://requireclientcert.example.com/",
288     undefined,
289     false
290   );
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;
305   await testHelper(
306     "Ask Every Time",
307     "https://requireclientcert.example.com/",
308     "https://requireclientcert.example.com/",
309     true,
310     {
311       private: true,
312     }
313   );
314   await testHelper(
315     "Ask Every Time",
316     "https://requireclientcert.example.com/",
317     "https://requireclientcert.example.com/",
318     true,
319     {
320       private: true,
321     }
322   );
323   await testHelper(
324     "Ask Every Time",
325     "https://requireclientcert.example.com/",
326     "https://requireclientcert.example.com/",
327     true
328   );
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")
341   ).then(
342     pem => {
343       let base64 = pemToBase64(pem);
344       let bin = atob(base64);
345       let bytes = [];
346       for (let i = 0; i < bin.length; i++) {
347         bytes.push(bin.charCodeAt(i));
348       }
349       return bytes;
350     },
351     error => {
352       throw error;
353     }
354   );
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;
359   await testHelper(
360     "Ask Every Time",
361     "https://requireclientcert.example.com/",
362     "https://requireclientcert.example.com/",
363     true
364   );
365   cars.clearRememberedDecisions();
366   // This will reset the added intermediate.
367   await SpecialPowers.pushPrefEnv({
368     set: [["security.enterprise_roots.enabled", true]],
369   });
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;
376   await testHelper(
377     "Ask Every Time",
378     "https://requireclientcert-untrusted.example.com/",
379     undefined,
380     false
381   );
382   // This clears all saved client auth certificate state so we don't influence
383   // subsequent tests.
384   cars.clearRememberedDecisions();