diff options
author | Ruben Rodriguez <ruben@gnu.org> | 2014-10-20 02:24:51 +0200 |
---|---|---|
committer | Ruben Rodriguez <ruben@gnu.org> | 2014-10-20 02:24:51 +0200 |
commit | 6e7918b6ccb69876d339a320091fdee811445395 (patch) | |
tree | 31cb88ee438d652fddefca1193f70289a8b3dcc8 /data/extensions/https-everywhere@eff.org/components/ssl-observatory.js | |
parent | 60e5b13c35d4d3ba21bb03b026750a0a414f6c77 (diff) |
Generalize data directory
Diffstat (limited to 'data/extensions/https-everywhere@eff.org/components/ssl-observatory.js')
-rw-r--r-- | data/extensions/https-everywhere@eff.org/components/ssl-observatory.js | 1026 |
1 files changed, 1026 insertions, 0 deletions
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]); |