diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /security/manager/ssl/tests/mochitest/browser/browser_clientAuth_connection.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | security/manager/ssl/tests/mochitest/browser/browser_clientAuth_connection.js | 385 |
1 files changed, 385 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..2eed2b620a --- /dev/null +++ b/security/manager/ssl/tests/mochitest/browser/browser_clientAuth_connection.js @@ -0,0 +1,385 @@ +// -*- 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 nsIClientAuthDialogService.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 nsIClientAuthDialogService. +const gClientAuthDialogService = { + _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, certArray, loadContext, callback) { + this.chooseCertificateCalled = true; + Assert.notEqual( + this.state, + DialogState.ASSERT_NOT_CALLED, + "chooseCertificate() should be called only when expected" + ); + Assert.equal( + hostname, + "requireclientcert.example.com", + "Hostname should be 'requireclientcert.example.com'" + ); + + // 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(certArray, null, "Cert list should not be null"); + Assert.equal( + certArray.length, + gExpectedClientCertificateChoices, + `${gExpectedClientCertificateChoices} certificates should be available` + ); + + for (let cert of certArray) { + 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) { + callback.certificateChosen( + certArray[0], + this.rememberClientAuthCertificate + ); + } else { + callback.certificateChosen(null, this.rememberClientAuthCertificate); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogService"]), +}; + +add_setup(async function () { + let clientAuthDialogServiceCID = MockRegistrar.register( + "@mozilla.org/security/ClientAuthDialogService;1", + gClientAuthDialogService + ); + registerCleanupFunction(() => { + MockRegistrar.unregister(clientAuthDialogServiceCID); + }); + + // 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 +) { + gClientAuthDialogService.chooseCertificateCalled = false; + await SpecialPowers.pushPrefEnv({ + set: [["security.default_personal_cert", prefValue]], + }); + + let win = await BrowserTestUtils.openNewBrowserWindow(options); + + BrowserTestUtils.startLoadingURIString( + 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( + gClientAuthDialogService.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 nsIClientAuthDialogService.chooseCertificate() is never called. +add_task(async function testCertChosenAutomatically() { + gClientAuthDialogService.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() { + gClientAuthDialogService.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() { + gClientAuthDialogService.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() { + gClientAuthDialogService.state = DialogState.RETURN_CERT_NOT_SELECTED; + gClientAuthDialogService.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() { + gClientAuthDialogService.rememberClientAuthCertificate = true; + gClientAuthDialogService.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; + gClientAuthDialogService.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, +// nsIClientAuthDialogService.chooseCertificate() is never called. +add_task(async function testNoDialogForUntrustedServerCertificate() { + gClientAuthDialogService.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(); +}); |