diff --git a/security/manager/pki/nsNSSDialogs.cpp b/security/manager/pki/nsNSSDialogs.cpp index e21697fdc88b..5f641758e4eb 100644 --- a/security/manager/pki/nsNSSDialogs.cpp +++ b/security/manager/pki/nsNSSDialogs.cpp @@ -179,6 +179,8 @@ nsNSSDialogs::ChooseCertificate(nsIInterfaceRequestor* ctx, return NS_ERROR_FAILURE; } + // SetObjects() expects an nsIMutableArray, which is why we can't directly use + // |certList| and have to add an extra layer of indirection. nsCOMPtr paramBlockArray = nsArrayBase::Create(); if (!paramBlockArray) { return NS_ERROR_FAILURE; diff --git a/security/manager/ssl/tests/mochitest/browser/browser.ini b/security/manager/ssl/tests/mochitest/browser/browser.ini index aabdef27fb4b..239e598b08a0 100644 --- a/security/manager/ssl/tests/mochitest/browser/browser.ini +++ b/security/manager/ssl/tests/mochitest/browser/browser.ini @@ -6,3 +6,5 @@ support-files = head.js [browser_certificateManagerLeak.js] [browser_certViewer.js] support-files = *.pem +[browser_clientAuth_connection.js] +[browser_clientAuth_ui.js] 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 000000000000..6362fd34de95 --- /dev/null +++ b/security/manager/ssl/tests/mochitest/browser/browser_clientAuth_connection.js @@ -0,0 +1,135 @@ +// -*- 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 } = + Cu.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, + + set state(newState) { + info(`old state: ${this._state}`); + this._state = newState; + info(`new state: ${this._state}`); + }, + + get state() { + return this._state; + }, + + chooseCertificate(ctx, hostname, port, organization, issuerOrg, certList, + selectedIndex) { + 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 = false; + + 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: XPCOMUtils.generateQI([Ci.nsIClientAuthDialogs]) +}; + +add_task(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|. + */ +function* testHelper(prefValue, expectedURL) { + yield SpecialPowers.pushPrefEnv({"set": [ + ["security.default_personal_cert", prefValue], + ]}); + + yield BrowserTestUtils.loadURI(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 = yield Promise.race([ + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser), + BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser), + ]); + Assert.equal(expectedURL, loadedURL, "Expected and actual URLs should match"); + + // Ensure previously successful connections don't influence future tests. + sdr.logoutAndTeardown(); +} + +// Test that if a certificate is chosen automatically the connection succeeds, +// and that nsIClientAuthDialogs.chooseCertificate() is never called. +add_task(function* testCertChosenAutomatically() { + gClientAuthDialogs.state = DialogState.ASSERT_NOT_CALLED; + yield* testHelper("Select Automatically", + "https://requireclientcert.example.com/"); +}); + +// Test that if the user doesn't choose a certificate, the connection fails and +// an error page is displayed. +add_task(function* testCertNotChosenByUser() { + gClientAuthDialogs.state = DialogState.RETURN_CERT_NOT_SELECTED; + yield* testHelper("Ask Every Time", undefined); +}); + +// Test that if the user chooses a certificate the connection suceeeds. +add_task(function* testCertChosenByUser() { + gClientAuthDialogs.state = DialogState.RETURN_CERT_SELECTED; + yield* testHelper("Ask Every Time", + "https://requireclientcert.example.com/"); +}); diff --git a/security/manager/ssl/tests/mochitest/browser/browser_clientAuth_ui.js b/security/manager/ssl/tests/mochitest/browser/browser_clientAuth_ui.js new file mode 100644 index 000000000000..2c500e2d3035 --- /dev/null +++ b/security/manager/ssl/tests/mochitest/browser/browser_clientAuth_ui.js @@ -0,0 +1,145 @@ +// -*- 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 that the client authentication certificate chooser correctly displays +// provided information and correctly returns user input. + +const TEST_HOSTNAME = "Test Hostname"; +const TEST_ORG = "Test Org"; +const TEST_ISSUER_ORG = "Test Issuer Org"; +const TEST_PORT = 123; + +var certDB = Cc["@mozilla.org/security/x509certdb;1"] + .getService(Ci.nsIX509CertDB); +/** + * Test certificate (i.e. build/pgo/certs/mochitest.client). + * @type nsIX509Cert + */ +var cert; + +/** + * Opens the client auth cert chooser dialog. + * + * @param {nsIX509Cert} cert The cert to pass to the dialog for display. + * @returns {Promise} + * A promise that resolves when the dialog has finished loading, with + * an array consisting of: + * 1. The window of the opened dialog. + * 2. The nsIDialogParamBlock passed to the dialog. + */ +function openClientAuthDialog(cert) { + let params = Cc["@mozilla.org/embedcomp/dialogparam;1"] + .createInstance(Ci.nsIDialogParamBlock); + + let certList = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + certList.appendElement(cert, false); + let array = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + array.appendElement(certList, false); + params.objects = array; + + params.SetString(0, TEST_HOSTNAME); + params.SetString(1, TEST_ORG); + params.SetString(2, TEST_ISSUER_ORG); + params.SetInt(0, TEST_PORT); + + let win = window.openDialog("chrome://pippki/content/clientauthask.xul", "", + "", params); + return new Promise((resolve, reject) => { + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad); + resolve([win, params]); + }); + }); +} + +/** + * Checks that the contents of the given cert chooser dialog match the details + * of build/pgo/certs/mochitest.client. + * + * @param {window} win The cert chooser window. + * @param {String} notBefore + * The notBeforeLocalTime attribute of mochitest.client. + * @param {String} notAfter + * The notAfterLocalTime attribute of mochitest.client. + */ +function checkDialogContents(win, notBefore, notAfter) { + Assert.equal(win.document.getElementById("hostname").textContent, + `${TEST_HOSTNAME}:${TEST_PORT}`, + "Actual and expected hostname and port should be equal"); + // “ and ” don't seem to work when embedded in the following literals, which + // is why escape codes are used instead. + Assert.equal(win.document.getElementById("organization").textContent, + `Organization: \u201C${TEST_ORG}\u201D`, + "Actual and expected organization should be equal"); + Assert.equal(win.document.getElementById("issuer").textContent, + `Issued Under: \u201C${TEST_ISSUER_ORG}\u201D`, + "Actual and expected issuer organization should be equal"); + + Assert.equal(win.document.getElementById("nicknames").label, + "test client certificate [03]", + "Actual and expected selected cert nickname and serial should " + + "be equal"); + + let [subject, serialNum, validity, issuer, tokenName] = + win.document.getElementById("details").value.split("\n"); + Assert.equal(subject, "Issued to: CN=Mochitest client", + "Actual and expected subject should be equal"); + Assert.equal(serialNum, "Serial number: 03", + "Actual and expected serial number should be equal"); + Assert.equal(validity, `Valid from ${notBefore} to ${notAfter}`, + "Actual and expected validity should be equal"); + Assert.equal(issuer, + "Issued by: CN=Temporary Certificate Authority,O=Mozilla " + + "Testing,OU=Profile Guided Optimization", + "Actual and expected issuer should be equal"); + Assert.equal(tokenName, "Stored on: Software Security Device", + "Actual and expected token name should be equal"); +} + +add_task(function* setup() { + cert = certDB.findCertByNickname("test client certificate"); + Assert.notEqual(cert, null, "Should be able to find the test client cert"); +}); + +// Test that the contents of the dialog correspond to the details of the +// provided cert. +add_task(function* testContents() { + let [win, params] = yield openClientAuthDialog(cert); + checkDialogContents(win, cert.validity.notBeforeLocalTime, + cert.validity.notAfterLocalTime); + yield BrowserTestUtils.closeWindow(win); +}); + +// Test that the right values are returned when the dialog is accepted. +add_task(function* testAcceptDialogReturnValues() { + let [win, params] = yield openClientAuthDialog(cert); + win.document.getElementById("rememberBox").checked = true; + info("Accepting dialog"); + win.document.getElementById("certAuthAsk").acceptDialog(); + yield BrowserTestUtils.windowClosed(win); + + Assert.equal(params.GetInt(0), 1, + "1 should be returned to signal user accepted"); + Assert.equal(params.GetInt(1), 0, + "0 should be returned as the selected index"); + Assert.equal(params.GetInt(2), 1, + "1 should be returned as the state of the 'Remember this " + + "decision' checkbox"); +}); + +// Test that the right values are returned when the dialog is canceled. +add_task(function* testCancelDialogReturnValues() { + let [win, params] = yield openClientAuthDialog(cert); + win.document.getElementById("rememberBox").checked = false; + info("Canceling dialog"); + win.document.getElementById("certAuthAsk").cancelDialog(); + yield BrowserTestUtils.windowClosed(win); + + Assert.equal(params.GetInt(0), 0, + "0 should be returned to signal user canceled"); + Assert.equal(params.GetInt(2), 0, + "0 should be returned as the state of the 'Remember this " + + "decision' checkbox"); +});