1349 lines
44 KiB
JavaScript
1349 lines
44 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
"use strict";
|
|
|
|
const { AppConstants } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/AppConstants.sys.mjs"
|
|
);
|
|
const { ctypes } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/ctypes.sys.mjs"
|
|
);
|
|
const { FileUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/FileUtils.sys.mjs"
|
|
);
|
|
const { HttpServer } = ChromeUtils.importESModule(
|
|
"resource://testing-common/httpd.sys.mjs"
|
|
);
|
|
const { MockRegistrar } = ChromeUtils.importESModule(
|
|
"resource://testing-common/MockRegistrar.sys.mjs"
|
|
);
|
|
const { NetUtil } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/NetUtil.sys.mjs"
|
|
);
|
|
const { XPCOMUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
|
);
|
|
|
|
const { X509 } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/psm/X509.sys.mjs"
|
|
);
|
|
|
|
const gIsDebugBuild = Cc["@mozilla.org/xpcom/debug;1"].getService(
|
|
Ci.nsIDebug2
|
|
).isDebugBuild;
|
|
|
|
// The test EV roots are only enabled in debug builds as a security measure.
|
|
const gEVExpected = gIsDebugBuild;
|
|
|
|
const CLIENT_AUTH_FILE_NAME = "ClientAuthRememberList.bin";
|
|
const SSS_STATE_FILE_NAME = "SiteSecurityServiceState.bin";
|
|
const SSS_STATE_OLD_FILE_NAME = "SiteSecurityServiceState.txt";
|
|
const CERT_OVERRIDE_FILE_NAME = "cert_override.txt";
|
|
|
|
const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
|
|
const SSL_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE;
|
|
const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE;
|
|
|
|
// This isn't really a valid PRErrorCode, but is useful for signalling that
|
|
// a test is expected to succeed.
|
|
const PRErrorCodeSuccess = 0;
|
|
|
|
// Sort in numerical order
|
|
const SEC_ERROR_INVALID_TIME = SEC_ERROR_BASE + 8;
|
|
const SEC_ERROR_BAD_DER = SEC_ERROR_BASE + 9;
|
|
const SEC_ERROR_BAD_SIGNATURE = SEC_ERROR_BASE + 10;
|
|
const SEC_ERROR_EXPIRED_CERTIFICATE = SEC_ERROR_BASE + 11;
|
|
const SEC_ERROR_REVOKED_CERTIFICATE = SEC_ERROR_BASE + 12;
|
|
const SEC_ERROR_UNKNOWN_ISSUER = SEC_ERROR_BASE + 13;
|
|
const SEC_ERROR_UNTRUSTED_ISSUER = SEC_ERROR_BASE + 20;
|
|
const SEC_ERROR_UNTRUSTED_CERT = SEC_ERROR_BASE + 21;
|
|
const SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE = SEC_ERROR_BASE + 30;
|
|
const SEC_ERROR_CA_CERT_INVALID = SEC_ERROR_BASE + 36;
|
|
const SEC_ERROR_UNKNOWN_CRITICAL_EXTENSION = SEC_ERROR_BASE + 41;
|
|
const SEC_ERROR_PKCS7_BAD_SIGNATURE = SEC_ERROR_BASE + 47;
|
|
const SEC_ERROR_INADEQUATE_KEY_USAGE = SEC_ERROR_BASE + 90;
|
|
const SEC_ERROR_INADEQUATE_CERT_TYPE = SEC_ERROR_BASE + 91;
|
|
const SEC_ERROR_CERT_NOT_IN_NAME_SPACE = SEC_ERROR_BASE + 112;
|
|
const SEC_ERROR_CERT_BAD_ACCESS_LOCATION = SEC_ERROR_BASE + 117;
|
|
const SEC_ERROR_OCSP_MALFORMED_REQUEST = SEC_ERROR_BASE + 120;
|
|
const SEC_ERROR_OCSP_SERVER_ERROR = SEC_ERROR_BASE + 121;
|
|
const SEC_ERROR_OCSP_TRY_SERVER_LATER = SEC_ERROR_BASE + 122;
|
|
const SEC_ERROR_OCSP_REQUEST_NEEDS_SIG = SEC_ERROR_BASE + 123;
|
|
const SEC_ERROR_OCSP_UNAUTHORIZED_REQUEST = SEC_ERROR_BASE + 124;
|
|
const SEC_ERROR_OCSP_UNKNOWN_CERT = SEC_ERROR_BASE + 126;
|
|
const SEC_ERROR_OCSP_MALFORMED_RESPONSE = SEC_ERROR_BASE + 129;
|
|
const SEC_ERROR_OCSP_UNAUTHORIZED_RESPONSE = SEC_ERROR_BASE + 130;
|
|
const SEC_ERROR_OCSP_OLD_RESPONSE = SEC_ERROR_BASE + 132;
|
|
const SEC_ERROR_UNSUPPORTED_ELLIPTIC_CURVE = SEC_ERROR_BASE + 141;
|
|
const SEC_ERROR_OCSP_INVALID_SIGNING_CERT = SEC_ERROR_BASE + 144;
|
|
const SEC_ERROR_POLICY_VALIDATION_FAILED = SEC_ERROR_BASE + 160;
|
|
const SEC_ERROR_OCSP_BAD_SIGNATURE = SEC_ERROR_BASE + 157;
|
|
const SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED = SEC_ERROR_BASE + 176;
|
|
|
|
const SSL_ERROR_NO_CYPHER_OVERLAP = SSL_ERROR_BASE + 2;
|
|
const SSL_ERROR_BAD_CERT_DOMAIN = SSL_ERROR_BASE + 12;
|
|
const SSL_ERROR_BAD_CERT_ALERT = SSL_ERROR_BASE + 17;
|
|
const SSL_ERROR_WEAK_SERVER_CERT_KEY = SSL_ERROR_BASE + 132;
|
|
const SSL_ERROR_DC_INVALID_KEY_USAGE = SSL_ERROR_BASE + 184;
|
|
|
|
const SSL_ERROR_ECH_RETRY_WITH_ECH = SSL_ERROR_BASE + 188;
|
|
const SSL_ERROR_ECH_RETRY_WITHOUT_ECH = SSL_ERROR_BASE + 189;
|
|
const SSL_ERROR_ECH_FAILED = SSL_ERROR_BASE + 190;
|
|
const SSL_ERROR_ECH_REQUIRED_ALERT = SSL_ERROR_BASE + 191;
|
|
|
|
const MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE = MOZILLA_PKIX_ERROR_BASE + 0;
|
|
const MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY =
|
|
MOZILLA_PKIX_ERROR_BASE + 1;
|
|
const MOZILLA_PKIX_ERROR_INADEQUATE_KEY_SIZE = MOZILLA_PKIX_ERROR_BASE + 2;
|
|
const MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA = MOZILLA_PKIX_ERROR_BASE + 3;
|
|
const MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE =
|
|
MOZILLA_PKIX_ERROR_BASE + 5;
|
|
const MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE =
|
|
MOZILLA_PKIX_ERROR_BASE + 6;
|
|
const MOZILLA_PKIX_ERROR_OCSP_RESPONSE_FOR_CERT_MISSING =
|
|
MOZILLA_PKIX_ERROR_BASE + 8;
|
|
const MOZILLA_PKIX_ERROR_REQUIRED_TLS_FEATURE_MISSING =
|
|
MOZILLA_PKIX_ERROR_BASE + 10;
|
|
const MOZILLA_PKIX_ERROR_EMPTY_ISSUER_NAME = MOZILLA_PKIX_ERROR_BASE + 12;
|
|
const MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED =
|
|
MOZILLA_PKIX_ERROR_BASE + 13;
|
|
const MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT = MOZILLA_PKIX_ERROR_BASE + 14;
|
|
const MOZILLA_PKIX_ERROR_MITM_DETECTED = MOZILLA_PKIX_ERROR_BASE + 15;
|
|
const MOZILLA_PKIX_ERROR_INSUFFICIENT_CERTIFICATE_TRANSPARENCY =
|
|
MOZILLA_PKIX_ERROR_BASE + 16;
|
|
const MOZILLA_PKIX_ERROR_ISSUER_NO_LONGER_TRUSTED =
|
|
MOZILLA_PKIX_ERROR_BASE + 17;
|
|
|
|
// A map from the name of a certificate usage to the value of the usage.
|
|
// Useful for printing debugging information and for enumerating all supported
|
|
// usages.
|
|
const verifyUsages = new Map([
|
|
["verifyUsageTLSClient", Ci.nsIX509CertDB.verifyUsageTLSClient],
|
|
["verifyUsageTLSServer", Ci.nsIX509CertDB.verifyUsageTLSServer],
|
|
["verifyUsageTLSServerCA", Ci.nsIX509CertDB.verifyUsageTLSServerCA],
|
|
["verifyUsageEmailSigner", Ci.nsIX509CertDB.verifyUsageEmailSigner],
|
|
["verifyUsageEmailRecipient", Ci.nsIX509CertDB.verifyUsageEmailRecipient],
|
|
]);
|
|
|
|
const NO_FLAGS = 0;
|
|
|
|
const CRLiteModeDisabledPrefValue = 0;
|
|
const CRLiteModeTelemetryOnlyPrefValue = 1;
|
|
const CRLiteModeEnforcePrefValue = 2;
|
|
const CRLiteModeConfirmRevocationsValue = 3;
|
|
|
|
// Convert a string to an array of bytes consisting of the char code at each
|
|
// index.
|
|
function stringToArray(s) {
|
|
let a = [];
|
|
for (let i = 0; i < s.length; i++) {
|
|
a.push(s.charCodeAt(i));
|
|
}
|
|
return a;
|
|
}
|
|
|
|
// Converts an array of bytes to a JS string using fromCharCode on each byte.
|
|
function arrayToString(a) {
|
|
let s = "";
|
|
for (let b of a) {
|
|
s += String.fromCharCode(b);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// Commonly certificates are represented as PEM. The format is roughly as
|
|
// follows:
|
|
//
|
|
// -----BEGIN CERTIFICATE-----
|
|
// [some lines of base64, each typically 64 characters long]
|
|
// -----END CERTIFICATE-----
|
|
//
|
|
// However, nsIX509CertDB.constructX509FromBase64 and related functions do not
|
|
// handle input of this form. Instead, they require a single string of base64
|
|
// with no newlines or BEGIN/END headers. This is a helper function to convert
|
|
// PEM to the format that nsIX509CertDB requires.
|
|
function pemToBase64(pem) {
|
|
return pem
|
|
.replace(/-----BEGIN CERTIFICATE-----/, "")
|
|
.replace(/-----END CERTIFICATE-----/, "")
|
|
.replace(/[\r\n]/g, "");
|
|
}
|
|
|
|
function build_cert_chain(certNames, testDirectory = "bad_certs") {
|
|
let certList = [];
|
|
certNames.forEach(function (certName) {
|
|
let cert = constructCertFromFile(`${testDirectory}/${certName}.pem`);
|
|
certList.push(cert);
|
|
});
|
|
return certList;
|
|
}
|
|
|
|
function areCertsEqual(certA, certB) {
|
|
let derA = certA.getRawDER();
|
|
let derB = certB.getRawDER();
|
|
if (derA.length != derB.length) {
|
|
return false;
|
|
}
|
|
for (let i = 0; i < derA.length; i++) {
|
|
if (derA[i] != derB[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function areCertArraysEqual(certArrayA, certArrayB) {
|
|
if (certArrayA.length != certArrayB.length) {
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < certArrayA.length; i++) {
|
|
const certA = certArrayA[i];
|
|
const certB = certArrayB[i];
|
|
if (!areCertsEqual(certA, certB)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function readFile(file) {
|
|
let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
|
|
Ci.nsIFileInputStream
|
|
);
|
|
fstream.init(file, -1, 0, 0);
|
|
let available = fstream.available();
|
|
let data =
|
|
available > 0 ? NetUtil.readInputStreamToString(fstream, available) : "";
|
|
fstream.close();
|
|
return data;
|
|
}
|
|
|
|
function addCertFromFile(certdb, filename, trustString) {
|
|
let certFile = do_get_file(filename, false);
|
|
let certBytes = readFile(certFile);
|
|
try {
|
|
return certdb.addCert(certBytes, trustString);
|
|
} catch (e) {}
|
|
// It might be PEM instead of DER.
|
|
return certdb.addCertFromBase64(pemToBase64(certBytes), trustString);
|
|
}
|
|
|
|
function constructCertFromFile(filename) {
|
|
let certFile = do_get_file(filename, false);
|
|
let certBytes = readFile(certFile);
|
|
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
|
|
Ci.nsIX509CertDB
|
|
);
|
|
try {
|
|
return certdb.constructX509(stringToArray(certBytes));
|
|
} catch (e) {}
|
|
// It might be PEM instead of DER.
|
|
return certdb.constructX509FromBase64(pemToBase64(certBytes));
|
|
}
|
|
|
|
function setCertTrust(cert, trustString) {
|
|
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
|
|
Ci.nsIX509CertDB
|
|
);
|
|
certdb.setCertTrustFromString(cert, trustString);
|
|
}
|
|
|
|
function getXPCOMStatusFromNSS(statusNSS) {
|
|
let nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService(
|
|
Ci.nsINSSErrorsService
|
|
);
|
|
return nssErrorsService.getXPCOMFromNSSError(statusNSS);
|
|
}
|
|
|
|
// Helper for checkCertErrorGenericAtTime
|
|
class CertVerificationExpectedErrorResult {
|
|
constructor(certName, expectedError, expectedEVStatus, resolve) {
|
|
this.certName = certName;
|
|
this.expectedError = expectedError;
|
|
this.expectedEVStatus = expectedEVStatus;
|
|
this.resolve = resolve;
|
|
}
|
|
|
|
verifyCertFinished(aPRErrorCode, aVerifiedChain, aHasEVPolicy) {
|
|
equal(
|
|
aPRErrorCode,
|
|
this.expectedError,
|
|
`verifying ${this.certName}: should get error ${this.expectedError}`
|
|
);
|
|
if (this.expectedEVStatus != undefined) {
|
|
equal(
|
|
aHasEVPolicy,
|
|
this.expectedEVStatus,
|
|
`verifying ${this.certName}: ` +
|
|
`should ${this.expectedEVStatus ? "be" : "not be"} EV`
|
|
);
|
|
}
|
|
this.resolve();
|
|
}
|
|
}
|
|
|
|
// certdb implements nsIX509CertDB. See nsIX509CertDB.idl for documentation.
|
|
// In particular, hostname is optional.
|
|
function checkCertErrorGenericAtTime(
|
|
certdb,
|
|
cert,
|
|
expectedError,
|
|
usage,
|
|
time,
|
|
/* optional */ isEVExpected,
|
|
/* optional */ hostname,
|
|
/* optional */ flags = NO_FLAGS
|
|
) {
|
|
return new Promise(resolve => {
|
|
let result = new CertVerificationExpectedErrorResult(
|
|
cert.commonName,
|
|
expectedError,
|
|
isEVExpected,
|
|
resolve
|
|
);
|
|
certdb.asyncVerifyCertAtTime(cert, usage, flags, hostname, time, result);
|
|
});
|
|
}
|
|
|
|
// certdb implements nsIX509CertDB. See nsIX509CertDB.idl for documentation.
|
|
// In particular, hostname is optional.
|
|
function checkCertErrorGeneric(
|
|
certdb,
|
|
cert,
|
|
expectedError,
|
|
usage,
|
|
/* optional */ isEVExpected,
|
|
/* optional */ hostname
|
|
) {
|
|
let now = new Date().getTime() / 1000;
|
|
return checkCertErrorGenericAtTime(
|
|
certdb,
|
|
cert,
|
|
expectedError,
|
|
usage,
|
|
now,
|
|
isEVExpected,
|
|
hostname
|
|
);
|
|
}
|
|
|
|
// Helper for checkRootOfBuiltChain
|
|
class CertVerificationExpectedRootResult {
|
|
constructor(certName, rootSha256SpkiDigest, resolve) {
|
|
this.certName = certName;
|
|
this.rootSha256SpkiDigest = rootSha256SpkiDigest;
|
|
this.resolve = resolve;
|
|
}
|
|
|
|
verifyCertFinished(aPRErrorCode, aVerifiedChain, _aHasEVPolicy) {
|
|
equal(
|
|
aPRErrorCode,
|
|
PRErrorCodeSuccess,
|
|
`verifying ${this.certName}: should succeed`
|
|
);
|
|
equal(
|
|
aVerifiedChain[aVerifiedChain.length - 1]
|
|
.sha256SubjectPublicKeyInfoDigest,
|
|
this.rootSha256SpkiDigest,
|
|
`verifying ${this.certName}: should build chain to ${this.rootSha256SpkiDigest}`
|
|
);
|
|
this.resolve();
|
|
}
|
|
}
|
|
|
|
function checkRootOfBuiltChain(
|
|
certdb,
|
|
cert,
|
|
rootSha256SpkiDigest,
|
|
time,
|
|
/* optional */ hostname,
|
|
/* optional */ flags = NO_FLAGS
|
|
) {
|
|
return new Promise(resolve => {
|
|
let result = new CertVerificationExpectedRootResult(
|
|
cert.commonName,
|
|
rootSha256SpkiDigest,
|
|
resolve
|
|
);
|
|
certdb.asyncVerifyCertAtTime(
|
|
cert,
|
|
Ci.nsIX509CertDB.verifyUsageTLSServer,
|
|
flags,
|
|
hostname,
|
|
time,
|
|
result
|
|
);
|
|
});
|
|
}
|
|
|
|
function checkEVStatus(certDB, cert, usage, isEVExpected) {
|
|
return checkCertErrorGeneric(
|
|
certDB,
|
|
cert,
|
|
PRErrorCodeSuccess,
|
|
usage,
|
|
isEVExpected
|
|
);
|
|
}
|
|
|
|
function _getLibraryFunctionWithNoArguments(
|
|
functionName,
|
|
libraryName,
|
|
returnType
|
|
) {
|
|
// Open the NSS library. copied from services/crypto/modules/WeaveCrypto.js
|
|
let path = ctypes.libraryName(libraryName);
|
|
|
|
// XXX really want to be able to pass specific dlopen flags here.
|
|
let nsslib;
|
|
try {
|
|
nsslib = ctypes.open(path);
|
|
} catch (e) {
|
|
// In case opening the library without a full path fails,
|
|
// try again with a full path.
|
|
let file = Services.dirsvc.get("GreBinD", Ci.nsIFile);
|
|
file.append(path);
|
|
nsslib = ctypes.open(file.path);
|
|
}
|
|
|
|
let SECStatus = ctypes.int;
|
|
let func = nsslib.declare(
|
|
functionName,
|
|
ctypes.default_abi,
|
|
returnType || SECStatus
|
|
);
|
|
return func;
|
|
}
|
|
|
|
function clearOCSPCache() {
|
|
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
|
|
Ci.nsIX509CertDB
|
|
);
|
|
certdb.clearOCSPCache();
|
|
}
|
|
|
|
function clearSessionCache() {
|
|
let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent);
|
|
nssComponent.clearSSLExternalAndInternalSessionCache();
|
|
}
|
|
|
|
function getSSLStatistics() {
|
|
let SSL3Statistics = new ctypes.StructType("SSL3Statistics", [
|
|
{ sch_sid_cache_hits: ctypes.long },
|
|
{ sch_sid_cache_misses: ctypes.long },
|
|
{ sch_sid_cache_not_ok: ctypes.long },
|
|
{ hsh_sid_cache_hits: ctypes.long },
|
|
{ hsh_sid_cache_misses: ctypes.long },
|
|
{ hsh_sid_cache_not_ok: ctypes.long },
|
|
{ hch_sid_cache_hits: ctypes.long },
|
|
{ hch_sid_cache_misses: ctypes.long },
|
|
{ hch_sid_cache_not_ok: ctypes.long },
|
|
{ sch_sid_stateless_resumes: ctypes.long },
|
|
{ hsh_sid_stateless_resumes: ctypes.long },
|
|
{ hch_sid_stateless_resumes: ctypes.long },
|
|
{ hch_sid_ticket_parse_failures: ctypes.long },
|
|
]);
|
|
let SSL3StatisticsPtr = new ctypes.PointerType(SSL3Statistics);
|
|
let SSL_GetStatistics = null;
|
|
try {
|
|
SSL_GetStatistics = _getLibraryFunctionWithNoArguments(
|
|
"SSL_GetStatistics",
|
|
"ssl3",
|
|
SSL3StatisticsPtr
|
|
);
|
|
} catch (e) {
|
|
// On Windows, this is actually in the nss3 library.
|
|
SSL_GetStatistics = _getLibraryFunctionWithNoArguments(
|
|
"SSL_GetStatistics",
|
|
"nss3",
|
|
SSL3StatisticsPtr
|
|
);
|
|
}
|
|
if (!SSL_GetStatistics) {
|
|
throw new Error("Failed to get SSL statistics");
|
|
}
|
|
return SSL_GetStatistics();
|
|
}
|
|
|
|
// Set up a TLS testing environment that has a TLS server running and
|
|
// ready to accept connections. This async function starts the server and
|
|
// waits for the server to indicate that it is ready.
|
|
//
|
|
// Each test should have its own subdomain of example.com, for example
|
|
// my-first-connection-test.example.com. The server can use the server
|
|
// name (passed through the SNI TLS extension) to determine what behavior
|
|
// the server side of the text should exhibit. See TLSServer.h for more
|
|
// information on how to write the server side of tests.
|
|
//
|
|
// Create a new source file for your new server executable in
|
|
// security/manager/ssl/tests/unit/tlsserver/cmd similar to the other ones in
|
|
// that directory, and add a reference to it to the sources variable in that
|
|
// directory's moz.build.
|
|
//
|
|
// Modify TEST_HARNESS_BINS in
|
|
// testing/mochitest/Makefile.in and NO_PKG_FILES in
|
|
// toolkit/mozapps/installer/packager.mk to make sure the new executable
|
|
// gets included in the packages used for shipping the tests to the test
|
|
// runners in our build/test farm. (Things will work fine locally without
|
|
// these changes but will break on TBPL.)
|
|
//
|
|
// Your test script should look something like this:
|
|
/*
|
|
|
|
// -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
"use strict";
|
|
|
|
// <documentation on your test>
|
|
|
|
function run_test() {
|
|
do_get_profile();
|
|
add_tls_server_setup("<test-server-name>", "<path-to-certificate-directory>");
|
|
|
|
add_connection_test("<test-name-1>.example.com",
|
|
SEC_ERROR_xxx,
|
|
function() { ... },
|
|
function(aTransportSecurityInfo) { ... },
|
|
function(aTransport) { ... });
|
|
[...]
|
|
add_connection_test("<test-name-n>.example.com", PRErrorCodeSuccess);
|
|
|
|
run_next_test();
|
|
}
|
|
*/
|
|
|
|
function add_tls_server_setup(serverBinName, certsPath, addDefaultRoot = true) {
|
|
add_test(function () {
|
|
_setupTLSServerTest(serverBinName, certsPath, addDefaultRoot);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a TLS connection test case.
|
|
*
|
|
* @param {string} aHost
|
|
* The hostname to pass in the SNI TLS extension; this should unambiguously
|
|
* identify which test is being run.
|
|
* @param {PRErrorCode} aExpectedResult
|
|
* The expected result of the connection. If an error is not expected, pass
|
|
* in PRErrorCodeSuccess.
|
|
* @param {Function} aBeforeConnect
|
|
* A callback function that takes no arguments that will be called before the
|
|
* connection is attempted.
|
|
* @param {Function} aWithSecurityInfo
|
|
* A callback function that takes an nsITransportSecurityInfo, which is called
|
|
* after the TLS handshake succeeds.
|
|
* @param {Function} aAfterStreamOpen
|
|
* A callback function that is called with the nsISocketTransport once the
|
|
* output stream is ready.
|
|
* @param {OriginAttributes} aOriginAttributes (optional)
|
|
* The origin attributes that the socket transport will have. This parameter
|
|
* affects OCSP because OCSP cache is double-keyed by origin attributes' first
|
|
* party domain.
|
|
*
|
|
* @param {OriginAttributes} aEchConfig (optional)
|
|
* A Base64-encoded ECHConfig. If non-empty, it will be configured to the client
|
|
* socket resulting in an Encrypted Client Hello extension being sent. The client
|
|
* keypair is ephermeral and generated within NSS.
|
|
*/
|
|
function add_connection_test(
|
|
aHost,
|
|
aExpectedResult,
|
|
aBeforeConnect,
|
|
aWithSecurityInfo,
|
|
aAfterStreamOpen,
|
|
/* optional */ aOriginAttributes,
|
|
/* optional */ aEchConfig
|
|
) {
|
|
add_test(function () {
|
|
if (aBeforeConnect) {
|
|
aBeforeConnect();
|
|
}
|
|
asyncConnectTo(
|
|
aHost,
|
|
aExpectedResult,
|
|
aWithSecurityInfo,
|
|
aAfterStreamOpen,
|
|
aOriginAttributes,
|
|
aEchConfig
|
|
).then(run_next_test);
|
|
});
|
|
}
|
|
|
|
async function asyncConnectTo(
|
|
aHost,
|
|
aExpectedResult,
|
|
/* optional */ aWithSecurityInfo = undefined,
|
|
/* optional */ aAfterStreamOpen = undefined,
|
|
/* optional */ aOriginAttributes = undefined,
|
|
/* optional */ aEchConfig = undefined
|
|
) {
|
|
const REMOTE_PORT = 8443;
|
|
|
|
function Connection(host) {
|
|
this.host = host;
|
|
this.thread = Services.tm.currentThread;
|
|
this.defer = Promise.withResolvers();
|
|
let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
|
|
Ci.nsISocketTransportService
|
|
);
|
|
this.transport = sts.createTransport(
|
|
["ssl"],
|
|
host,
|
|
REMOTE_PORT,
|
|
null,
|
|
null
|
|
);
|
|
if (aEchConfig) {
|
|
this.transport.setEchConfig(atob(aEchConfig));
|
|
}
|
|
// See bug 1129771 - attempting to connect to [::1] when the server is
|
|
// listening on 127.0.0.1 causes frequent failures on OS X 10.10.
|
|
this.transport.connectionFlags |= Ci.nsISocketTransport.DISABLE_IPV6;
|
|
this.transport.setEventSink(this, this.thread);
|
|
if (aOriginAttributes) {
|
|
this.transport.originAttributes = aOriginAttributes;
|
|
}
|
|
this.inputStream = null;
|
|
this.outputStream = null;
|
|
this.connected = false;
|
|
}
|
|
|
|
Connection.prototype = {
|
|
// nsITransportEventSink
|
|
onTransportStatus(aTransport, aStatus) {
|
|
if (
|
|
!this.connected &&
|
|
aStatus == Ci.nsISocketTransport.STATUS_CONNECTED_TO
|
|
) {
|
|
this.connected = true;
|
|
this.outputStream.asyncWait(this, 0, 0, this.thread);
|
|
}
|
|
},
|
|
|
|
// nsIInputStreamCallback
|
|
onInputStreamReady(aStream) {
|
|
try {
|
|
// this will throw if the stream has been closed by an error
|
|
let str = NetUtil.readInputStreamToString(aStream, aStream.available());
|
|
Assert.equal(str, "0", "Should have received ASCII '0' from server");
|
|
this.inputStream.close();
|
|
this.outputStream.close();
|
|
this.result = Cr.NS_OK;
|
|
} catch (e) {
|
|
this.result = e.result;
|
|
}
|
|
this.defer.resolve(this);
|
|
},
|
|
|
|
// nsIOutputStreamCallback
|
|
onOutputStreamReady() {
|
|
if (aAfterStreamOpen) {
|
|
aAfterStreamOpen(this.transport);
|
|
}
|
|
this.outputStream.write("0", 1);
|
|
let inStream = this.transport
|
|
.openInputStream(0, 0, 0)
|
|
.QueryInterface(Ci.nsIAsyncInputStream);
|
|
this.inputStream = inStream;
|
|
this.inputStream.asyncWait(this, 0, 0, this.thread);
|
|
},
|
|
|
|
go() {
|
|
this.outputStream = this.transport
|
|
.openOutputStream(0, 0, 0)
|
|
.QueryInterface(Ci.nsIAsyncOutputStream);
|
|
return this.defer.promise;
|
|
},
|
|
};
|
|
|
|
/* Returns a promise to connect to host that resolves to the result of that
|
|
* connection */
|
|
function connectTo(host) {
|
|
Services.prefs.setCharPref("network.dns.localDomains", host);
|
|
let connection = new Connection(host);
|
|
return connection.go();
|
|
}
|
|
|
|
return connectTo(aHost).then(async function (conn) {
|
|
info("handling " + aHost);
|
|
let expectedNSResult =
|
|
aExpectedResult == PRErrorCodeSuccess
|
|
? Cr.NS_OK
|
|
: getXPCOMStatusFromNSS(aExpectedResult);
|
|
Assert.equal(
|
|
conn.result,
|
|
expectedNSResult,
|
|
"Actual and expected connection result should match"
|
|
);
|
|
if (aWithSecurityInfo) {
|
|
aWithSecurityInfo(
|
|
await conn.transport.tlsSocketControl.asyncGetSecurityInfo()
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
function _getBinaryUtil(binaryUtilName) {
|
|
let utilBin = Services.dirsvc.get("GreD", Ci.nsIFile);
|
|
// On macOS, GreD is .../Contents/Resources, and most binary utilities
|
|
// are located there, but certutil is in GreBinD (or .../Contents/MacOS),
|
|
// so we have to change the path accordingly.
|
|
if (binaryUtilName === "certutil") {
|
|
utilBin = Services.dirsvc.get("GreBinD", Ci.nsIFile);
|
|
}
|
|
utilBin.append(binaryUtilName + mozinfo.bin_suffix);
|
|
// If we're testing locally, the above works. If not, the server executable
|
|
// is in another location.
|
|
if (!utilBin.exists()) {
|
|
utilBin = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
|
|
while (utilBin.path.includes("xpcshell")) {
|
|
utilBin = utilBin.parent;
|
|
}
|
|
utilBin.append("bin");
|
|
utilBin.append(binaryUtilName + mozinfo.bin_suffix);
|
|
}
|
|
// But maybe we're on Android, where binaries are in /data/local/xpcb.
|
|
if (!utilBin.exists()) {
|
|
utilBin.initWithPath("/data/local/xpcb/");
|
|
utilBin.append(binaryUtilName);
|
|
}
|
|
Assert.ok(utilBin.exists(), `Binary util ${binaryUtilName} should exist`);
|
|
return utilBin;
|
|
}
|
|
|
|
// Do not call this directly; use add_tls_server_setup
|
|
function _setupTLSServerTest(serverBinName, certsPath, addDefaultRoot) {
|
|
asyncStartTLSTestServer(serverBinName, certsPath, addDefaultRoot).then(
|
|
run_next_test
|
|
);
|
|
}
|
|
|
|
async function asyncStartTLSTestServer(
|
|
serverBinName,
|
|
certsPath,
|
|
addDefaultRoot
|
|
) {
|
|
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
|
|
Ci.nsIX509CertDB
|
|
);
|
|
// The trusted CA that is typically used for "good" certificates.
|
|
if (addDefaultRoot) {
|
|
addCertFromFile(certdb, `${certsPath}/test-ca.pem`, "CTu,u,u");
|
|
}
|
|
|
|
const CALLBACK_PORT = 8444;
|
|
|
|
let greBinDir = Services.dirsvc.get("GreBinD", Ci.nsIFile);
|
|
Services.env.set("DYLD_LIBRARY_PATH", greBinDir.path);
|
|
// TODO(bug 1107794): Android libraries are in /data/local/xpcb, but "GreBinD"
|
|
// does not return this path on Android, so hard code it here.
|
|
Services.env.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb");
|
|
Services.env.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3");
|
|
Services.env.set("MOZ_TLS_SERVER_CALLBACK_PORT", CALLBACK_PORT);
|
|
|
|
let httpServer = new HttpServer();
|
|
let serverReady = new Promise(resolve => {
|
|
httpServer.registerPathHandler(
|
|
"/",
|
|
function handleServerCallback(aRequest, aResponse) {
|
|
aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
|
|
aResponse.setHeader("Content-Type", "text/plain");
|
|
let responseBody = "OK!";
|
|
aResponse.bodyOutputStream.write(responseBody, responseBody.length);
|
|
executeSoon(function () {
|
|
httpServer.stop(resolve);
|
|
});
|
|
}
|
|
);
|
|
httpServer.start(CALLBACK_PORT);
|
|
});
|
|
|
|
let serverBin = _getBinaryUtil(serverBinName);
|
|
let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
|
|
process.init(serverBin);
|
|
let certDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
|
|
certDir.append(`${certsPath}`);
|
|
Assert.ok(certDir.exists(), `certificate folder (${certsPath}) should exist`);
|
|
// Using "sql:" causes the SQL DB to be used so we can run tests on Android.
|
|
process.run(false, ["sql:" + certDir.path, Services.appinfo.processID], 2);
|
|
|
|
registerCleanupFunction(function () {
|
|
process.kill();
|
|
});
|
|
|
|
await serverReady;
|
|
}
|
|
|
|
// Returns an Array of OCSP responses for a given ocspRespArray and a location
|
|
// for a nssDB where the certs and public keys are prepopulated.
|
|
// ocspRespArray is an array of arrays like:
|
|
// [ [typeOfResponse, certnick, extracertnick, thisUpdateSkew]...]
|
|
function generateOCSPResponses(ocspRespArray, nssDBlocation) {
|
|
let utilBinName = "GenerateOCSPResponse";
|
|
let ocspGenBin = _getBinaryUtil(utilBinName);
|
|
let retArray = [];
|
|
|
|
for (let i = 0; i < ocspRespArray.length; i++) {
|
|
let argArray = [];
|
|
let ocspFilepre = do_get_file(i.toString() + ".ocsp", true);
|
|
let filename = ocspFilepre.path;
|
|
// Using "sql:" causes the SQL DB to be used so we can run tests on Android.
|
|
argArray.push("sql:" + nssDBlocation);
|
|
argArray.push(ocspRespArray[i][0]); // ocsRespType;
|
|
argArray.push(ocspRespArray[i][1]); // nick;
|
|
argArray.push(ocspRespArray[i][2]); // extranickname
|
|
argArray.push(ocspRespArray[i][3]); // thisUpdate skew
|
|
argArray.push(filename);
|
|
info("argArray = " + argArray);
|
|
|
|
let process = Cc["@mozilla.org/process/util;1"].createInstance(
|
|
Ci.nsIProcess
|
|
);
|
|
process.init(ocspGenBin);
|
|
process.run(true, argArray, argArray.length);
|
|
Assert.equal(0, process.exitValue, "Process exit value should be 0");
|
|
let ocspFile = do_get_file(i.toString() + ".ocsp", false);
|
|
retArray.push(readFile(ocspFile));
|
|
ocspFile.remove(false);
|
|
}
|
|
return retArray;
|
|
}
|
|
|
|
// Starts and returns an http responder that will cause a test failure if it is
|
|
// queried. The server identities are given by a non-empty array
|
|
// serverIdentities.
|
|
function getFailingHttpServer(serverPort, serverIdentities) {
|
|
let httpServer = new HttpServer();
|
|
httpServer.registerPrefixHandler("/", function () {
|
|
Assert.ok(false, "HTTP responder should not have been queried");
|
|
});
|
|
httpServer.identity.setPrimary("http", serverIdentities.shift(), serverPort);
|
|
serverIdentities.forEach(function (identity) {
|
|
httpServer.identity.add("http", identity, serverPort);
|
|
});
|
|
httpServer.start(serverPort);
|
|
return httpServer;
|
|
}
|
|
|
|
// Starts an http OCSP responder that serves good OCSP responses and
|
|
// returns an object with a method stop that should be called to stop
|
|
// the http server.
|
|
// NB: Because generating OCSP responses inside the HTTP request
|
|
// handler can cause timeouts, the expected responses are pre-generated
|
|
// all at once before starting the server. This means that their producedAt
|
|
// times will all be the same. If a test depends on this not being the case,
|
|
// perhaps calling startOCSPResponder twice (at different times) will be
|
|
// necessary.
|
|
//
|
|
// serverPort is the port of the http OCSP responder
|
|
// identity is the http hostname that will answer the OCSP requests
|
|
// nssDBLocation is the location of the NSS database from where the OCSP
|
|
// responses will be generated (assumes appropiate keys are present)
|
|
// expectedCertNames is an array of nicks of the certs to be responsed
|
|
// expectedBasePaths is an optional array that is used to indicate
|
|
// what is the expected base path of the OCSP request.
|
|
// expectedMethods is an optional array of methods ("GET" or "POST") indicating
|
|
// by which HTTP method the server is expected to be queried.
|
|
// expectedResponseTypes is an optional array of OCSP response types to use (see
|
|
// GenerateOCSPResponse.cpp).
|
|
// responseHeaderPairs is an optional array of HTTP header (name, value) pairs
|
|
// to set in each response.
|
|
function startOCSPResponder(
|
|
serverPort,
|
|
identity,
|
|
nssDBLocation,
|
|
expectedCertNames,
|
|
expectedBasePaths,
|
|
expectedMethods,
|
|
expectedResponseTypes,
|
|
responseHeaderPairs = []
|
|
) {
|
|
let ocspResponseGenerationArgs = expectedCertNames.map(
|
|
function (expectedNick) {
|
|
let responseType = "good";
|
|
if (expectedResponseTypes && expectedResponseTypes.length >= 1) {
|
|
responseType = expectedResponseTypes.shift();
|
|
}
|
|
return [responseType, expectedNick, "unused", 0];
|
|
}
|
|
);
|
|
let ocspResponses = generateOCSPResponses(
|
|
ocspResponseGenerationArgs,
|
|
nssDBLocation
|
|
);
|
|
let httpServer = new HttpServer();
|
|
httpServer.registerPrefixHandler(
|
|
"/",
|
|
function handleServerCallback(aRequest, aResponse) {
|
|
info("got request for: " + aRequest.path);
|
|
let basePath = aRequest.path.slice(1).split("/")[0];
|
|
if (expectedBasePaths.length >= 1) {
|
|
Assert.equal(
|
|
basePath,
|
|
expectedBasePaths.shift(),
|
|
"Actual and expected base path should match"
|
|
);
|
|
}
|
|
Assert.ok(
|
|
expectedCertNames.length >= 1,
|
|
"expectedCertNames should contain >= 1 entries"
|
|
);
|
|
if (expectedMethods && expectedMethods.length >= 1) {
|
|
Assert.equal(
|
|
aRequest.method,
|
|
expectedMethods.shift(),
|
|
"Actual and expected fetch method should match"
|
|
);
|
|
}
|
|
aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
|
|
aResponse.setHeader("Content-Type", "application/ocsp-response");
|
|
for (let headerPair of responseHeaderPairs) {
|
|
aResponse.setHeader(headerPair[0], headerPair[1]);
|
|
}
|
|
aResponse.write(ocspResponses.shift());
|
|
}
|
|
);
|
|
httpServer.identity.setPrimary("http", identity, serverPort);
|
|
httpServer.start(serverPort);
|
|
return {
|
|
stop(callback) {
|
|
// make sure we consumed each expected response
|
|
Assert.equal(
|
|
ocspResponses.length,
|
|
0,
|
|
"Should have 0 remaining expected OCSP responses"
|
|
);
|
|
if (expectedMethods) {
|
|
Assert.equal(
|
|
expectedMethods.length,
|
|
0,
|
|
"Should have 0 remaining expected fetch methods"
|
|
);
|
|
}
|
|
if (expectedBasePaths) {
|
|
Assert.equal(
|
|
expectedBasePaths.length,
|
|
0,
|
|
"Should have 0 remaining expected base paths"
|
|
);
|
|
}
|
|
if (expectedResponseTypes) {
|
|
Assert.equal(
|
|
expectedResponseTypes.length,
|
|
0,
|
|
"Should have 0 remaining expected response types"
|
|
);
|
|
}
|
|
httpServer.stop(callback);
|
|
},
|
|
};
|
|
}
|
|
|
|
// Given an OCSP responder (see startOCSPResponder), returns a promise that
|
|
// resolves when the responder has successfully stopped.
|
|
function stopOCSPResponder(responder) {
|
|
return new Promise(resolve => {
|
|
responder.stop(resolve);
|
|
});
|
|
}
|
|
|
|
// Utility functions for adding tests relating to certificate error overrides
|
|
|
|
// Helper function for add_cert_override_test. Probably doesn't need to be
|
|
// called directly.
|
|
function add_cert_override(aHost, aSecurityInfo) {
|
|
let cert = aSecurityInfo.serverCert;
|
|
let certOverrideService = Cc[
|
|
"@mozilla.org/security/certoverride;1"
|
|
].getService(Ci.nsICertOverrideService);
|
|
certOverrideService.rememberValidityOverride(aHost, 8443, {}, cert, true);
|
|
}
|
|
|
|
// Given a host and an expected error code, tests that an initial connection to
|
|
// the host fails with the expected error and that adding an override results
|
|
// in a subsequent connection succeeding.
|
|
function add_cert_override_test(aHost, aExpectedError) {
|
|
add_connection_test(
|
|
aHost,
|
|
aExpectedError,
|
|
null,
|
|
add_cert_override.bind(this, aHost)
|
|
);
|
|
add_connection_test(aHost, PRErrorCodeSuccess, null, aSecurityInfo => {
|
|
Assert.ok(
|
|
aSecurityInfo.securityState &
|
|
Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN,
|
|
"Cert override flag should be set on the security state"
|
|
);
|
|
});
|
|
}
|
|
|
|
// Helper function for add_prevented_cert_override_test. This is much like
|
|
// add_cert_override except it may not be the case that the connection has an
|
|
// SecInfo set on it. In this case, the error was not overridable anyway, so
|
|
// we consider it a success.
|
|
function attempt_adding_cert_override(aHost, aSecurityInfo) {
|
|
if (aSecurityInfo.serverCert) {
|
|
let cert = aSecurityInfo.serverCert;
|
|
let certOverrideService = Cc[
|
|
"@mozilla.org/security/certoverride;1"
|
|
].getService(Ci.nsICertOverrideService);
|
|
certOverrideService.rememberValidityOverride(aHost, 8443, {}, cert, true);
|
|
}
|
|
}
|
|
|
|
// Given a host and an expected error code, tests that an initial connection to
|
|
// the host fails with the expected error and that adding an override does not
|
|
// result in a subsequent connection succeeding (i.e. the same error code is
|
|
// encountered).
|
|
// The idea here is that for HSTS hosts or hosts with key pins, no error is
|
|
// overridable, even if an entry is added to the override service.
|
|
function add_prevented_cert_override_test(aHost, aExpectedError) {
|
|
add_connection_test(
|
|
aHost,
|
|
aExpectedError,
|
|
null,
|
|
attempt_adding_cert_override.bind(this, aHost)
|
|
);
|
|
add_connection_test(aHost, aExpectedError);
|
|
}
|
|
|
|
// Helper for asyncTestCertificateUsages.
|
|
class CertVerificationResult {
|
|
constructor(certName, usageString, successExpected, resolve) {
|
|
this.certName = certName;
|
|
this.usageString = usageString;
|
|
this.successExpected = successExpected;
|
|
this.resolve = resolve;
|
|
}
|
|
|
|
verifyCertFinished(aPRErrorCode) {
|
|
if (this.successExpected) {
|
|
equal(
|
|
aPRErrorCode,
|
|
PRErrorCodeSuccess,
|
|
`verifying ${this.certName} for ${this.usageString} should succeed`
|
|
);
|
|
} else {
|
|
notEqual(
|
|
aPRErrorCode,
|
|
PRErrorCodeSuccess,
|
|
`verifying ${this.certName} for ${this.usageString} should fail`
|
|
);
|
|
}
|
|
this.resolve();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asynchronously attempts to verify the given certificate for all supported
|
|
* usages (see allCertificateUsages). Verifies that the results match the
|
|
* expected successful usages. Returns a promise that will resolve when all
|
|
* verifications have been performed.
|
|
* Verification happens "now" with no specified flags or hostname.
|
|
*
|
|
* @param {nsIX509CertDB} certdb
|
|
* The certificate database to use to verify the certificate.
|
|
* @param {nsIX509Cert} cert
|
|
* The certificate to be verified.
|
|
* @param {number[]} expectedUsages
|
|
* A list of usages (as their integer values) that are expected to verify
|
|
* successfully.
|
|
* @returns {Promise}
|
|
* A promise that will resolve with no value when all asynchronous operations
|
|
* have completed.
|
|
*/
|
|
function asyncTestCertificateUsages(certdb, cert, expectedUsages) {
|
|
let now = new Date().getTime() / 1000;
|
|
let promises = [];
|
|
verifyUsages.keys().forEach(usageString => {
|
|
let promise = new Promise(resolve => {
|
|
let usage = verifyUsages.get(usageString);
|
|
let successExpected = expectedUsages.includes(usage);
|
|
let result = new CertVerificationResult(
|
|
cert.commonName,
|
|
usageString,
|
|
successExpected,
|
|
resolve
|
|
);
|
|
let flags = Ci.nsIX509CertDB.FLAG_LOCAL_ONLY;
|
|
certdb.asyncVerifyCertAtTime(cert, usage, flags, null, now, result);
|
|
});
|
|
promises.push(promise);
|
|
});
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* Loads the pkcs11testmodule.cpp test PKCS #11 module, and registers a cleanup
|
|
* function that unloads it once the calling test completes.
|
|
*
|
|
* @param {nsIFile} libraryFile
|
|
* The dynamic library file that implements the module to
|
|
* load.
|
|
* @param {string} moduleName
|
|
* What to call the module.
|
|
* @param {boolean} expectModuleUnloadToFail
|
|
* Should be set to true for tests that manually unload the
|
|
* test module, so the attempt to auto unload the test module
|
|
* doesn't cause a test failure. Should be set to false
|
|
* otherwise, so failure to automatically unload the test
|
|
* module gets reported.
|
|
*/
|
|
function loadPKCS11Module(libraryFile, moduleName, expectModuleUnloadToFail) {
|
|
ok(libraryFile.exists(), "The PKCS11 module file should exist");
|
|
|
|
let pkcs11ModuleDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService(
|
|
Ci.nsIPKCS11ModuleDB
|
|
);
|
|
registerCleanupFunction(() => {
|
|
try {
|
|
pkcs11ModuleDB.deleteModule(moduleName);
|
|
} catch (e) {
|
|
Assert.ok(
|
|
expectModuleUnloadToFail,
|
|
`Module unload should suceed only when expected: ${e}`
|
|
);
|
|
}
|
|
});
|
|
pkcs11ModuleDB.addModule(moduleName, libraryFile.path, 0, 0);
|
|
}
|
|
|
|
/**
|
|
* @param {string} data
|
|
* @returns {string}
|
|
*/
|
|
function hexify(data) {
|
|
// |slice(-2)| chomps off the last two characters of a string.
|
|
// Therefore, if the Unicode value is < 0x10, we have a single-character hex
|
|
// string when we want one that's two characters, and unconditionally
|
|
// prepending a "0" solves the problem.
|
|
return Array.from(data, (c, i) =>
|
|
("0" + data.charCodeAt(i).toString(16)).slice(-2)
|
|
).join("");
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} lines
|
|
* Lines to write. Each line automatically has "\n" appended to it when
|
|
* being written.
|
|
* @param {nsIFileOutputStream} outputStream
|
|
*/
|
|
function writeLinesAndClose(lines, outputStream) {
|
|
for (let line of lines) {
|
|
line += "\n";
|
|
outputStream.write(line, line.length);
|
|
}
|
|
outputStream.close();
|
|
}
|
|
|
|
/**
|
|
* @param {string} moduleName
|
|
* The name of the module that should not be loaded.
|
|
* @param {string} libraryName
|
|
* A unique substring of name of the dynamic library file of the module
|
|
* that should not be loaded.
|
|
*/
|
|
function checkPKCS11ModuleNotPresent(moduleName, libraryName = "undefined") {
|
|
let moduleDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService(
|
|
Ci.nsIPKCS11ModuleDB
|
|
);
|
|
let modules = moduleDB.listModules();
|
|
ok(
|
|
modules.hasMoreElements(),
|
|
"One or more modules should be present with test module not present"
|
|
);
|
|
for (let module of modules) {
|
|
notEqual(
|
|
module.name,
|
|
moduleName,
|
|
`Non-test module name shouldn't equal '${moduleName}'`
|
|
);
|
|
if (libraryName != "undefined") {
|
|
ok(
|
|
!(module.libName && module.libName.includes(libraryName)),
|
|
`Non-test module lib name should not include '${libraryName}'`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks that the test module exists in the module list.
|
|
* Also checks various attributes of the test module for correctness.
|
|
*
|
|
* @param {string} moduleName
|
|
* The name of the module that should be present.
|
|
* @param {string} libraryName
|
|
* A unique substring of the name of the dynamic library file
|
|
* of the module that should be loaded.
|
|
* @returns {nsIPKCS11Module}
|
|
* The test module.
|
|
*/
|
|
function checkPKCS11ModuleExists(moduleName, libraryName = "undefined") {
|
|
let moduleDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService(
|
|
Ci.nsIPKCS11ModuleDB
|
|
);
|
|
let modules = moduleDB.listModules();
|
|
ok(
|
|
modules.hasMoreElements(),
|
|
"One or more modules should be present with test module present"
|
|
);
|
|
let testModule = null;
|
|
for (let module of modules) {
|
|
if (module.name == moduleName) {
|
|
testModule = module;
|
|
break;
|
|
}
|
|
}
|
|
notEqual(testModule, null, "Test module should have been found");
|
|
if (libraryName != "undefined") {
|
|
notEqual(
|
|
testModule.libName,
|
|
null,
|
|
"Test module lib name should not be null"
|
|
);
|
|
ok(
|
|
testModule.libName.includes(ctypes.libraryName(libraryName)),
|
|
`Test module lib name should include lib name of '${libraryName}'`
|
|
);
|
|
}
|
|
|
|
return testModule;
|
|
}
|
|
|
|
// Given an nsIX509Cert, return the bytes of its subject DN (as a JS string) and
|
|
// the sha-256 hash of its subject public key info, base64-encoded.
|
|
function getSubjectAndSPKIHash(nsCert) {
|
|
let certBytes = nsCert.getRawDER();
|
|
let cert = new X509.Certificate();
|
|
cert.parse(certBytes);
|
|
let subject = cert.tbsCertificate.subject._der._bytes;
|
|
let subjectString = arrayToString(subject);
|
|
let spkiHashString = nsCert.sha256SubjectPublicKeyInfoDigest;
|
|
return { subjectString, spkiHashString };
|
|
}
|
|
|
|
function run_certutil_on_directory(directory, args, expectSuccess = true) {
|
|
let greBinDir = Services.dirsvc.get("GreBinD", Ci.nsIFile);
|
|
Services.env.set("DYLD_LIBRARY_PATH", greBinDir.path);
|
|
// TODO(bug 1107794): Android libraries are in /data/local/xpcb, but "GreBinD"
|
|
// does not return this path on Android, so hard code it here.
|
|
Services.env.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb");
|
|
let certutilBin = _getBinaryUtil("certutil");
|
|
let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
|
|
process.init(certutilBin);
|
|
args.push("-d");
|
|
args.push(`sql:${directory}`);
|
|
process.run(true, args, args.length);
|
|
if (expectSuccess) {
|
|
Assert.equal(process.exitValue, 0, "certutil should succeed");
|
|
}
|
|
}
|
|
|
|
function get_data_storage_contents(dataStorageFileName) {
|
|
let stateFile = do_get_profile();
|
|
stateFile.append(dataStorageFileName);
|
|
if (!stateFile.exists()) {
|
|
return undefined;
|
|
}
|
|
return readFile(stateFile);
|
|
}
|
|
|
|
function u16_to_big_endian_bytes(u16) {
|
|
Assert.less(u16, 65536);
|
|
return [u16 / 256, u16 % 256];
|
|
}
|
|
|
|
// Appends a line to the given data storage file (as an nsIOutputStream).
|
|
// score is an integer representing the number of unique days the item has been accessed.
|
|
// lastAccessed is the day since the epoch the item was last accessed.
|
|
// key and value are strings representing the key and value of the item.
|
|
function append_line_to_data_storage_file(
|
|
outputStream,
|
|
score,
|
|
lastAccessed,
|
|
key,
|
|
value,
|
|
valueLength = 24,
|
|
useBadChecksum = false
|
|
) {
|
|
let line = arrayToString(u16_to_big_endian_bytes(score));
|
|
line = line + arrayToString(u16_to_big_endian_bytes(lastAccessed));
|
|
line = line + key;
|
|
let keyPadding = [];
|
|
for (let i = 0; i < 256 - key.length; i++) {
|
|
keyPadding.push(0);
|
|
}
|
|
line = line + arrayToString(keyPadding);
|
|
line = line + value;
|
|
let valuePadding = [];
|
|
for (let i = 0; i < valueLength - value.length; i++) {
|
|
valuePadding.push(0);
|
|
}
|
|
line = line + arrayToString(valuePadding);
|
|
let checksum = 0;
|
|
Assert.equal(line.length % 2, 0);
|
|
for (let i = 0; i < line.length; i += 2) {
|
|
checksum ^= (line.charCodeAt(i) << 8) + line.charCodeAt(i + 1);
|
|
}
|
|
line =
|
|
arrayToString(
|
|
u16_to_big_endian_bytes(useBadChecksum ? ~checksum & 0xffff : checksum)
|
|
) + line;
|
|
outputStream.write(line, line.length);
|
|
}
|
|
|
|
// Helper constants for setting security.pki.certificate_transparency.mode.
|
|
const CT_MODE_COLLECT_TELEMETRY = 1;
|
|
const CT_MODE_ENFORCE = 2;
|
|
|
|
// Helper function for add_ct_test. Returns a function that checks that the
|
|
// nsITransportSecurityInfo of the connection has the expected CT and resumed
|
|
// statuses.
|
|
function expectCT(expectedCTValue, expectedResumed) {
|
|
return securityInfo => {
|
|
Assert.equal(
|
|
securityInfo.certificateTransparencyStatus,
|
|
expectedCTValue,
|
|
"actual and expected CT status should match"
|
|
);
|
|
Assert.equal(
|
|
securityInfo.resumed,
|
|
expectedResumed,
|
|
"connection should be resumed (or not) as expected"
|
|
);
|
|
};
|
|
}
|
|
|
|
// Helper function to add a certificate transparency test. The connection is
|
|
// expected to succeed with the given CT status (see nsITransportSecurityInfo).
|
|
// Additionally, if an additional connection is made, it is expected that TLS
|
|
// resumption is used and that the CT status is the same with the resumed
|
|
// connection.
|
|
function add_ct_test(host, expectedCTValue, expectConnectionSuccess) {
|
|
add_connection_test(
|
|
host,
|
|
expectConnectionSuccess
|
|
? PRErrorCodeSuccess
|
|
: MOZILLA_PKIX_ERROR_INSUFFICIENT_CERTIFICATE_TRANSPARENCY,
|
|
null,
|
|
expectCT(expectedCTValue, false)
|
|
);
|
|
// Test that session resumption results in the same expected CT status for
|
|
// successful connections.
|
|
if (expectConnectionSuccess) {
|
|
add_connection_test(
|
|
host,
|
|
PRErrorCodeSuccess,
|
|
null,
|
|
expectCT(expectedCTValue, true)
|
|
);
|
|
}
|
|
}
|