Bug 623917 - Add basic client authentication tests. r=keeler

This patch adds tests for the core aspects of the client authentication code,
mainly to ensure the client auth process even works.

MozReview-Commit-ID: DzV4BuwlrDE

--HG--
extra : rebase_source : 43224d3159964f02b175e8c54491b2cabba2cb8a
This commit is contained in:
Cykesiopka 2016-08-12 16:36:43 +08:00
parent b223cdde05
commit cb172720f2
4 changed files with 284 additions and 0 deletions

View File

@ -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<nsIMutableArray> paramBlockArray = nsArrayBase::Create();
if (!paramBlockArray) {
return NS_ERROR_FAILURE;

View File

@ -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]

View File

@ -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/");
});

View File

@ -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");
});