/* 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"; // function run_test() { do_get_profile(); add_tls_server_setup("", ""); add_connection_test(".example.com", SEC_ERROR_xxx, function() { ... }, function(aTransportSecurityInfo) { ... }, function(aTransport) { ... }); [...] add_connection_test(".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); }