diff options
Diffstat (limited to 'security/manager/ssl/tests/mochitest/browser/browser_clientAuth_connection.js')
-rw-r--r-- | security/manager/ssl/tests/mochitest/browser/browser_clientAuth_connection.js | 401 |
1 files changed, 401 insertions, 0 deletions
diff --git a/security/manager/ssl/tests/mochitest/browser/browser_clientAuth_connection.js b/security/manager/ssl/tests/mochitest/browser/browser_clientAuth_connection.js new file mode 100644 index 0000000000..d8f57c3e8b --- /dev/null +++ b/security/manager/ssl/tests/mochitest/browser/browser_clientAuth_connection.js @@ -0,0 +1,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(); +}); |