fune/security/manager/ssl/tests/mochitest/browser/browser_clientAuth_connection.js
Dana Keeler 4a553d09d1 bug 1554152 - use the auto-clearing TLS context for connections with origin attributes from private contexts r=KevinJacobs
PSM has two instances of TLS bookkeeping structures ("SharedSSLState"): a
"public" one for most connections and a "private" one that automatically clears
its state when the last private browsing context (usually a window) closes.
Since we moved to separating connections by origin attributes, the latter is
largely redundant because keying by origin attributes already separates
connections from different contexts, even when using the "public" shared TLS
state structure. However, it still has the advantage of clearing its state when
the last private browsing context closes. This patch updates the decision of
which SharedSSLState to use by taking into account origin attributes. That is,
if the origin attributes of the connection has a private browsing ID that isn't
the default (unset), we'll use the auto-clearing SharedSSLState. This has the
effect of auto-clearing cached client auth certificate state for private
contexts when the last private browsing window closes. It also clears
accumulated TLS intolerance state in the private context, but that isn't as
relevant any more since we don't do TLS fallback by default.

Differential Revision: https://phabricator.services.mozilla.com/D33099

--HG--
extra : moz-landing-system : lando
2019-05-31 21:59:10 +00:00

190 lines
7.7 KiB
JavaScript

// -*- 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();
});