diff options
Diffstat (limited to 'data/extensions/https-everywhere@eff.org/components')
-rw-r--r-- | data/extensions/https-everywhere@eff.org/components/https-everywhere.js | 832 | ||||
-rw-r--r-- | data/extensions/https-everywhere@eff.org/components/ssl-observatory.js | 1026 |
2 files changed, 1858 insertions, 0 deletions
diff --git a/data/extensions/https-everywhere@eff.org/components/https-everywhere.js b/data/extensions/https-everywhere@eff.org/components/https-everywhere.js new file mode 100644 index 0000000..90f55e2 --- /dev/null +++ b/data/extensions/https-everywhere@eff.org/components/https-everywhere.js @@ -0,0 +1,832 @@ +// LOG LEVELS --- + +VERB=1; +DBUG=2; +INFO=3; +NOTE=4; +WARN=5; + +// PREFERENCE BRANCHES +PREFBRANCH_ROOT=0; +PREFBRANCH_RULE_TOGGLE=1; + +//--------------- + +https_domains = {}; // maps domain patterns (with at most one + // wildcard) to RuleSets + +https_everywhere_blacklist = {}; // URLs we've given up on rewriting because + // of redirection loops + +https_blacklist_domains = {}; // domains for which there is at least one + // blacklisted URL + +// +const CI = Components.interfaces; +const CC = Components.classes; +const CU = Components.utils; +const CR = Components.results; +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); + +const CP_SHOULDPROCESS = 4; + +const SERVICE_CTRID = "@eff.org/https-everywhere;1"; +const SERVICE_ID=Components.ID("{32c165b4-fe5e-4964-9250-603c410631b4}"); +const SERVICE_NAME = "Encrypts your communications with a number of major websites"; + +const LLVAR = "LogLevel"; + +const IOS = CC["@mozilla.org/network/io-service;1"].getService(CI.nsIIOService); +const OS = CC['@mozilla.org/observer-service;1'].getService(CI.nsIObserverService); +const LOADER = CC["@mozilla.org/moz/jssubscript-loader;1"].getService(CI.mozIJSSubScriptLoader); +const _INCLUDED = {}; + +// NoScript uses this blob to include js constructs that stored in the chrome/ +// directory, but are not attached to the Firefox UI (normally, js located +// there is attached to an Overlay and therefore is part of the UI). + +// Reasons for this: things in components/ directory cannot be split into +// separate files; things in chrome/ can be + +const INCLUDE = function(name) { + if (arguments.length > 1) + for (var j = 0, len = arguments.length; j < len; j++) + INCLUDE(arguments[j]); + else if (!_INCLUDED[name]) { + // we used to try/catch here, but that was less useful because it didn't + // produce line numbers for syntax errors + LOADER.loadSubScript("chrome://https-everywhere/content/code/" + + name + ".js"); + _INCLUDED[name] = true; + } +}; + +const WP_STATE_START = CI.nsIWebProgressListener.STATE_START; +const WP_STATE_STOP = CI.nsIWebProgressListener.STATE_STOP; +const WP_STATE_DOC = CI.nsIWebProgressListener.STATE_IS_DOCUMENT; +const WP_STATE_START_DOC = WP_STATE_START | WP_STATE_DOC; +const WP_STATE_RESTORING = CI.nsIWebProgressListener.STATE_RESTORING; + +const LF_VALIDATE_ALWAYS = CI.nsIRequest.VALIDATE_ALWAYS; +const LF_LOAD_BYPASS_ALL_CACHES = CI.nsIRequest.LOAD_BYPASS_CACHE | CI.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE; + +const NS_OK = 0; +const NS_BINDING_ABORTED = 0x804b0002; +const NS_BINDING_REDIRECTED = 0x804b0003; +const NS_ERROR_UNKNOWN_HOST = 0x804b001e; +const NS_ERROR_REDIRECT_LOOP = 0x804b001f; +const NS_ERROR_CONNECTION_REFUSED = 0x804b000e; +const NS_ERROR_NOT_AVAILABLE = 0x804b0111; + +const LOG_CONTENT_BLOCK = 1; +const LOG_CONTENT_CALL = 2; +const LOG_CONTENT_INTERCEPT = 4; +const LOG_CHROME_WIN = 8; +const LOG_XSS_FILTER = 16; +const LOG_INJECTION_CHECK = 32; +const LOG_DOM = 64; +const LOG_JS = 128; +const LOG_LEAKS = 1024; +const LOG_SNIFF = 2048; +const LOG_CLEARCLICK = 4096; +const LOG_ABE = 8192; + +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +const WHERE_UNTRUSTED = 1; +const WHERE_TRUSTED = 2; +const ANYWHERE = 3; + +const N_COHORTS = 1000; + +const DUMMY_OBJ = {}; +DUMMY_OBJ.wrappedJSObject = DUMMY_OBJ; +const DUMMY_FUNC = function() {}; +const DUMMY_ARRAY = []; + +const EARLY_VERSION_CHECK = !("nsISessionStore" in CI && typeof(/ /) === "object"); + +// This is probably obsolete since the switch to the channel.redirectTo API +const OBSERVER_TOPIC_URI_REWRITE = "https-everywhere-uri-rewrite"; + +// XXX: Better plan for this? +// We need it to exist to make our updates of ChannelReplacement.js easier. +var ABE = { + consoleDump: false, + log: function(str) { + https_everywhereLog(WARN, str); + } +}; + +function xpcom_generateQI(iids) { + var checks = []; + for each (var iid in iids) { + checks.push("CI." + iid.name + ".equals(iid)"); + } + var src = checks.length + ? "if (" + checks.join(" || ") + ") return this;\n" + : ""; + return new Function("iid", src + "throw Components.results.NS_ERROR_NO_INTERFACE;"); +} + +function xpcom_checkInterfaces(iid,iids,ex) { + for (var j = iids.length; j-- >0;) { + if (iid.equals(iids[j])) return true; + } + throw ex; +} + +INCLUDE('ChannelReplacement', 'IOUtil', 'HTTPSRules', 'HTTPS', 'Thread', 'ApplicableList'); + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +// This is black magic for storing Expando data w/ an nsIDOMWindow +// See http://pastebin.com/qY28Jwbv , +// https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIControllers + +StorageController.prototype = { + QueryInterface: XPCOMUtils.generateQI( + [ Components.interfaces.nsISupports, + Components.interfaces.nsIController ]), + wrappedJSObject: null, // Initialized by constructor + supportsCommand: function (cmd) {return (cmd == this.command);}, + isCommandEnabled: function (cmd) {return (cmd == this.command);}, + onEvent: function(eventName) {return true;}, + doCommand: function() {return true;} +}; + +function StorageController(command) { + this.command = command; + this.data = {}; + this.wrappedJSObject = this; +} + +/*var Controller = Class("Controller", XPCOM(CI.nsIController), { + init: function (command, data) { + this.command = command; + this.data = data; + }, + supportsCommand: function (cmd) cmd === this.command +});*/ + +function HTTPSEverywhere() { + + // Set up logging in each component: + HTTPS.log = HTTPSRules.log = RuleWriter.log = this.log = https_everywhereLog; + + this.log = https_everywhereLog; + this.wrappedJSObject = this; + this.https_rules = HTTPSRules; + this.INCLUDE=INCLUDE; + this.ApplicableList = ApplicableList; + this.browser_initialised = false; // the browser is completely loaded + + this.prefs = this.get_prefs(); + this.rule_toggle_prefs = this.get_prefs(PREFBRANCH_RULE_TOGGLE); + + // We need to use observers instead of categories for FF3.0 for these: + // https://developer.mozilla.org/en/Observer_Notifications + // https://developer.mozilla.org/en/nsIObserverService. + // https://developer.mozilla.org/en/nsIObserver + // We also use the observer service to let other extensions know about URIs + // we rewrite. + this.obsService = CC["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + + if(this.prefs.getBoolPref("globalEnabled")){ + this.obsService.addObserver(this, "profile-before-change", false); + this.obsService.addObserver(this, "profile-after-change", false); + this.obsService.addObserver(this, "sessionstore-windows-restored", false); + this.obsService.addObserver(this, "browser:purge-session-history", false); + } + + var pref_service = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranchInternal); + var branch = pref_service.QueryInterface(Components.interfaces.nsIPrefBranchInternal); + + branch.addObserver("extensions.https_everywhere.enable_mixed_rulesets", + this, false); + branch.addObserver("security.mixed_content.block_active_content", + this, false); + + return; +} + + +// nsIContentPolicy interface +// we use numeric constants for performance sake: +const TYPE_OTHER = 1; +const TYPE_SCRIPT = 2; +const TYPE_IMAGE = 3; +const TYPE_STYLESHEET = 4; +const TYPE_OBJECT = 5; +const TYPE_DOCUMENT = 6; +const TYPE_SUBDOCUMENT = 7; +const TYPE_REFRESH = 8; +const TYPE_XBL = 9; +const TYPE_PING = 10; +const TYPE_XMLHTTPREQUEST = 11; +const TYPE_OBJECT_SUBREQUEST = 12; +const TYPE_DTD = 13; +const TYPE_FONT = 14; +const TYPE_MEDIA = 15; +// -------------- +// REJECT_SERVER = -3 +// ACCEPT = 1 + + +// Some of these types are known by arbitrary assertion at +// https://bugzilla.mozilla.org/show_bug.cgi?id=677643#c47 +// TYPE_FONT was required to fix https://trac.torproject.org/projects/tor/ticket/4194 +// TYPE_SUBDOCUMENT was required to fix https://trac.torproject.org/projects/tor/ticket/4149 +// I have NO IDEA why JS won't let me use the constants above in defining this +const shouldLoadTargets = { + 1 : true, + 3 : true, + 5 : true, + 12 : true, + 14 : true, + 7 : true +}; + + + +/* +In recent versions of Firefox and HTTPS Everywhere, the call stack for performing an HTTP -> HTTPS rewrite looks like this: + +1. HTTPSEverywhere.observe() gets a callback with the "http-on-modify-request" topic, and the channel as a subject + + 2. HTTPS.replaceChannel() + + 3. HTTPSRules.rewrittenURI() + + 4. HTTPSRules.potentiallyApplicableRulesets uses <target host=""> elements to identify relevant rulesets + + foreach RuleSet: + + 4. RuleSet.transformURI() + + 5. RuleSet.apply() does the tests and rewrites with RegExps, returning a string + + 4. RuleSet.transformURI() makes a new uri object for the destination string, if required + + 2. HTTPS.replaceChannel() calls channel.redirectTo() if a redirect is needed + + +In addition, the following other important tasks happen along the way: + +HTTPSEverywhere.observe() aborts if there is a redirect loop + finds a reference to the ApplicableList or alist that represents the toolbar context menu + +HTTPS.replaceChannel() notices redirect loops (and used to do much more complex XPCOM API work in the NoScript-based past) + +HTTPSRules.rewrittenURI() works around weird URI types like about: and http://user:pass@example.com/ + and notifies the alist of what it should display for each ruleset + +*/ + +// This defines for Mozilla what stuff HTTPSEverywhere will implement. + +// ChannelEventSink used to be necessary in order to handle redirects (eg +// HTTP redirects) correctly. It may now be obsolete? XXX + +HTTPSEverywhere.prototype = { + prefs: null, + // properties required for XPCOM registration: + classDescription: SERVICE_NAME, + classID: SERVICE_ID, + contractID: SERVICE_CTRID, + + _xpcom_factory: { + createInstance: function (outer, iid) { + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + if (!HTTPSEverywhere.instance) + HTTPSEverywhere.instance = new HTTPSEverywhere(); + return HTTPSEverywhere.instance.QueryInterface(iid); + }, + + QueryInterface: XPCOMUtils.generateQI( + [ Components.interfaces.nsISupports, + Components.interfaces.nsIModule, + Components.interfaces.nsIFactory ]) + }, + + // [optional] an array of categories to register this component in. + _xpcom_categories: [ + { + category: "app-startup", + }, + ], + + // QueryInterface implementation, e.g. using the generateQI helper + QueryInterface: XPCOMUtils.generateQI( + [ Components.interfaces.nsIObserver, + Components.interfaces.nsISupports, + Components.interfaces.nsISupportsWeakReference, + Components.interfaces.nsIWebProgressListener, + Components.interfaces.nsIWebProgressListener2, + Components.interfaces.nsIChannelEventSink ]), + + wrappedJSObject: null, // Initialized by constructor + + getWeakReference: function () { + return Components.utils.getWeakReference(this); + }, + + // An "expando" is an attribute glued onto something. From NoScript. + getExpando: function(domWin, key) { + var c = domWin.controllers.getControllerForCommand("https-everywhere-storage"); + try { + if (c) { + c = c.wrappedJSObject; + //this.log(DBUG, "Found a controller, returning data"); + return c.data[key]; + } else { + this.log(INFO, "No controller attached to " + domWin); + return null; + } + } catch(e) { + // Firefox 3.5 + this.log(WARN,"exception in getExpando"); + this.getExpando = this.getExpando_old; + this.setExpando = this.setExpando_old; + return this.getExpando_old(domWin, key, null); + } + }, + setExpando: function(domWin, key, value) { + var c = domWin.controllers.getControllerForCommand("https-everywhere-storage"); + try { + if (!c) { + this.log(DBUG, "Appending new StorageController for " + domWin); + c = new StorageController("https-everywhere-storage"); + domWin.controllers.appendController(c); + } else { + c = c.wrappedJSObject; + } + c.data[key] = value; + } catch(e) { + this.log(WARN,"exception in setExpando"); + this.getExpando = this.getExpando_old; + this.setExpando = this.setExpando_old; + this.setExpando_old(domWin, key, value); + } + }, + + // This method is straight out of NoScript... we fall back to it in FF 3.*? + getExpando_old: function(domWin, key, defValue) { + var domObject = domWin.document; + return domObject && domObject.__httpsEStorage && domObject.__httpsEStorage[key] || + (defValue ? this.setExpando(domObject, key, defValue) : null); + }, + setExpando_old: function(domWin, key, value) { + var domObject = domWin.document; + if (!domObject) return null; + if (!domObject.__httpsEStorage) domObject.__httpsEStorage = {}; + if (domObject.__httpsEStorage) domObject.__httpsEStorage[key] = value; + else this.log(WARN, "Warning: cannot set expando " + key + " to value " + value); + return value; + }, + + // We use onLocationChange to make a fresh list of rulesets that could have + // applied to the content in the current page (the "applicable list" is used + // for the context menu in the UI). This will be appended to as various + // content is embedded / requested by JavaScript. + onLocationChange: function(wp, req, uri) { + if (wp instanceof CI.nsIWebProgress) { + if (!this.newApplicableListForDOMWin(wp.DOMWindow)) + this.log(WARN,"Something went wrong in onLocationChange"); + } else { + this.log(WARN,"onLocationChange: no nsIWebProgress"); + } + }, + + getWindowForChannel: function(channel) { + // Obtain an nsIDOMWindow from a channel + let loadContext; + try { + loadContext = channel.notificationCallbacks.getInterface(CI.nsILoadContext); + } catch(e) { + try { + loadContext = channel.loadGroup.notificationCallbacks.getInterface(CI.nsILoadContext); + } catch(e) { + this.log(NOTE, "No loadContext for " + channel.URI.spec); + return null; + } + } + + let domWin = loadContext.associatedWindow; + if (!domWin) { + this.log(NOTE, "failed to get DOMWin for " + channel.URI.spec); + return null; + } + + domWin = domWin.top; + return domWin; + }, + + // the lists get made when the urlbar is loading something new, but they + // need to be appended to with reference only to the channel + getApplicableListForChannel: function(channel) { + var domWin = this.getWindowForChannel(channel); + return this.getApplicableListForDOMWin(domWin, "on-modify-request w " + domWin); + }, + + newApplicableListForDOMWin: function(domWin) { + if (!domWin || !(domWin instanceof CI.nsIDOMWindow)) { + this.log(WARN, "Get alist without domWin"); + return null; + } + var dw = domWin.top; + var alist = new ApplicableList(this.log,dw.document,dw); + this.setExpando(dw,"applicable_rules",alist); + return alist; + }, + + getApplicableListForDOMWin: function(domWin, where) { + if (!domWin || !(domWin instanceof CI.nsIDOMWindow)) { + //this.log(WARN, "Get alist without domWin"); + return null; + } + var dw = domWin.top; + var alist= this.getExpando(dw,"applicable_rules",null); + if (alist) { + //this.log(DBUG,"get AL success in " + where); + return alist; + } else { + //this.log(DBUG, "Making new AL in getApplicableListForDOMWin in " + where); + alist = new ApplicableList(this.log,dw.document,dw); + this.setExpando(dw,"applicable_rules",alist); + } + return alist; + }, + + observe: function(subject, topic, data) { + // Top level glue for the nsIObserver API + var channel = subject; + //this.log(VERB,"Got observer topic: "+topic); + + if (topic == "http-on-modify-request") { + if (!(channel instanceof CI.nsIHttpChannel)) return; + + this.log(DBUG,"Got http-on-modify-request: "+channel.URI.spec); + var lst = this.getApplicableListForChannel(channel); // null if no window is associated (ex: xhr) + if (channel.URI.spec in https_everywhere_blacklist) { + this.log(DBUG, "Avoiding blacklisted " + channel.URI.spec); + if (lst) lst.breaking_rule(https_everywhere_blacklist[channel.URI.spec]); + else this.log(NOTE,"Failed to indicate breakage in content menu"); + return; + } + HTTPS.replaceChannel(lst, channel); + } else if (topic == "http-on-examine-response") { + this.log(DBUG, "Got http-on-examine-response @ "+ (channel.URI ? channel.URI.spec : '') ); + HTTPS.handleSecureCookies(channel); + } else if (topic == "http-on-examine-merged-response") { + this.log(DBUG, "Got http-on-examine-merged-response "); + HTTPS.handleSecureCookies(channel); + } else if (topic == "cookie-changed") { + // Javascript can add cookies via document.cookie that are insecure. + if (data == "added" || data == "changed") { + // subject can also be an nsIArray! bleh. + try { + subject.QueryInterface(CI.nsIArray); + var elems = subject.enumerate(); + while (elems.hasMoreElements()) { + var cookie = elems.getNext() + .QueryInterface(CI.nsICookie2); + if (!cookie.isSecure) { + HTTPS.handleInsecureCookie(cookie); + } + } + } catch(e) { + subject.QueryInterface(CI.nsICookie2); + if(!subject.isSecure) { + HTTPS.handleInsecureCookie(subject); + } + } + } + } else if (topic == "profile-before-change") { + this.log(INFO, "Got profile-before-change"); + var catman = Components.classes["@mozilla.org/categorymanager;1"] + .getService(Components.interfaces.nsICategoryManager); + catman.deleteCategoryEntry("net-channel-event-sinks", SERVICE_CTRID, true); + Thread.hostRunning = false; + } else if (topic == "profile-after-change") { + this.log(DBUG, "Got profile-after-change"); + + if(this.prefs.getBoolPref("globalEnabled")){ + OS.addObserver(this, "cookie-changed", false); + OS.addObserver(this, "http-on-modify-request", false); + OS.addObserver(this, "http-on-examine-merged-response", false); + OS.addObserver(this, "http-on-examine-response", false); + + var dls = CC['@mozilla.org/docloaderservice;1'] + .getService(CI.nsIWebProgress); + dls.addProgressListener(this, CI.nsIWebProgress.NOTIFY_LOCATION); + this.log(INFO,"ChannelReplacement.supported = "+ChannelReplacement.supported); + + HTTPSRules.init(); + + Thread.hostRunning = true; + var catman = Components.classes["@mozilla.org/categorymanager;1"] + .getService(Components.interfaces.nsICategoryManager); + // hook on redirections (non persistent, otherwise crashes on 1.8.x) + catman.addCategoryEntry("net-channel-event-sinks", SERVICE_CTRID, + SERVICE_CTRID, false, true); + } + } else if (topic == "sessionstore-windows-restored") { + this.log(DBUG,"Got sessionstore-windows-restored"); + this.maybeShowObservatoryPopup(); + this.browser_initialised = true; + } else if (topic == "nsPref:changed") { + // If the user toggles the Mixed Content Blocker settings, reload the rulesets + // to enable/disable the mixedcontent ones + + // this pref gets set to false and then true during FF 26 startup! + // so do nothing if we're being notified during startup + if (!this.browser_initialised) + return; + switch (data) { + case "security.mixed_content.block_active_content": + case "extensions.https_everywhere.enable_mixed_rulesets": + var p = CC["@mozilla.org/preferences-service;1"].getService(CI.nsIPrefBranch); + var val = p.getBoolPref("security.mixed_content.block_active_content"); + this.log(INFO,"nsPref:changed for "+data + " to " + val); + HTTPSRules.init(); + break; + } + } else if (topic == "browser:purge-session-history") { + // The list of rulesets that have been loaded from the sqlite DB + // constitutes a parallel history store, so we have to clear it. + this.log(DBUG, "History cleared, reloading HTTPSRules to avoid information leak."); + HTTPSRules.init(); + } + return; + }, + + maybeShowObservatoryPopup: function() { + // Show the popup at most once. Users who enabled the Observatory before + // a version that would have shown it to them, don't need to see it + // again. + var ssl_observatory = CC["@eff.org/ssl-observatory;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + var shown = ssl_observatory.myGetBoolPref("popup_shown"); + var enabled = ssl_observatory.myGetBoolPref("enabled"); + var that = this; + var obs_popup_callback = function(result) { + if (result) that.log(INFO, "Got positive proxy test."); + else that.log(INFO, "Got negative proxy text."); + // We are now ready to show the popup in its most informative state + that.chrome_opener("chrome://https-everywhere/content/observatory-popup.xul"); + }; + if (!shown && !enabled) + ssl_observatory.registerProxyTestNotification(obs_popup_callback); + + if (shown && enabled) + this.maybeCleanupObservatoryPrefs(ssl_observatory); + }, + + maybeCleanupObservatoryPrefs: function(ssl_observatory) { + // Recover from a past UI processing bug that would leave the Obsevatory + // accidentally disabled for some users + // https://trac.torproject.org/projects/tor/ticket/10728 + var clean = ssl_observatory.myGetBoolPref("clean_config"); + if (clean) return; + + // unchanged: returns true if a pref has not been modified + var unchanged = function(p){return !ssl_observatory.prefs.prefHasUserValue("extensions.https_everywhere._observatory."+p)}; + var cleanup_obsprefs_callback = function(tor_avail) { + // we only run this once + ssl_observatory.prefs.setBoolPref("extensions.https_everywhere._observatory.clean_config", true); + if (!tor_avail) { + // use_custom_proxy is the variable that is often false when it should be true; + if (!ssl_observatory.myGetBoolPref("use_custom_proxy")) { + // however don't do anything if any of the prefs have been set by the user + if (unchanged("alt_roots") && unchanged("self_signed") && unchanged ("send_asn") && unchanged("priv_dns")) { + ssl_observatory.prefs.setBoolPref("extensions.https_everywhere._observatory.use_custom_proxy", true); + } + } + } + } + ssl_observatory.registerProxyTestNotification(cleanup_obsprefs_callback); + }, + + + getExperimentalFeatureCohort: function() { + // This variable is used for gradually turning on features for testing and + // scalability purposes. It is a random integer [0,N_COHORTS) generated + // once and stored thereafter. + // + // This is not currently used/called in the development branch + var cohort; + try { + cohort = this.prefs.getIntPref("experimental_feature_cohort"); + } catch(e) { + cohort = Math.round(Math.random() * N_COHORTS); + this.prefs.setIntPref("experimental_feature_cohort", cohort); + } + return cohort; + }, + + // nsIChannelEventSink implementation + // XXX This was here for rewrites in the past. Do we still need it? + onChannelRedirect: function(oldChannel, newChannel, flags) { + const uri = newChannel.URI; + this.log(DBUG,"Got onChannelRedirect to "+uri.spec); + if (!(newChannel instanceof CI.nsIHttpChannel)) { + this.log(DBUG, newChannel + " is not an instance of nsIHttpChannel"); + return; + } + var alist = this.juggleApplicableListsDuringRedirection(oldChannel, newChannel); + HTTPS.replaceChannel(alist,newChannel); + }, + + juggleApplicableListsDuringRedirection: function(oldChannel, newChannel) { + // If the new channel doesn't yet have a list of applicable rulesets, start + // with the old one because that's probably a better representation of how + // secure the load process was for this page + var domWin = this.getWindowForChannel(oldChannel); + var old_alist = null; + if (domWin) + old_alist = this.getExpando(domWin,"applicable_rules", null); + domWin = this.getWindowForChannel(newChannel); + if (!domWin) return null; + var new_alist = this.getExpando(domWin,"applicable_rules", null); + if (old_alist && !new_alist) { + new_alist = old_alist; + this.setExpando(domWin,"applicable_rules",new_alist); + } else if (!new_alist) { + new_alist = new ApplicableList(this.log, domWin.document, domWin); + this.setExpando(domWin,"applicable_rules",new_alist); + } + return new_alist; + }, + + asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) { + this.onChannelRedirect(oldChannel, newChannel, flags); + callback.onRedirectVerifyCallback(0); + }, + + get_prefs: function(prefBranch) { + if(!prefBranch) prefBranch = PREFBRANCH_ROOT; + + // get our preferences branch object + // FIXME: Ugly hack stolen from https + var branch_name; + if(prefBranch == PREFBRANCH_RULE_TOGGLE) + branch_name = "extensions.https_everywhere.rule_toggle."; + else + branch_name = "extensions.https_everywhere."; + var o_prefs = false; + var o_branch = false; + // this function needs to be called from inside https_everywhereLog, so + // it needs to do its own logging... + var econsole = Components.classes["@mozilla.org/consoleservice;1"] + .getService(Components.interfaces.nsIConsoleService); + + o_prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + + if (!o_prefs) + { + econsole.logStringMessage("HTTPS Everywhere: Failed to get preferences-service!"); + return false; + } + + o_branch = o_prefs.getBranch(branch_name); + if (!o_branch) + { + econsole.logStringMessage("HTTPS Everywhere: Failed to get prefs branch!"); + return false; + } + + if(prefBranch == PREFBRANCH_ROOT) { + // make sure there's an entry for our log level + try { + o_branch.getIntPref(LLVAR); + } catch (e) { + econsole.logStringMessage("Creating new about:config https_everywhere.LogLevel variable"); + o_branch.setIntPref(LLVAR, WARN); + } + } + + return o_branch; + }, + + chrome_opener: function(uri, args) { + // we don't use window.open, because we need to work around TorButton's + // state control + args = args || 'chrome,centerscreen'; + return CC['@mozilla.org/appshell/window-mediator;1'] + .getService(CI.nsIWindowMediator) + .getMostRecentWindow('navigator:browser') + .open(uri,'', args ); + }, + + tab_opener: function(uri) { + var gb = CC['@mozilla.org/appshell/window-mediator;1'] + .getService(CI.nsIWindowMediator) + .getMostRecentWindow('navigator:browser') + .gBrowser; + var tab = gb.addTab(uri); + gb.selectedTab = tab; + return tab; + }, + + toggleEnabledState: function() { + if(this.prefs.getBoolPref("globalEnabled")){ + try{ + this.obsService.removeObserver(this, "profile-before-change"); + this.obsService.removeObserver(this, "profile-after-change"); + this.obsService.removeObserver(this, "sessionstore-windows-restored"); + OS.removeObserver(this, "cookie-changed"); + OS.removeObserver(this, "http-on-modify-request"); + OS.removeObserver(this, "http-on-examine-merged-response"); + OS.removeObserver(this, "http-on-examine-response"); + + var catman = Components.classes["@mozilla.org/categorymanager;1"] + .getService(Components.interfaces.nsICategoryManager); + catman.deleteCategoryEntry("net-channel-event-sinks", SERVICE_CTRID, true); + + var dls = CC['@mozilla.org/docloaderservice;1'] + .getService(CI.nsIWebProgress); + dls.removeProgressListener(this); + + this.prefs.setBoolPref("globalEnabled", false); + } + catch(e){ + this.log(WARN, "Couldn't remove observers: " + e); + } + } + else{ + try{ + this.obsService.addObserver(this, "profile-before-change", false); + this.obsService.addObserver(this, "profile-after-change", false); + this.obsService.addObserver(this, "sessionstore-windows-restored", false); + OS.addObserver(this, "cookie-changed", false); + OS.addObserver(this, "http-on-modify-request", false); + OS.addObserver(this, "http-on-examine-merged-response", false); + OS.addObserver(this, "http-on-examine-response", false); + + var dls = CC['@mozilla.org/docloaderservice;1'] + .getService(CI.nsIWebProgress); + dls.addProgressListener(this, CI.nsIWebProgress.NOTIFY_LOCATION); + + this.log(INFO,"ChannelReplacement.supported = "+ChannelReplacement.supported); + + if(!Thread.hostRunning) + Thread.hostRunning = true; + + var catman = Components.classes["@mozilla.org/categorymanager;1"] + .getService(Components.interfaces.nsICategoryManager); + // hook on redirections (non persistent, otherwise crashes on 1.8.x) + catman.addCategoryEntry("net-channel-event-sinks", SERVICE_CTRID, + SERVICE_CTRID, false, true); + + HTTPSRules.init(); + this.prefs.setBoolPref("globalEnabled", true); + } + catch(e){ + this.log(WARN, "Couldn't add observers: " + e); + } + } + } +}; + +var prefs = 0; +var econsole = 0; +function https_everywhereLog(level, str) { + if (prefs == 0) { + prefs = HTTPSEverywhere.instance.get_prefs(); + econsole = Components.classes["@mozilla.org/consoleservice;1"] + .getService(Components.interfaces.nsIConsoleService); + } + try { + var threshold = prefs.getIntPref(LLVAR); + } catch (e) { + econsole.logStringMessage( "HTTPS Everywhere: Failed to read about:config LogLevel"); + threshold = WARN; + } + if (level >= threshold) { + dump("HTTPS Everywhere: "+str+"\n"); + econsole.logStringMessage("HTTPS Everywhere: " +str); + } +} + +/** +* XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4). +* XPCOMUtils.generateNSGetModule is for Mozilla 1.9.2 (Firefox 3.6). +*/ +if (XPCOMUtils.generateNSGetFactory) + var NSGetFactory = XPCOMUtils.generateNSGetFactory([HTTPSEverywhere]); +else + var NSGetModule = XPCOMUtils.generateNSGetModule([HTTPSEverywhere]); + +/* vim: set tabstop=4 expandtab: */ diff --git a/data/extensions/https-everywhere@eff.org/components/ssl-observatory.js b/data/extensions/https-everywhere@eff.org/components/ssl-observatory.js new file mode 100644 index 0000000..7b301d1 --- /dev/null +++ b/data/extensions/https-everywhere@eff.org/components/ssl-observatory.js @@ -0,0 +1,1026 @@ +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; + +const CI = Components.interfaces; +const CC = Components.classes; +const CR = Components.results; +const CU = Components.utils; + +// Log levels +VERB=1; +DBUG=2; +INFO=3; +NOTE=4; +WARN=5; + +BASE_REQ_SIZE=4096; +MAX_OUTSTANDING = 20; // Max # submission XHRs in progress +MAX_DELAYED = 32; // Max # XHRs are waiting around to be sent or retried +TIMEOUT = 60000; + +ASN_PRIVATE = -1; // Do not record the ASN this cert was seen on +ASN_IMPLICIT = -2; // ASN can be learned from connecting IP +ASN_UNKNOWABLE = -3; // Cert was seen in the absence of [trustworthy] Internet access + +// XXX: We should make the _observatory tree relative. +LLVAR="extensions.https_everywhere.LogLevel"; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/ctypes.jsm"); + + +const OS = Cc['@mozilla.org/observer-service;1'].getService(CI.nsIObserverService); + +const SERVICE_CTRID = "@eff.org/ssl-observatory;1"; +const SERVICE_ID=Components.ID("{0f9ab521-986d-4ad8-9c1f-6934e195c15c}"); +const SERVICE_NAME = "Anonymously Submits SSL certificates to EFF for security auditing."; +const LOADER = CC["@mozilla.org/moz/jssubscript-loader;1"].getService(CI.mozIJSSubScriptLoader); + +const _INCLUDED = {}; + +const INCLUDE = function(name) { + if (arguments.length > 1) + for (var j = 0, len = arguments.length; j < len; j++) + INCLUDE(arguments[j]); + else if (!_INCLUDED[name]) { + try { + LOADER.loadSubScript("chrome://https-everywhere/content/code/" + + name + ".js"); + _INCLUDED[name] = true; + } catch(e) { + dump("INCLUDE " + name + ": " + e + "\n"); + } + } +}; + +INCLUDE('Root-CAs'); +INCLUDE('sha256'); +INCLUDE('X509ChainWhitelist'); +INCLUDE('NSS'); + +function SSLObservatory() { + this.prefs = CC["@mozilla.org/preferences-service;1"] + .getService(CI.nsIPrefBranch); + + try { + // Check for torbutton + this.tor_logger = CC["@torproject.org/torbutton-logger;1"] + .getService(CI.nsISupports).wrappedJSObject; + this.torbutton_installed = true; + } catch(e) { + this.torbutton_installed = false; + } + + this.HTTPSEverywhere = CC["@eff.org/https-everywhere;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + + /* The proxy test result starts out null until the test is attempted. + * This is for UI notification purposes */ + this.proxy_test_successful = null; + this.proxy_test_callback = null; + this.cto_url = "https://check.torproject.org/?TorButton=true"; + // a regexp to match the above URL + this.cto_regexp = RegExp("^https://check\\.torproject\\.org/"); + + this.public_roots = root_ca_hashes; + + // Clear these on cookies-cleared observer event + this.already_submitted = {}; + this.delayed_submissions = {}; + + // Figure out the url to submit to + this.submit_host = null; + this.findSubmissionTarget(); + + // Used to track current number of pending requests to the server + this.current_outstanding_requests = 0; + + // We can't always know private browsing state per request, sometimes + // we have to guess based on what we've seen in the past + this.everSeenPrivateBrowsing = false; + + // Generate nonce to append to url, to catch in nsIProtocolProxyFilter + // and to protect against CSRF + this.csrf_nonce = "#"+Math.random().toString()+Math.random().toString(); + + this.compatJSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + + var pref_service = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranchInternal); + var branch = pref_service.QueryInterface(Components.interfaces.nsIPrefBranchInternal); + + branch.addObserver("extensions.https_everywhere._observatory.enabled", + this, false); + + if (this.myGetBoolPref("enabled")) { + OS.addObserver(this, "cookie-changed", false); + OS.addObserver(this, "http-on-examine-response", false); + + var dls = CC['@mozilla.org/docloaderservice;1'] + .getService(CI.nsIWebProgress); + dls.addProgressListener(this, + Ci.nsIWebProgress.NOTIFY_STATE_REQUEST); + } + + // Register protocolproxyfilter + this.pps = CC["@mozilla.org/network/protocol-proxy-service;1"] + .getService(CI.nsIProtocolProxyService); + + this.pps.registerFilter(this, 0); + this.wrappedJSObject = this; + + this.client_asn = ASN_PRIVATE; + if (this.myGetBoolPref("send_asn")) + this.setupASNWatcher(); + + try { + NSS.initialize(""); + } catch(e) { + this.log(WARN, "Failed to initialize NSS component:" + e); + } + + this.testProxySettings(); + + this.log(DBUG, "Loaded observatory component!"); +} + +SSLObservatory.prototype = { + // QueryInterface implementation, e.g. using the generateQI helper + QueryInterface: XPCOMUtils.generateQI( + [ CI.nsIObserver, + CI.nsIProtocolProxyFilter, + //CI.nsIWifiListener, + CI.nsIWebProgressListener, + CI.nsISupportsWeakReference, + CI.nsIInterfaceRequestor]), + + wrappedJSObject: null, // Initialized by constructor + + // properties required for XPCOM registration: + classDescription: SERVICE_NAME, + classID: SERVICE_ID, + contractID: SERVICE_CTRID, + + // https://developer.mozilla.org/En/How_to_check_the_security_state_of_an_XMLHTTPRequest_over_SSL + getSSLCertChain: function(channel) { + try { + // Do we have a valid channel argument? + if (!channel instanceof Ci.nsIChannel) { + return null; + } + var secInfo = channel.securityInfo; + + // Print general connection security state + if (secInfo instanceof Ci.nsITransportSecurityInfo) { + secInfo.QueryInterface(Ci.nsITransportSecurityInfo); + } else { + return null; + } + + if (secInfo instanceof Ci.nsISSLStatusProvider) { + return secInfo.QueryInterface(Ci.nsISSLStatusProvider). + SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert; + } + return null; + } catch(err) { + return null; + } + }, + + findSubmissionTarget: function() { + // Compute the URL that the Observatory will currently submit to + var host = this.prefs.getCharPref("extensions.https_everywhere._observatory.server_host"); + // Rebuild the regexp iff the host has changed + if (host != this.submit_host) { + this.submit_host = host; + this.submit_url = "https://" + host + "/submit_cert"; + this.submission_regexp = RegExp("^" + this.regExpEscape(this.submit_url)); + } + }, + + regExpEscape: function(s) { + // Borrowed from the Closure Library, + // https://closure-library.googlecode.com/svn/docs/closure_goog_string_string.js.source.html + return String(s).replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').replace(/\x08/g, '\\x08'); + }, + + notifyCertProblem: function(socketInfo, status, targetSite) { + this.log(NOTE, "cert warning for " + targetSite); + if (targetSite == "observatory.eff.org") { + this.log(WARN, "Surpressing observatory warning"); + return true; + } + return false; + }, + + setupASNWatcher: function() { + this.getClientASN(); + this.max_ap = null; + + // we currently do not actually do *any* ASN watching from the client + // (in other words, the db will not have ASNs for certs submitted + // through Tor, even if the user checks the "send ASN" option) + // all of this code for guessing at changes in our public IP via WiFi hints + // is therefore disabled + /* + // Observe network changes to get new ASNs + OS.addObserver(this, "network:offline-status-changed", false); + var pref_service = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranchInternal); + var proxy_branch = pref_service.QueryInterface(Ci.nsIPrefBranchInternal); + proxy_branch.addObserver("network.proxy", this, false); + + try { + var wifi_service = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor); + wifi_service.startWatching(this); + } catch(e) { + this.log(INFO, "Failed to register ASN change monitor: "+e); + }*/ + }, + + stopASNWatcher: function() { + this.client_asn = ASN_PRIVATE; + /* + // unhook the observers we registered above + OS.removeObserver(this, "network:offline-status-changed"); + var pref_service = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranchInternal); + var proxy_branch = pref_service.QueryInterface(Ci.nsIPrefBranchInternal); + proxy_branch.removeObserver(this, "network.proxy"); + try { + var wifi_service = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor); + wifi_service.stopWatching(this); + } catch(e) { + this.log(WARN, "Failed to stop wifi state monitor: "+e); + }*/ + }, + + getClientASN: function() { + // Fetch a new client ASN.. + if (!this.myGetBoolPref("send_asn")) { + this.client_asn = ASN_PRIVATE; + return; + } + else if (!this.torbutton_installed) { + this.client_asn = ASN_IMPLICIT; + return; + } + // XXX As a possible base case: the user is running Tor, is not using + // bridges, and has send_asn enabled: should we ping an eff.org URL to + // learn our ASN? + return; + }, + + /* + // Wifi status listener + onChange: function(accessPoints) { + try { + var max_ap = accessPoints[0].mac; + } catch(e) { + return null; // accessPoints[0] is undefined + } + var max_signal = accessPoints[0].signal; + var old_max_present = false; + for (var i=0; i<accessPoints.length; i++) { + if (accessPoints[i].mac == this.max_ap) { + old_max_present = true; + } + if (accessPoints[i].signal > max_signal) { + max_ap = accessPoints[i].mac; + max_signal = accessPoints[i].signal; + } + } + this.max_ap = max_ap; + if (!old_max_present) { + this.log(INFO, "Old access point is out of range. Getting new ASN"); + this.getClientASN(); + } else { + this.log(DBUG, "Old access point is still in range."); + } + }, + + // Wifi status listener + onError: function(value) { + // XXX: Do we care? + this.log(NOTE, "ASN change observer got an error: "+value); + this.getClientASN(); + }, + */ + + ourFingerprint: function(cert) { + // Calculate our custom fingerprint from an nsIX509Cert + return (cert.md5Fingerprint+cert.sha1Fingerprint).replace(":", "", "g"); + }, + + // onSecurity is used to listen for bad cert warnings + // There is also onSecurityStateChange, but it does not handle subdocuments. See git + // history for an implementation stub. + onStateChange: function(aProgress, aRequest, aState, aStatus) { + if (!aRequest) return; + var chan = null; + try { + chan = aRequest.QueryInterface(Ci.nsIHttpChannel); + } catch(e) { + return; + } + if (chan) { + if (!this.observatoryActive(chan)) return; + var certchain = this.getSSLCertChain(chan); + if (certchain) { + this.log(INFO, "Got state cert chain for " + + chan.originalURI.spec + "->" + chan.URI.spec + ", state: " + aState); + var warning = true; + this.submitCertChainForChannel(certchain, chan, warning); + } + } + }, + + observe: function(subject, topic, data) { + if (topic == "cookie-changed" && data == "cleared") { + this.already_submitted = {}; + this.delayed_submissions = {}; + this.log(INFO, "Cookies were cleared. Purging list of pending and already submitted certs"); + return; + } + + if ("http-on-examine-response" == topic) { + var channel = subject; + if (!this.observatoryActive(channel)) return; + + var certchain = this.getSSLCertChain(subject); + var warning = false; + this.submitCertChainForChannel(certchain, channel, warning); + } + + if (topic == "network:offline-status-changed" && data == "online") { + this.log(INFO, "Browser back online. Getting new ASN."); + this.getClientASN(); + return; + } + + if (topic == "nsPref:changed") { + // If the user toggles the SSL Observatory settings, we need to add or remove + // our observers + switch (data) { + case "network.proxy.ssl": + case "network.proxy.ssl_port": + case "network.proxy.socks": + case "network.proxy.socks_port": + // XXX: We somehow need to only call this once. Right now, we'll make + // like 3 calls to getClientASN().. The only thing I can think + // of is a timer... + this.log(INFO, "Proxy settings have changed. Getting new ASN"); + this.getClientASN(); + break; + case "extensions.https_everywhere._observatory.enabled": + if (this.myGetBoolPref("enabled")) { + this.pps.registerFilter(this, 0); + OS.addObserver(this, "cookie-changed", false); + OS.addObserver(this, "http-on-examine-response", false); + + var dls = CC['@mozilla.org/docloaderservice;1'] + .getService(CI.nsIWebProgress); + dls.addProgressListener(this, + Ci.nsIWebProgress.NOTIFY_STATE_REQUEST); + this.log(INFO,"SSL Observatory is now enabled via pref change!"); + } else { + try { + this.pps.unregisterFilter(this); + OS.removeObserver(this, "cookie-changed"); + OS.removeObserver(this, "http-on-examine-response"); + + var dls = CC['@mozilla.org/docloaderservice;1'] + .getService(CI.nsIWebProgress); + dls.removeProgressListener(this); + this.log(INFO,"SSL Observatory is now disabled via pref change!"); + } catch(e) { + this.log(WARN, "Removing SSL Observatory observers failed: "+e); + } + } + break; + } + return; + } + + }, + + submitCertChainForChannel: function(certchain, channel, warning) { + if (!certchain) { + return; + } + var host_ip = "-1"; + var httpchannelinternal = channel.QueryInterface(Ci.nsIHttpChannelInternal); + try { + host_ip = httpchannelinternal.remoteAddress; + } catch(e) { + this.log(INFO, "Could not get server IP address."); + } + + channel.QueryInterface(Ci.nsIHttpChannel); + var chainEnum = certchain.getChain(); + var chainArray = []; + var chainArrayFpStr = ''; + var fps = []; + for(var i = 0; i < chainEnum.length; i++) { + var cert = chainEnum.queryElementAt(i, Ci.nsIX509Cert); + chainArray.push(cert); + var fp = this.ourFingerprint(cert); + fps.push(fp); + chainArrayFpStr = chainArrayFpStr + fp; + } + var chain_hash = sha256_digest(chainArrayFpStr).toUpperCase(); + this.log(INFO, "SHA-256 hash of cert chain for "+new String(channel.URI.host)+" is "+ chain_hash); + + if(!this.myGetBoolPref("use_whitelist")) { + this.log(WARN, "Not using whitelist to filter cert chains."); + } + else if (this.isChainWhitelisted(chain_hash)) { + this.log(INFO, "This cert chain is whitelisted. Not submitting."); + return; + } + else { + this.log(INFO, "Cert chain is NOT whitelisted. Proceeding with submission."); + } + + if (channel.URI.port == -1) { + this.submitChainArray(chainArray, fps, new String(channel.URI.host), channel, host_ip, warning, false); + } else { + this.submitChainArray(chainArray, fps, channel.URI.host+":"+channel.URI.port, channel, host_ip, warning, false); + } + }, + + observatoryActive: function(channel) { + + if (!this.myGetBoolPref("enabled")) + return false; + + if (this.torbutton_installed && this.proxy_test_successful) { + // Allow Tor users to choose if they want to submit + // during tor and/or non-tor + if (this.myGetBoolPref("submit_during_tor") && + this.prefs.getBoolPref("extensions.torbutton.tor_enabled")) + return true; + + if (this.myGetBoolPref("submit_during_nontor") && + !this.prefs.getBoolPref("extensions.torbutton.tor_enabled")) + return true; + + return false; + } + + if (this.proxy_test_successful) { + return true; + } else if (this.myGetBoolPref("use_custom_proxy")) { + // no torbutton; the custom proxy is probably the user opting to + // submit certs without strong anonymisation. Because the + // anonymisation is weak, we avoid submitting during private browsing + // mode. + var pbm = this.inPrivateBrowsingMode(channel); + this.log(DBUG, "Private browsing mode: " + pbm); + return !pbm; + } + }, + + inPrivateBrowsingMode: function(channel) { + // In classic firefox fashion, there are multiple versions of this API + // https://developer.mozilla.org/EN/docs/Supporting_per-window_private_browsing + try { + // Firefox 20+, this state is per-window; + // should raise an exception on FF < 20 + CU.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + if (!(channel instanceof CI.nsIHttpChannel)) { + this.log(NOTE, "observatoryActive() without a channel"); + // This is a windowless request. We cannot tell if private browsing + // applies. Conservatively, if we have ever seen PBM, it might be + // active now + return this.everSeenPrivateBrowsing; + } + var win = this.HTTPSEverywhere.getWindowForChannel(channel); + if (!win) return this.everSeenPrivateBrowsing; // windowless request + + if (PrivateBrowsingUtils.isWindowPrivate(win)) { + this.everSeenPrivateBrowsing = true; + return true; + } + } catch (e) { + // Firefox < 20, this state is global + try { + var pbs = CC["@mozilla.org/privatebrowsing;1"].getService(CI.nsIPrivateBrowsingService); + if (pbs.privateBrowsingEnabled) { + this.everSeenPrivateBrowsing = true; + return true; + } + } catch (e) { /* seamonkey or very old firefox */ } + } + return false; + }, + + myGetBoolPref: function(prefstring) { + // syntactic sugar + return this.prefs.getBoolPref ("extensions.https_everywhere._observatory." + prefstring); + }, + + isChainWhitelisted: function(chainhash) { + if (X509ChainWhitelist == null) { + this.log(WARN, "Could not find whitelist of popular certificate chains, so ignoring whitelist"); + return false; + } + if (X509ChainWhitelist[chainhash] != null) { + return true; + } + return false; + }, + + findRootInChain: function(certArray) { + // Return the position in the chain Array of the/a root CA + var rootidx = -1; + var nextInChain = certArray[0].issuer; + for (var i = 0; i < certArray.length; i++) { + // Find the next cert in the valid chain + if (certArray[i].equals(nextInChain)) { + if (certArray[i].issuerName == certArray[i].subjectName) { + // All X509 root certs are self-signed + this.log(INFO, "Got root cert at position: "+i); + rootidx = i; + break; + } else { + // This is an intermediate CA cert; keep looking for the root + nextInChain = certArray[i].issuer; + } + } + } + return rootidx; + }, + + processConvergenceChain: function(chain) { + // Make sure the chain we're working with is sane, even if Convergence is + // present. + + // Convergence currently performs MITMs against the Firefox in order to + // get around https://bugzilla.mozilla.org/show_bug.cgi?id=644640. The + // end-entity cert produced by Convergence contains a copy of the real + // end-entity cert inside an X509v3 extension. We extract this and send + // it rather than the Convergence certs. + var convergence = Components.classes['@thoughtcrime.org/convergence;1']; + if (!convergence) return null; + convergence = convergence.getService().wrappedJSObject; + if (!convergence || !convergence.enabled) return null; + + this.log(INFO, "Convergence uses its own internal root certs; not submitting those"); + + //this.log(WARN, convergence.certificateStatus.getVerificiationStatus(chain.certArray[0])); + try { + var certInfo = this.extractRealLeafFromConveregenceLeaf(chain.certArray[0]); + var b64Cert = certInfo["certificate"]; + var certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(Ci.nsIX509CertDB); + chain.leaf = certDB.constructX509FromBase64(b64Cert); + chain.certArray = [chain.leaf]; + chain.fps = [this.ourFingerprint(chain.leaf)]; + } catch (e) { + this.log(WARN, "Failed to extract leaf cert from Convergence cert " + e); + chain.certArray = chain.certArray.slice(0,1); + chain.fps = chain.fps.slice(0,1); + } + + }, + + extractRealLeafFromConveregenceLeaf: function(certificate) { + // Copied from Convergence's CertificateStatus.getVerificiationStatus + var len = {}; + var derEncoding = certificate.getRawDER(len); + + var derItem = NSS.types.SECItem(); + derItem.data = NSS.lib.ubuffer(derEncoding); + derItem.len = len.value; + + var completeCertificate = NSS.lib.CERT_DecodeDERCertificate(derItem.address(), 1, null); + + var extItem = NSS.types.SECItem(); + var status = NSS.lib.CERT_FindCertExtension(completeCertificate, + NSS.lib.SEC_OID_NS_CERT_EXT_COMMENT, + extItem.address()); + if (status != -1) { + var encoded = ''; + var asArray = ctypes.cast(extItem.data, ctypes.ArrayType(ctypes.unsigned_char, extItem.len).ptr).contents; + var marker = false; + + for (var i=0;i<asArray.length;i++) { + if (marker) { + encoded += String.fromCharCode(asArray[i]); + } else if (asArray[i] == 0x00) { + marker = true; + } + } + + return JSON.parse(encoded); + } + }, + + shouldSubmit: function(chain, domain) { + // Return true if we should submit this chain to the SSL Observatory + var rootidx = this.findRootInChain(chain.certArray); + var ss = false; // ss: self-signed + + if (chain.leaf.issuerName == chain.leaf.subjectName) + ss = true; + + if (!this.myGetBoolPref("self_signed") && ss) { + this.log(INFO, "Not submitting self-signed cert for " + domain); + return false; + } + + if (!ss && !this.myGetBoolPref("alt_roots")) { + if (rootidx == -1) { + // A cert with an unknown/absent Issuer. Out of caution, don't submit these + this.log(INFO, "Cert for " + domain + " issued by unknown CA " + + chain.leaf.issuerName + " (not submitting due to settings)"); + return false; + } else if (!(chain.fps[rootidx] in this.public_roots)) { + // A cert with a known but non-public Issuer + this.log(INFO, "Got a private root cert. Ignoring domain " + +domain+" with root "+chain.fps[rootidx]); + return false; + } + } + + if (chain.fps[0] in this.already_submitted) { + this.log(INFO, "Already submitted cert for "+domain+". Ignoring"); + return false; + } + return true; + }, + + submitChainArray: function(certArray, fps, domain, channel, host_ip, warning, resubmitting) { + var base64Certs = []; + // Put all this chain data in one object so that it can be modified by + // subroutines if required + var c = {}; c.certArray = certArray; c.fps = fps; c.leaf = certArray[0]; + this.processConvergenceChain(c); + if (!this.shouldSubmit(c,domain)) return; + + // only try to submit now if there aren't too many outstanding requests + if (this.current_outstanding_requests > MAX_OUTSTANDING) { + this.log(WARN, "Too many outstanding requests ("+this.current_outstanding_requests+"), not submitting"); + + // if there are too many current requests but not too many + // delayed/pending ones, then delay this one + if (Object.keys(this.delayed_submissions).length < MAX_DELAYED) + if (!(c.fps[0] in this.delayed_submissions)) { + this.log(WARN, "Planning to retry submission..."); + let retry = function() { this.submitChainArray(certArray, fps, domain, channel, host_ip, warning, true); }; + this.delayed_submissions[c.fps[0]] = retry; + } + return; + } + + for (var i = 0; i < c.certArray.length; i++) { + var len = new Object(); + var derData = c.certArray[i].getRawDER(len); + let result = ""; + for (let j = 0, dataLength = derData.length; j < dataLength; ++j) + result += String.fromCharCode(derData[j]); + base64Certs.push(btoa(result)); + } + + var reqParams = []; + reqParams.push("domain="+domain); + reqParams.push("server_ip="+host_ip); + if (this.myGetBoolPref("testing")) { + reqParams.push("testing=1"); + // The server can compute these, but they're a nice test suite item! + reqParams.push("fplist="+this.compatJSON.encode(c.fps)); + } + reqParams.push("certlist="+this.compatJSON.encode(base64Certs)); + + if (resubmitting) { + reqParams.push("client_asn="+ASN_UNKNOWABLE); + } else { + reqParams.push("client_asn="+this.client_asn); + } + + if (this.myGetBoolPref("priv_dns")) { + reqParams.push("private_opt_in=1"); + } else { + reqParams.push("private_opt_in=0"); + } + + if (warning) { + reqParams.push("browser_warning=1"); + } else { + reqParams.push("browser_warning=0"); + } + + var params = reqParams.join("&") + "&padding=0"; + var tot_len = BASE_REQ_SIZE; + + this.log(INFO, "Submitting cert for "+domain); + this.log(DBUG, "submit_cert params: "+params); + + // Pad to exp scale. This is done because the distribution of cert sizes + // is almost certainly pareto, and definitely not uniform. + for (tot_len = BASE_REQ_SIZE; tot_len < params.length; tot_len*=2); + + while (params.length != tot_len) { + params += "0"; + } + + var that = this; // We have neither SSLObservatory nor this in scope in the lambda + + var win = channel ? this.HTTPSEverywhere.getWindowForChannel(channel) : null; + var req = this.buildRequest(params); + req.timeout = TIMEOUT; + + req.onreadystatechange = function(evt) { + if (req.readyState == 4) { + // pop off one outstanding request + that.current_outstanding_requests -= 1; + that.log(DBUG, "Popping one off of outstanding requests, current num is: "+that.current_outstanding_requests); + + if (req.status == 200) { + that.log(INFO, "Successful cert submission"); + if (!that.prefs.getBoolPref("extensions.https_everywhere._observatory.cache_submitted")) + if (c.fps[0] in that.already_submitted) + delete that.already_submitted[c.fps[0]]; + + // Retry up to two previously failed submissions + let n = 0; + for (let fp in that.delayed_submissions) { + that.log(WARN, "Retrying a submission..."); + that.delayed_submissions[fp](); + delete that.delayed_submissions[fp]; + if (++n >= 2) break; + } + } else if (req.status == 403) { + that.log(WARN, "The SSL Observatory has issued a warning about this certificate for " + domain); + try { + var warningObj = JSON.parse(req.responseText); + if (win) that.warnUser(warningObj, win, c.certArray[0]); + } catch(e) { + that.log(WARN, "Failed to process SSL Observatory cert warnings :( " + e); + that.log(WARN, req.responseText); + } + } else { + // Submission failed + if (c.fps[0] in that.already_submitted) + delete that.already_submitted[c.fps[0]]; + try { + that.log(WARN, "Cert submission failure "+req.status+": "+req.responseText); + } catch(e) { + that.log(WARN, "Cert submission failure and exception: "+e); + } + // If we don't have too many delayed submissions, and this isn't + // (somehow?) one of them, then plan to retry this submission later + if (Object.keys(that.delayed_submissions).length < MAX_DELAYED) + if (!(c.fps[0] in that.delayed_submissions)) { + that.log(WARN, "Planning to retry submission..."); + let retry = function() { that.submitChainArray(certArray, fps, domain, channel, host_ip, warning, true); }; + that.delayed_submissions[c.fps[0]] = retry; + } + + } + } + }; + + // Cache this here to prevent multiple submissions for all the content elements. + that.already_submitted[c.fps[0]] = true; + + // add one to current outstanding request number + that.current_outstanding_requests += 1; + that.log(DBUG, "Adding outstanding request, current num is: "+that.current_outstanding_requests); + req.send(params); + }, + + buildRequest: function(params) { + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + + // We do this again in case the user altered about:config + this.findSubmissionTarget(); + req.open("POST", this.submit_url+this.csrf_nonce, true); + + // Send the proper header information along with the request + // Do not set gzip header.. It will ruin the padding + req.setRequestHeader("X-Privacy-Info", "EFF SSL Observatory: https://eff.org/r.22c"); + req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + req.setRequestHeader("Content-length", params.length); + req.setRequestHeader("Connection", "close"); + // Need to clear useragent and other headers.. + req.setRequestHeader("User-Agent", ""); + req.setRequestHeader("Accept", ""); + req.setRequestHeader("Accept-Language", ""); + req.setRequestHeader("Accept-Encoding", ""); + req.setRequestHeader("Accept-Charset", ""); + return req; + }, + + warnUser: function(warningObj, win, cert) { + var aWin = CC['@mozilla.org/appshell/window-mediator;1'] + .getService(CI.nsIWindowMediator) + .getMostRecentWindow('navigator:browser'); + aWin.openDialog("chrome://https-everywhere/content/observatory-warning.xul", + "","chrome,centerscreen", warningObj, win, cert); + }, + + registerProxyTestNotification: function(callback_fcn) { + if (this.proxy_test_successful != null) { + /* Proxy test already ran. Callback immediately. */ + callback_fcn(this.proxy_test_successful); + this.proxy_test_callback = null; + return; + } else { + this.proxy_test_callback = callback_fcn; + } + }, + + testProxySettings: function() { + /* Plan: + * 1. Launch an async XMLHttpRequest to check.tp.o with magic nonce + * 3. Filter the nonce in protocolProxyFilter to use proxy settings + * 4. Async result function sets test result status based on check.tp.o + */ + this.proxy_test_successful = null; + + try { + var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Components.interfaces.nsIXMLHttpRequest); + var url = this.cto_url + this.csrf_nonce; + req.open('GET', url, true); + req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + req.overrideMimeType("text/xml"); + var that = this; // Scope gymnastics for async callback + req.onreadystatechange = function (oEvent) { + if (req.readyState === 4) { + that.proxy_test_successful = false; + + if(req.status == 200) { + if(!req.responseXML) { + that.log(INFO, "Tor check failed: No XML returned by check service."); + that.proxyTestFinished(); + return; + } + + var result = req.responseXML.getElementById('TorCheckResult'); + if(result===null) { + that.log(INFO, "Tor check failed: Non-XML returned by check service."); + } else if(typeof(result.target) == 'undefined' + || result.target === null) { + that.log(INFO, "Tor check failed: Busted XML returned by check service."); + } else if(result.target === "success") { + that.log(INFO, "Tor check succeeded."); + that.proxy_test_successful = true; + } else { + that.log(INFO, "Tor check failed: "+result.target); + } + } else { + that.log(INFO, "Tor check failed: HTTP Error "+req.status); + } + + /* Notify the UI of the test result */ + if (that.proxy_test_callback) { + that.proxy_test_callback(that.proxy_test_successful); + that.proxy_test_callback = null; + } + that.proxyTestFinished(); + } + }; + req.send(null); + } catch(e) { + this.proxy_test_successful = false; + if(e.result == 0x80004005) { // NS_ERROR_FAILURE + this.log(INFO, "Tor check failed: Proxy not running."); + } + this.log(INFO, "Tor check failed: Internal error: "+e); + if (this.proxy_test_callback) { + this.proxy_test_callback(this.proxy_test_successful); + this.proxy_test_callback = null; + } + that.proxyTestFinished(); + } + }, + + proxyTestFinished: function() { + if (!this.myGetBoolPref("enabled")) { + this.pps.unregisterFilter(this); + } + }, + + getProxySettings: function(testingForTor) { + // This may be called either for an Observatory submission, or during a test to see if Tor is + // present. The testingForTor argument is true in the latter case. + var proxy_settings = ["direct", "", 0]; + this.log(INFO,"in getProxySettings()"); + var custom_proxy_type = this.prefs.getCharPref("extensions.https_everywhere._observatory.proxy_type"); + if (this.torbutton_installed && this.myGetBoolPref("use_tor_proxy")) { + this.log(INFO,"CASE: use_tor_proxy"); + // extract torbutton proxy settings + proxy_settings[0] = "http"; + proxy_settings[1] = this.prefs.getCharPref("extensions.torbutton.https_proxy"); + proxy_settings[2] = this.prefs.getIntPref("extensions.torbutton.https_port"); + + if (proxy_settings[2] == 0) { + proxy_settings[0] = "socks"; + proxy_settings[1] = this.prefs.getCharPref("extensions.torbutton.socks_host"); + proxy_settings[2] = this.prefs.getIntPref("extensions.torbutton.socks_port"); + } + /* Regarding the test below: + * + * custom_proxy_type == "direct" is indicative of the user having selected "submit certs even if + * Tor is not available", rather than true custom Tor proxy settings. So in that case, there's + * not much point probing to see if the direct proxy is actually a Tor connection, and + * localhost:9050 is a better bet. People whose networks send all traffc through Tor can just + * tell the Observatory to submit certs without Tor. + */ + } else if (this.myGetBoolPref("use_custom_proxy") && !(testingForTor && custom_proxy_type == "direct")) { + this.log(INFO,"CASE: use_custom_proxy"); + proxy_settings[0] = custom_proxy_type; + proxy_settings[1] = this.prefs.getCharPref("extensions.https_everywhere._observatory.proxy_host"); + proxy_settings[2] = this.prefs.getIntPref("extensions.https_everywhere._observatory.proxy_port"); + } else { + /* Take a guess at default tor proxy settings */ + this.log(INFO,"CASE: try localhost:9050"); + proxy_settings[0] = "socks"; + proxy_settings[1] = "localhost"; + proxy_settings[2] = 9050; + } + this.log(INFO, "Using proxy: " + proxy_settings); + return proxy_settings; + }, + + applyFilter: function(aProxyService, inURI, aProxy) { + + try { + if (inURI instanceof Ci.nsIURI) { + var aURI = inURI.QueryInterface(Ci.nsIURI); + if (!aURI) this.log(WARN, "Failed to QI to nsIURI!"); + } else { + this.log(WARN, "applyFilter called without URI"); + } + } catch (e) { + this.log(WARN, "EXPLOSION: " + e); + } + + var isSubmission = this.submission_regexp.test(aURI.spec); + var testingForTor = this.cto_regexp.test(aURI.spec); + + if (isSubmission || testingForTor) { + if (aURI.path.search(this.csrf_nonce+"$") != -1) { + + this.log(INFO, "Got observatory url + nonce: "+aURI.spec); + var proxy_settings = null; + var proxy = null; + + // Send it through tor by creating an nsIProxy instance + // for the torbutton proxy settings. + try { + proxy_settings = this.getProxySettings(testingForTor); + proxy = this.pps.newProxyInfo(proxy_settings[0], proxy_settings[1], + proxy_settings[2], + Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST, + 0xFFFFFFFF, null); + } catch(e) { + this.log(WARN, "Error specifying proxy for observatory: "+e); + } + + this.log(INFO, "Specifying proxy: "+proxy); + + // TODO: Use new identity or socks u/p to ensure we get a unique + // tor circuit for this request + return proxy; + } + } + return aProxy; + }, + + // [optional] an array of categories to register this component in. + // Hack to cause us to get instantiate early + _xpcom_categories: [ { category: "profile-after-change" }, ], + + encString: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + encStringS: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', + + log: function(level, str) { + var econsole = CC["@mozilla.org/consoleservice;1"] + .getService(CI.nsIConsoleService); + try { + var threshold = this.prefs.getIntPref(LLVAR); + } catch (e) { + econsole.logStringMessage( "SSL Observatory: Failed to read about:config LogLevel"); + threshold = WARN; + } + if (level >= threshold) { + dump("SSL Observatory: "+str+"\n"); + econsole.logStringMessage("SSL Observatory: " +str); + } + } +}; + +/** +* XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4). +* XPCOMUtils.generateNSGetModule is for Mozilla 1.9.2 (Firefox 3.6). +*/ +if (XPCOMUtils.generateNSGetFactory) + var NSGetFactory = XPCOMUtils.generateNSGetFactory([SSLObservatory]); +else + var NSGetModule = XPCOMUtils.generateNSGetModule([SSLObservatory]); |