// -*- 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.import("resource://testing-common/MockRegistrar.jsm"); 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", }; let sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing); // 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(ctx, hostname, port, organization, issuerOrg, certList, selectedIndex) { this.chooseCertificateCalled = true; Assert.notEqual(this.state, DialogState.ASSERT_NOT_CALLED, "chooseCertificate() should be called only when expected"); let caud = ctx.QueryInterface(Ci.nsIClientAuthUserDecision); Assert.notEqual(caud, null, "nsIClientAuthUserDecision should be queryable from the " + "given context"); caud.rememberClientAuthCertificate = 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, only the cert at build/pgo/certs/mochitest.client should // be selectable, so we do some brief checks to confirm this. Assert.notEqual(certList, null, "Cert list should not be null"); Assert.equal(certList.length, 1, "Only 1 certificate should be available"); let cert = certList.queryElementAt(0, Ci.nsIX509Cert); Assert.notEqual(cert, null, "Cert list should contain an nsIX509Cert"); Assert.equal(cert.commonName, "Mochitest client", "Cert CN should be 'Mochitest client'"); if (this.state == DialogState.RETURN_CERT_SELECTED) { selectedIndex.value = 0; return true; } return false; }, QueryInterface: ChromeUtils.generateQI([Ci.nsIClientAuthDialogs]), }; add_task(async function setup() { let clientAuthDialogsCID = MockRegistrar.register("@mozilla.org/nsClientAuthDialogs;1", gClientAuthDialogs); registerCleanupFunction(() => { MockRegistrar.unregister(clientAuthDialogsCID); }); }); /** * Test helper for the tests below. * * @param {String} prefValue * Value to set the "security.default_personal_cert" pref 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 {Object} options * Optional options object to pass on to the window that gets opened. */ async function testHelper(prefValue, expectedURL, options = undefined) { gClientAuthDialogs.chooseCertificateCalled = false; await SpecialPowers.pushPrefEnv({"set": [ ["security.default_personal_cert", prefValue], ]}); let win = await BrowserTestUtils.openNewBrowserWindow(options); await BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "https://requireclientcert.example.com:443"); // |loadedURL| will be a string URL if browserLoaded() wins the race, or // |undefined| if waitForErrorPage() wins the race. let loadedURL = await Promise.race([ BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser), BrowserTestUtils.waitForErrorPage(win.gBrowser.selectedBrowser), ]); Assert.equal(expectedURL, loadedURL, "Expected and actual URLs should match"); Assert.equal(gClientAuthDialogs.chooseCertificateCalled, prefValue == "Ask Every Time", "chooseCertificate should have been called if we were expecting it to be called"); 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/"); // This clears all saved client auth certificate state so we don't influence // subsequent tests. sdr.logoutAndTeardown(); }); // 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", undefined); sdr.logoutAndTeardown(); }); // 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/"); sdr.logoutAndTeardown(); }); // 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/", {private: true}); await testHelper("Ask Every Time", "https://requireclientcert.example.com/", {private: true}); await testHelper("Ask Every Time", "https://requireclientcert.example.com/"); // NB: we don't `sdr.logoutAndTeardown()` 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). sdr.logoutAndTeardown(); });