diff options
Diffstat (limited to 'security/manager/ssl/tests/unit/head_psm.js')
-rw-r--r-- | security/manager/ssl/tests/unit/head_psm.js | 863 |
1 files changed, 863 insertions, 0 deletions
diff --git a/security/manager/ssl/tests/unit/head_psm.js b/security/manager/ssl/tests/unit/head_psm.js new file mode 100644 index 000000000..b6489d605 --- /dev/null +++ b/security/manager/ssl/tests/unit/head_psm.js @@ -0,0 +1,863 @@ +/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +const { AppConstants } = + Cu.import("resource://gre/modules/AppConstants.jsm", {}); +const { ctypes } = Cu.import("resource://gre/modules/ctypes.jsm", {}); +const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {}); +const { HttpServer } = Cu.import("resource://testing-common/httpd.js", {}); +const { MockRegistrar } = + Cu.import("resource://testing-common/MockRegistrar.jsm", {}); +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); +const { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); +const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); + +const isDebugBuild = Cc["@mozilla.org/xpcom/debug;1"] + .getService(Ci.nsIDebug2).isDebugBuild; + +// The test EV roots are only enabled in debug builds as a security measure. +// +// Bug 1008316: B2G doesn't have EV enabled, so EV is not expected even in debug +// builds. +const gEVExpected = isDebugBuild && !("@mozilla.org/b2g-process-global;1" in Cc); + +const SSS_STATE_FILE_NAME = "SiteSecurityServiceState.txt"; +const PRELOAD_STATE_FILE_NAME = "SecurityPreloadState.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_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 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; + +// Supported Certificate Usages +const certificateUsageSSLClient = 0x0001; +const certificateUsageSSLServer = 0x0002; +const certificateUsageSSLCA = 0x0008; +const certificateUsageEmailSigner = 0x0010; +const certificateUsageEmailRecipient = 0x0020; +const certificateUsageObjectSigner = 0x0040; +const certificateUsageVerifyCA = 0x0100; +const certificateUsageStatusResponder = 0x0400; + +// 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 allCertificateUsages = { + certificateUsageSSLClient, + certificateUsageSSLServer, + certificateUsageSSLCA, + certificateUsageEmailSigner, + certificateUsageEmailRecipient, + certificateUsageObjectSigner, + certificateUsageVerifyCA, + certificateUsageStatusResponder +}; + +const NO_FLAGS = 0; + +// 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 readFile(file) { + let fstream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + fstream.init(file, -1, 0, 0); + let data = NetUtil.readInputStreamToString(fstream, fstream.available()); + fstream.close(); + return data; +} + +function addCertFromFile(certdb, filename, trustString) { + let certFile = do_get_file(filename, false); + let certBytes = readFile(certFile); + let successful = false; + try { + certdb.addCert(certBytes, trustString, null); + successful = true; + } catch (e) {} + if (!successful) { + // It might be PEM instead of DER. + certdb.addCertFromBase64(pemToBase64(certBytes), trustString, null); + } +} + +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(certBytes, certBytes.length); + } 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); +} + +// certdb implements nsIX509CertDB. See nsIX509CertDB.idl for documentation. +// In particular, hostname is optional. +function checkCertErrorGenericAtTime(certdb, cert, expectedError, usage, time, + /*optional*/ hasEVPolicy, + /*optional*/ hostname) { + do_print(`cert cn=${cert.commonName}`); + do_print(`cert issuer cn=${cert.issuerCommonName}`); + let verifiedChain = {}; + let error = certdb.verifyCertAtTime(cert, usage, NO_FLAGS, hostname, time, + verifiedChain, hasEVPolicy || {}); + Assert.equal(error, expectedError, + "Actual and expected error should match"); +} + +// certdb implements nsIX509CertDB. See nsIX509CertDB.idl for documentation. +// In particular, hostname is optional. +function checkCertErrorGeneric(certdb, cert, expectedError, usage, + /*optional*/ hasEVPolicy, + /*optional*/ hostname) { + do_print(`cert cn=${cert.commonName}`); + do_print(`cert issuer cn=${cert.issuerCommonName}`); + let verifiedChain = {}; + let error = certdb.verifyCertNow(cert, usage, NO_FLAGS, hostname, + verifiedChain, hasEVPolicy || {}); + Assert.equal(error, expectedError, + "Actual and expected error should match"); +} + +function checkEVStatus(certDB, cert, usage, isEVExpected) { + let hasEVPolicy = {}; + checkCertErrorGeneric(certDB, cert, PRErrorCodeSuccess, usage, hasEVPolicy); + Assert.equal(hasEVPolicy.value, isEVExpected, + "Actual and expected EV status should match"); +} + +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.nsILocalFile); + 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 SSL_ClearSessionCache = null; + try { + SSL_ClearSessionCache = + _getLibraryFunctionWithNoArguments("SSL_ClearSessionCache", "ssl3"); + } catch (e) { + // On Windows, this is actually in the nss3 library. + SSL_ClearSessionCache = + _getLibraryFunctionWithNoArguments("SSL_ClearSessionCache", "nss3"); + } + if (!SSL_ClearSessionCache || SSL_ClearSessionCache() != 0) { + throw new Error("Failed to clear SSL session cache"); + } +} + +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) { + add_test(function() { + _setupTLSServerTest(serverBinName, certsPath); + }); +} + +/** + * 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. + */ +function add_connection_test(aHost, aExpectedResult, + aBeforeConnect, aWithSecurityInfo, + aAfterStreamOpen, + /*optional*/ aOriginAttributes) { + const REMOTE_PORT = 8443; + + function Connection(host) { + this.host = host; + let threadManager = Cc["@mozilla.org/thread-manager;1"] + .getService(Ci.nsIThreadManager); + this.thread = threadManager.currentThread; + this.defer = Promise.defer(); + let sts = Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + this.transport = sts.createTransport(["ssl"], 1, host, REMOTE_PORT, null); + // 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: function(aTransport, aStatus, aProgress, aProgressMax) { + if (!this.connected && aStatus == Ci.nsISocketTransport.STATUS_CONNECTED_TO) { + this.connected = true; + this.outputStream.asyncWait(this, 0, 0, this.thread); + } + }, + + // nsIInputStreamCallback + onInputStreamReady: function(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: function(aStream) { + if (aAfterStreamOpen) { + aAfterStreamOpen(this.transport); + } + let sslSocketControl = this.transport.securityInfo + .QueryInterface(Ci.nsISSLSocketControl); + sslSocketControl.proxyStartSSL(); + 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: function() { + 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(); + } + + add_test(function() { + if (aBeforeConnect) { + aBeforeConnect(); + } + connectTo(aHost).then(function(conn) { + do_print("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(conn.transport.securityInfo + .QueryInterface(Ci.nsITransportSecurityInfo)); + } + run_next_test(); + }); + }); +} + +function _getBinaryUtil(binaryUtilName) { + let directoryService = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + + let utilBin = directoryService.get("CurProcD", Ci.nsILocalFile); + 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 = directoryService.get("CurWorkD", Ci.nsILocalFile); + while (utilBin.path.indexOf("xpcshell") != -1) { + utilBin = utilBin.parent; + } + utilBin.append("bin"); + utilBin.append(binaryUtilName + mozinfo.bin_suffix); + } + // But maybe we're on Android or B2G, 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) +{ + let certdb = Cc["@mozilla.org/security/x509certdb;1"] + .getService(Ci.nsIX509CertDB); + // The trusted CA that is typically used for "good" certificates. + addCertFromFile(certdb, `${certsPath}/test-ca.pem`, "CTu,u,u"); + + const CALLBACK_PORT = 8444; + + let directoryService = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + let envSvc = Cc["@mozilla.org/process/environment;1"] + .getService(Ci.nsIEnvironment); + let greBinDir = directoryService.get("GreBinD", Ci.nsIFile); + envSvc.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. + envSvc.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb"); + envSvc.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3"); + envSvc.set("MOZ_TLS_SERVER_CALLBACK_PORT", CALLBACK_PORT); + + let httpServer = new HttpServer(); + 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); + do_execute_soon(function() { + httpServer.stop(run_next_test); + }); + }); + httpServer.start(CALLBACK_PORT); + + let serverBin = _getBinaryUtil(serverBinName); + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(serverBin); + let certDir = directoryService.get("CurWorkD", Ci.nsILocalFile); + 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 ], 1); + + do_register_cleanup(function() { + process.kill(); + }); +} + +// 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]...] +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(filename); + do_print("argArray = " + argArray); + + let process = Cc["@mozilla.org/process/util;1"] + .createInstance(Ci.nsIProcess); + process.init(ocspGenBin); + process.run(true, argArray, 5); + 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(request, response) { + 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). +function startOCSPResponder(serverPort, identity, nssDBLocation, + expectedCertNames, expectedBasePaths, + expectedMethods, expectedResponseTypes) { + let ocspResponseGenerationArgs = expectedCertNames.map( + function(expectedNick) { + let responseType = "good"; + if (expectedResponseTypes && expectedResponseTypes.length >= 1) { + responseType = expectedResponseTypes.shift(); + } + return [responseType, expectedNick, "unused"]; + } + ); + let ocspResponses = generateOCSPResponses(ocspResponseGenerationArgs, + nssDBLocation); + let httpServer = new HttpServer(); + httpServer.registerPrefixHandler("/", + function handleServerCallback(aRequest, aResponse) { + do_print("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"); + aResponse.write(ocspResponses.shift()); + }); + httpServer.identity.setPrimary("http", identity, serverPort); + httpServer.start(serverPort); + return { + stop: function(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); + } + }; +} + +// A prototype for a fake, error-free sslstatus +var FakeSSLStatus = function(certificate) { + this.serverCert = certificate; +}; + +FakeSSLStatus.prototype = { + serverCert: null, + cipherName: null, + keyLength: 2048, + isDomainMismatch: false, + isNotValidAtThisTime: false, + isUntrusted: false, + isExtendedValidation: false, + getInterface: function(aIID) { + return this.QueryInterface(aIID); + }, + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsISSLStatus) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw new Error(Cr.NS_ERROR_NO_INTERFACE); + }, +}; + +// 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, aExpectedBits, aExpectedErrorRegexp, + aSecurityInfo) { + if (aExpectedErrorRegexp) { + do_print(aSecurityInfo.errorMessage); + Assert.ok(aExpectedErrorRegexp.test(aSecurityInfo.errorMessage), + "Actual error message should match expected error regexp"); + } + let sslstatus = aSecurityInfo.QueryInterface(Ci.nsISSLStatusProvider) + .SSLStatus; + let bits = + (sslstatus.isUntrusted ? Ci.nsICertOverrideService.ERROR_UNTRUSTED : 0) | + (sslstatus.isDomainMismatch ? Ci.nsICertOverrideService.ERROR_MISMATCH : 0) | + (sslstatus.isNotValidAtThisTime ? Ci.nsICertOverrideService.ERROR_TIME : 0); + Assert.equal(bits, aExpectedBits, + "Actual and expected override bits should match"); + let cert = sslstatus.serverCert; + let certOverrideService = Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService); + certOverrideService.rememberValidityOverride(aHost, 8443, cert, aExpectedBits, + true); +} + +// Given a host, expected error bits (see nsICertOverrideService.idl), an +// expected error code, and optionally a regular expression that the resulting +// error message must match, tests that an initial connection to the host fails +// with the expected errors and that adding an override results in a subsequent +// connection succeeding. +function add_cert_override_test(aHost, aExpectedBits, aExpectedError, + aExpectedErrorRegexp = undefined) { + add_connection_test(aHost, aExpectedError, null, + add_cert_override.bind(this, aHost, aExpectedBits, + aExpectedErrorRegexp)); + 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 +// SSLStatus set on it. In this case, the error was not overridable anyway, so +// we consider it a success. +function attempt_adding_cert_override(aHost, aExpectedBits, aSecurityInfo) { + let sslstatus = aSecurityInfo.QueryInterface(Ci.nsISSLStatusProvider) + .SSLStatus; + if (sslstatus) { + let bits = + (sslstatus.isUntrusted ? Ci.nsICertOverrideService.ERROR_UNTRUSTED : 0) | + (sslstatus.isDomainMismatch ? Ci.nsICertOverrideService.ERROR_MISMATCH : 0) | + (sslstatus.isNotValidAtThisTime ? Ci.nsICertOverrideService.ERROR_TIME : 0); + Assert.equal(bits, aExpectedBits, + "Actual and expected override bits should match"); + let cert = sslstatus.serverCert; + let certOverrideService = Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService); + certOverrideService.rememberValidityOverride(aHost, 8443, cert, aExpectedBits, + true); + } +} + +// Given a host, expected error bits (see nsICertOverrideService.idl), and +// an expected error code, tests that an initial connection to the host fails +// with the expected errors 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, aExpectedBits, aExpectedError) { + add_connection_test(aHost, aExpectedError, null, + attempt_adding_cert_override.bind(this, aHost, aExpectedBits)); + add_connection_test(aHost, aExpectedError); +} + +function loginToDBWithDefaultPassword() { + let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"] + .getService(Ci.nsIPK11TokenDB); + let token = tokenDB.getInternalKeyToken(); + token.initPassword(""); + token.login(/*force*/ false); +} + +// Helper for asyncTestCertificateUsages. +class CertVerificationResult { + constructor(certName, usageString, successExpected, resolve) { + this.certName = certName; + this.usageString = usageString; + this.successExpected = successExpected; + this.resolve = resolve; + } + + verifyCertFinished(aPRErrorCode, aVerifiedChain, aHasEVPolicy) { + 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. + * @return {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 = []; + Object.keys(allCertificateUsages).forEach(usageString => { + let promise = new Promise((resolve, reject) => { + let usage = allCertificateUsages[usageString]; + let successExpected = expectedUsages.includes(usage); + let result = new CertVerificationResult(cert.commonName, usageString, + successExpected, resolve); + certdb.asyncVerifyCertAtTime(cert, usage, 0, 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 {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 loadPKCS11TestModule(expectModuleUnloadToFail) { + let libraryFile = Services.dirsvc.get("CurWorkD", Ci.nsILocalFile); + libraryFile.append("pkcs11testmodule"); + libraryFile.append(ctypes.libraryName("pkcs11testmodule")); + ok(libraryFile.exists(), "The pkcs11testmodule file should exist"); + + let pkcs11 = Cc["@mozilla.org/security/pkcs11;1"].getService(Ci.nsIPKCS11); + do_register_cleanup(() => { + try { + pkcs11.deleteModule("PKCS11 Test Module"); + } catch (e) { + Assert.ok(expectModuleUnloadToFail, + `Module unload should suceed only when expected: ${e}`); + } + }); + pkcs11.addModule("PKCS11 Test Module", libraryFile.path, 0, 0); +} |