summaryrefslogtreecommitdiff
path: root/data/extensions/https-everywhere-eff@eff.org/components/ssl-observatory.js
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/https-everywhere-eff@eff.org/components/ssl-observatory.js')
-rw-r--r--data/extensions/https-everywhere-eff@eff.org/components/ssl-observatory.js945
1 files changed, 945 insertions, 0 deletions
diff --git a/data/extensions/https-everywhere-eff@eff.org/components/ssl-observatory.js b/data/extensions/https-everywhere-eff@eff.org/components/ssl-observatory.js
new file mode 100644
index 0000000..76cc116
--- /dev/null
+++ b/data/extensions/https-everywhere-eff@eff.org/components/ssl-observatory.js
@@ -0,0 +1,945 @@
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+const CI = Components.interfaces;
+const CC = Components.classes;
+const CR = Components.results;
+
+// Log levels
+let VERB=1;
+let DBUG=2;
+let INFO=3;
+let NOTE=4;
+let WARN=5;
+
+let BASE_REQ_SIZE=4096;
+let TIMEOUT = 60000;
+let MAX_OUTSTANDING = 20; // Max # submission XHRs in progress
+let MAX_DELAYED = 32; // Max # XHRs are waiting around to be sent or retried
+
+let ASN_PRIVATE = -1; // Do not record the ASN this cert was seen on
+let ASN_IMPLICIT = -2; // ASN can be learned from connecting IP
+let ASN_UNKNOWABLE = -3; // Cert was seen in the absence of [trustworthy] Internet access
+
+// XXX: We should make the _observatory tree relative.
+let LLVAR="extensions.https_everywhere.LogLevel";
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/ctypes.jsm");
+
+// Alias to reduce the number of spurious warnings from amo-validator.
+let tcypes = ctypes;
+
+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
+ var tor_logger_component = CC["@torproject.org/torbutton-logger;1"];
+ if (tor_logger_component) {
+ this.tor_logger =
+ tor_logger_component.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 = {};
+ OS.addObserver(this, "cookie-changed", false);
+
+ // 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;
+
+ // 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);
+
+ // Register observer
+ OS.addObserver(this, "http-on-examine-response", false);
+
+ // 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.nsIBadCertListener2]),
+
+ 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
+ getSSLCert: 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();
+ },
+ */
+
+ // Calculate the MD5 fingerprint for a cert. This is the fingerprint of the
+ // DER-encoded form, same as the result of
+ // openssl x509 -md5 -fingerprint -noout
+ // We use this because the SSL Observatory depends in many places on a special
+ // fingerprint which is the concatenation of MD5+SHA1, and the MD5 fingerprint
+ // is no longer available on the cert object.
+ // Implementation cribbed from
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsICryptoHash
+ md5Fingerprint: function(cert) {
+ var len = new Object();
+ var derData = cert.getRawDER(len);
+ var ch = CC["@mozilla.org/security/hash;1"].createInstance(CI.nsICryptoHash);
+ ch.init(ch.MD5);
+ ch.update(derData,derData.length);
+ var h = ch.finish(false);
+
+ function toHexString(charCode) {
+ return ("0" + charCode.toString(16)).slice(-2);
+ }
+ return [toHexString(h.charCodeAt(i)) for (i in h)].join("").toUpperCase();
+ },
+
+ ourFingerprint: function(cert) {
+ // Calculate our custom fingerprint from an nsIX509Cert
+ return (this.md5Fingerprint(cert)+cert.sha1Fingerprint).replace(":", "", "g");
+ },
+
+ 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 (topic == "nsPref:changed") {
+ // 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...
+ if (data == "network.proxy.ssl" || data == "network.proxy.ssl_port" ||
+ data == "network.proxy.socks" || data == "network.proxy.socks_port") {
+ this.log(INFO, "Proxy settings have changed. Getting new ASN");
+ this.getClientASN();
+ }
+ return;
+ }
+
+ if (topic == "network:offline-status-changed" && data == "online") {
+ this.log(INFO, "Browser back online. Getting new ASN.");
+ this.getClientASN();
+ return;
+ }
+
+ if ("http-on-examine-response" == topic) {
+
+ if (!this.observatoryActive()) return;
+
+ var host_ip = "-1";
+ var httpchannelinternal = subject.QueryInterface(Ci.nsIHttpChannelInternal);
+ try {
+ host_ip = httpchannelinternal.remoteAddress;
+ } catch(e) {
+ this.log(INFO, "Could not get server IP address.");
+ }
+ subject.QueryInterface(Ci.nsIHttpChannel);
+ var certchain = this.getSSLCert(subject);
+ if (certchain) {
+ 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(subject.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 (subject.URI.port == -1) {
+ this.submitChain(chainArray, fps, new String(subject.URI.host), subject, host_ip, false);
+ } else {
+ this.submitChain(chainArray, fps, subject.URI.host+":"+subject.URI.port, subject, host_ip, false);
+ }
+ }
+ }
+ },
+
+ observatoryActive: function() {
+
+ 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.
+ try {
+ var pbs = CC["@mozilla.org/privatebrowsing;1"].getService(CI.nsIPrivateBrowsingService);
+ if (pbs.privateBrowsingEnabled) return false;
+ } catch (e) { /* seamonkey or old firefox */ }
+
+ return true;
+ }
+
+ 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 = tcypes.cast(extItem.data, tcypes.ArrayType(tcypes.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;
+
+ 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;
+ },
+
+ submitChain: function(certArray, fps, domain, channel, host_ip, 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.submitChain(certArray, fps, domain, channel, host_ip, 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");
+ }
+
+ 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 HTTPSEverywhere = CC["@eff.org/https-everywhere;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ var win = null;
+ if (channel) {
+ var browser = this.HTTPSEverywhere.getBrowserForChannel(channel);
+ if (browser) {
+ var win = browser.contentWindow;
+ }
+ }
+ 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") &&
+ 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 &&
+ c.fps[0] in that.delayed_submissions) {
+ that.log(WARN, "Planning to retry submission...");
+ let retry = function() { that.submitChain(certArray, fps, domain, channel, host_ip, 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://www.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.");
+ 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;
+ }
+ }
+ };
+ 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;
+ }
+ }
+ },
+
+ 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) {
+ var levelName = ["", "VERB", "DBUG", "INFO", "NOTE", "WARN"][level];
+ var prefix = "SSL Observatory " + levelName + ": ";
+ // dump() prints to browser stdout. That's sometimes undesireable,
+ // so only do it when a pref is set (running from test.sh enables
+ // this pref).
+ if (this.prefs.getBoolPref("extensions.https_everywhere.log_to_stdout")) {
+ dump(prefix + str + "\n");
+ }
+ econsole.logStringMessage(prefix + 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]);