bug 1328718 - implement system add-on to facilitate rollout of disabling SHA-1 r=Felipe,jcj data-review=bsmedberg
☠☠ backed out by 80d616e63bc5 ☠ ☠
authorDavid Keeler <dkeeler@mozilla.com>
Thu, 12 Jan 2017 11:54:22 -0800
changeset 375190 1da29f8933816efad948dd41e9138f2346de2e84
parent 375189 367612b00df056a10b60bd6e334193b33710b7cd
child 375191 f3bdcf57e1cfd674cc145edc5f9578e9293fc84c
push id6996
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 20:48:21 +0000
treeherdermozilla-beta@d89512dab048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersFelipe, jcj
bugs1328718
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 1328718 - implement system add-on to facilitate rollout of disabling SHA-1 r=Felipe,jcj data-review=bsmedberg MozReview-Commit-ID: L5Q5VGr6UPU
browser/extensions/disableSHA1rollout/README.md
browser/extensions/disableSHA1rollout/bootstrap.js
browser/extensions/disableSHA1rollout/install.rdf.in
browser/extensions/disableSHA1rollout/moz.build
browser/extensions/moz.build
new file mode 100644
--- /dev/null
+++ b/browser/extensions/disableSHA1rollout/README.md
@@ -0,0 +1,99 @@
+This system add-on is a follow-up to the MITM prevalence experiment. The purpose
+is to facilitate rolling out the disabling of SHA-1 in signatures on
+certificates issued by publicly-trusted roots. When installed, this add-on will
+perform a number of checks to determine if it should change the preference that
+controls the SHA-1 policy. First, this should only apply to users on the beta
+update channel. It should also only apply to users who have not otherwise
+changed the policy to always allow or always forbid SHA-1. Additionally, it
+must double-check that the user is not affected by a TLS intercepting proxy
+using a publicly-trusted root. If these checks pass, the add-on will divide the
+population into a test group and a control group (starting on a 10%/90% split).
+The test group will have the policy changed. After doing this, a telemetry
+payload is reported with the following values:
+
+* cohortName -- the name of the group this user is in:
+  1. "notSafeToDisableSHA1" if the user is behind a MITM proxy using a
+     publicly-trusted root
+  2. "optedOut" if the user already set the SHA-1 policy to always allow or
+     always forbid
+  3. "optedIn" if the user already set the SHA-1 policy to only allow for
+     non-built-in roots
+  4. "test" if the user is in the test cohort (and SHA-1 will be disabled)
+  5. "control" if the user is not in the test cohort
+* errorCode -- 0 for successful connections, some PR error code otherwise
+* error -- a short description of one of four error conditions encountered, if
+  applicable, and an empty string otherwise:
+  1. "timeout" if the connection to telemetry.mozilla.org timed out
+  2. "user override" if the user has stored a permanent certificate exception
+     override for telemetry.mozilla.org (due to technical limitations, we can't
+     gather much information in this situation)
+  3. "certificate reverification" if re-building the certificate chain after
+     connecting failed for some reason (unfortunately this step is necessary
+     due to technical limitations)
+  4. "connection error" if the connection to telemetry.mozilla.org failed for
+     another reason
+* chain -- a list of dictionaries each corresponding to a certificate in the
+  verified certificate chain, if it was successfully constructed. The first
+  entry is the end-entity certificate. The last entry is the root certificate.
+  This will be empty if the connection failed or if reverification failed. Each
+  element in the list contains the following values:
+  * sha256Fingerprint -- a hex string representing the SHA-256 hash of the
+    certificate
+  * isBuiltInRoot -- true if the certificate is a trust anchor in the web PKI,
+    false otherwise
+  * signatureAlgorithm -- a description of the algorithm used to sign the
+    certificate. Will be one of "md2WithRSAEncryption", "md5WithRSAEncryption",
+    "sha1WithRSAEncryption", "sha256WithRSAEncryption",
+    "sha384WithRSAEncryption", "sha512WithRSAEncryption", "ecdsaWithSHA1",
+    "ecdsaWithSHA224", "ecdsaWithSHA256", "ecdsaWithSHA384", "ecdsaWithSHA512",
+    or "unknown".
+* disabledSHA1 -- true if SHA-1 was disabled, false otherwise
+* didNotDisableSHA1Because -- a short string describing why SHA-1 could not be
+    disabled, if applicable. Reasons are limited to:
+    1. "MITM" if the user is behind a TLS intercepting proxy using a
+       publicly-trusted root
+    2. "connection error" if there was an error connecting to
+       telemetry.mozilla.org
+    3. "code error" if some inconsistent state was detected, and it was
+       determined that the experiment should not attempt to change the
+       preference
+    4. "preference:userReset" if the user reset the SHA-1 policy after it had
+       been changed by this add-on
+    5. "preference:allow" if the user had already configured Firefox to always
+       accept SHA-1 signatures
+    6. "preference:forbid" if the user had already configured Firefox to always
+       forbid SHA-1 signatures
+
+For a connection not intercepted by a TLS proxy and where the user is in the
+test cohort, the expected result will be:
+
+    { "cohortName": "test",
+      "errorCode": 0,
+      "error": "",
+      "chain": [
+        { "sha256Fingerprint": "197feaf3faa0f0ad637a89c97cb91336bfc114b6b3018203cbd9c3d10c7fa86c",
+          "isBuiltInRoot": false,
+          "signatureAlgorithm": "sha256WithRSAEncryption"
+        },
+        { "sha256Fingerprint": "154c433c491929c5ef686e838e323664a00e6a0d822ccc958fb4dab03e49a08f",
+          "isBuiltInRoot": false,
+          "signatureAlgorithm": "sha256WithRSAEncryption"
+        },
+        { "sha256Fingerprint": "4348a0e9444c78cb265e058d5e8944b4d84f9662bd26db257f8934a443c70161",
+          "isBuiltInRoot": true,
+          "signatureAlgorithm": "sha1WithRSAEncryption"
+        }
+      ],
+      "disabledSHA1": true,
+      "didNotDisableSHA1Because": ""
+    }
+
+When this result is encountered, the user's preferences are updated to disable
+SHA-1 in signatures on certificates issued by publicly-trusted roots.
+Similarly, if the user is behind a TLS intercepting proxy but the root
+certificate is not part of the public web PKI, we can also disable SHA-1 in
+signatures on certificates issued by publicly-trusted roots.
+
+If the user has already indicated in their preferences that they will always
+accept SHA-1 in signatures or that they will never accept SHA-1 in signatures,
+then the preference is not changed.
copy from browser/extensions/e10srollout/bootstrap.js
copy to browser/extensions/disableSHA1rollout/bootstrap.js
--- a/browser/extensions/e10srollout/bootstrap.js
+++ b/browser/extensions/disableSHA1rollout/bootstrap.js
@@ -4,171 +4,303 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/UpdateUtils.jsm");
+Cu.import("resource://gre/modules/TelemetryController.jsm");
 
- // The amount of people to be part of e10s
+ // Percentage of the population to attempt to disable SHA-1 for, by channel.
 const TEST_THRESHOLD = {
-  "beta"    : 0.5,  // 50%
-  "release" : 1.0,  // 100%
-};
-
-const ADDON_ROLLOUT_POLICY = {
-  "beta"    : "51alladdons", // Any WebExtension or addon except with mpc = false
-  "release" : "51set1",
+  beta: 0.1, // 10%
 };
 
-const PREF_COHORT_SAMPLE       = "e10s.rollout.cohortSample";
-const PREF_COHORT_NAME         = "e10s.rollout.cohort";
-const PREF_E10S_OPTED_IN       = "browser.tabs.remote.autostart";
-const PREF_E10S_FORCE_ENABLED  = "browser.tabs.remote.force-enable";
-const PREF_E10S_FORCE_DISABLED = "browser.tabs.remote.force-disable";
-const PREF_TOGGLE_E10S         = "browser.tabs.remote.autostart.2";
-const PREF_E10S_ADDON_POLICY   = "extensions.e10s.rollout.policy";
-const PREF_E10S_ADDON_BLOCKLIST = "extensions.e10s.rollout.blocklist";
-const PREF_E10S_HAS_NONEXEMPT_ADDON = "extensions.e10s.rollout.hasAddon";
+const PREF_COHORT_SAMPLE = "disableSHA1.rollout.cohortSample";
+const PREF_COHORT_NAME = "disableSHA1.rollout.cohort";
+const PREF_SHA1_POLICY = "security.pki.sha1_enforcement_level";
+const PREF_SHA1_POLICY_SET_BY_ADDON = "disableSHA1.rollout.policySetByAddOn";
+const PREF_SHA1_POLICY_RESET_BY_USER = "disableSHA1.rollout.userResetPref";
+
+const SHA1_MODE_ALLOW = 0;
+const SHA1_MODE_FORBID = 1;
+const SHA1_MODE_IMPORTED_ROOTS_ONLY = 3;
+const SHA1_MODE_CURRENT_DEFAULT = 4;
 
 function startup() {
-  // In theory we only need to run this once (on install()), but
-  // it's better to also run it on every startup. If the user has
-  // made manual changes to the prefs, this will keep the data
-  // reported more accurate.
-  // It's also fine (and preferred) to just do it here on startup
-  // (instead of observing prefs), because e10s takes a restart
-  // to take effect, so we keep the data based on how it was when
-  // the session started.
-  defineCohort();
+  Preferences.observe(PREF_SHA1_POLICY, policyPreferenceChanged);
 }
 
 function install() {
-  defineCohort();
+  let updateChannel = UpdateUtils.getUpdateChannel(false);
+  if (updateChannel in TEST_THRESHOLD) {
+    makeRequest().then(defineCohort).catch((e) => console.error(e));
+  }
 }
 
-let cohortDefinedOnThisSession = false;
-
-function defineCohort() {
-  // Avoid running twice when it was called by install() first
-  if (cohortDefinedOnThisSession) {
-    return;
-  }
-  cohortDefinedOnThisSession = true;
-
-  let updateChannel = UpdateUtils.getUpdateChannel(false);
-  if (!(updateChannel in TEST_THRESHOLD)) {
-    setCohort("unsupportedChannel");
-    return;
-  }
-
-  let addonPolicy = "unknown";
-  if (updateChannel in ADDON_ROLLOUT_POLICY) {
-    addonPolicy = ADDON_ROLLOUT_POLICY[updateChannel];
-    Preferences.set(PREF_E10S_ADDON_POLICY, addonPolicy);
-    // This is also the proper place to set the blocklist pref
-    // in case it is necessary.
-
-    // Tab Mix Plus exception tracked at bug 1185672.
-    Preferences.set(PREF_E10S_ADDON_BLOCKLIST,
-                    "{dc572301-7619-498c-a57d-39143191b318}");
-  } else {
-    Preferences.reset(PREF_E10S_ADDON_POLICY);
-  }
-
-  let userOptedOut = optedOut();
-  let userOptedIn = optedIn();
-  let disqualified = (Services.appinfo.multiprocessBlockPolicy != 0);
-  let testGroup = (getUserSample() < TEST_THRESHOLD[updateChannel]);
-  let hasNonExemptAddon = Preferences.get(PREF_E10S_HAS_NONEXEMPT_ADDON, false);
-  let temporaryDisqualification = getTemporaryDisqualification();
-
-  let cohortPrefix = "";
-  if (disqualified) {
-    cohortPrefix = "disqualified-";
-  } else if (hasNonExemptAddon) {
-    cohortPrefix = `addons-set${addonPolicy}-`;
-  }
-
-  if (userOptedOut) {
-    setCohort("optedOut");
-  } else if (userOptedIn) {
-    setCohort("optedIn");
-  } else if (temporaryDisqualification != "") {
-    // Users who are disqualified by the backend (from multiprocessBlockPolicy)
-    // can be put into either the test or control groups, because e10s will
-    // still be denied by the backend, which is useful so that the E10S_STATUS
-    // telemetry probe can be correctly set.
-
-    // For these volatile disqualification reasons, however, we must not try
-    // to activate e10s because the backend doesn't know about it. E10S_STATUS
-    // here will be accumulated as "2 - Disabled", which is fine too.
-    setCohort(`temp-disqualified-${temporaryDisqualification}`);
-    Preferences.reset(PREF_TOGGLE_E10S);
-  } else if (testGroup) {
-    setCohort(`${cohortPrefix}test`);
-    Preferences.set(PREF_TOGGLE_E10S, true);
-  } else {
-    setCohort(`${cohortPrefix}control`);
-    Preferences.reset(PREF_TOGGLE_E10S);
+function policyPreferenceChanged() {
+  let currentPrefValue = Preferences.get(PREF_SHA1_POLICY,
+                                         SHA1_MODE_CURRENT_DEFAULT);
+  Preferences.reset(PREF_SHA1_POLICY_RESET_BY_USER);
+  if (currentPrefValue == SHA1_MODE_CURRENT_DEFAULT) {
+    Preferences.set(PREF_SHA1_POLICY_RESET_BY_USER, true);
   }
 }
 
+function defineCohort(result) {
+  let userOptedOut = optedOut();
+  let userOptedIn = optedIn();
+  let shouldNotDisableSHA1Because = reasonToNotDisableSHA1(result);
+  let safeToDisableSHA1 = shouldNotDisableSHA1Because.length == 0;
+  let updateChannel = UpdateUtils.getUpdateChannel(false);
+  let testGroup = getUserSample() < TEST_THRESHOLD[updateChannel];
+
+  let cohortName;
+  if (!safeToDisableSHA1) {
+    cohortName = "notSafeToDisableSHA1";
+  } else if (userOptedOut) {
+    cohortName = "optedOut";
+  } else if (userOptedIn) {
+    cohortName = "optedIn";
+  } else if (testGroup) {
+    cohortName = "test";
+    Preferences.ignore(PREF_SHA1_POLICY, policyPreferenceChanged);
+    Preferences.set(PREF_SHA1_POLICY, SHA1_MODE_IMPORTED_ROOTS_ONLY);
+    Preferences.observe(PREF_SHA1_POLICY, policyPreferenceChanged);
+    Preferences.set(PREF_SHA1_POLICY_SET_BY_ADDON, true);
+  } else {
+    cohortName = "control";
+  }
+  Preferences.set(PREF_COHORT_NAME, cohortName);
+  reportTelemetry(result, cohortName, shouldNotDisableSHA1Because,
+                  cohortName == "test");
+}
+
 function shutdown(data, reason) {
+  Preferences.ignore(PREF_SHA1_POLICY, policyPreferenceChanged);
 }
 
 function uninstall() {
 }
 
 function getUserSample() {
   let prefValue = Preferences.get(PREF_COHORT_SAMPLE, undefined);
   let value = 0.0;
 
   if (typeof(prefValue) == "string") {
     value = parseFloat(prefValue, 10);
     return value;
   }
 
-  if (typeof(prefValue) == "number") {
-    // convert old integer value
-    value = prefValue / 100;
-  } else {
-    value = Math.random();
-  }
+  value = Math.random();
 
   Preferences.set(PREF_COHORT_SAMPLE, value.toString().substr(0, 8));
   return value;
 }
 
-function setCohort(cohortName) {
-  Preferences.set(PREF_COHORT_NAME, cohortName);
-  try {
-    if (Ci.nsICrashReporter) {
-      Services.appinfo.QueryInterface(Ci.nsICrashReporter).annotateCrashReport("E10SCohort", cohortName);
+function reportTelemetry(result, cohortName, didNotDisableSHA1Because,
+                         disabledSHA1) {
+  result.cohortName = cohortName;
+  result.disabledSHA1 = disabledSHA1;
+  if (cohortName == "optedOut") {
+    let userResetPref = Preferences.get(PREF_SHA1_POLICY_RESET_BY_USER, false);
+    let currentPrefValue = Preferences.get(PREF_SHA1_POLICY,
+                                           SHA1_MODE_CURRENT_DEFAULT);
+    if (userResetPref) {
+      didNotDisableSHA1Because = "preference:userReset";
+    } else if (currentPrefValue == SHA1_MODE_ALLOW) {
+      didNotDisableSHA1Because = "preference:allow";
+    } else {
+      didNotDisableSHA1Because = "preference:forbid";
     }
-  } catch (e) {}
+  }
+  result.didNotDisableSHA1Because = didNotDisableSHA1Because;
+  return TelemetryController.submitExternalPing("disableSHA1rollout", result,
+                                                {});
 }
 
 function optedIn() {
-  return Preferences.get(PREF_E10S_OPTED_IN, false) ||
-         Preferences.get(PREF_E10S_FORCE_ENABLED, false);
+  let policySetByAddOn = Preferences.get(PREF_SHA1_POLICY_SET_BY_ADDON, false);
+  let currentPrefValue = Preferences.get(PREF_SHA1_POLICY,
+                                         SHA1_MODE_CURRENT_DEFAULT);
+  return currentPrefValue == SHA1_MODE_IMPORTED_ROOTS_ONLY && !policySetByAddOn;
 }
 
 function optedOut() {
-  // Users can also opt-out by toggling back the pref to false.
-  // If they reset the pref instead they might be re-enabled if
-  // they are still part of the threshold.
-  return Preferences.get(PREF_E10S_FORCE_DISABLED, false) ||
-         (Preferences.isSet(PREF_TOGGLE_E10S) &&
-          Preferences.get(PREF_TOGGLE_E10S) == false);
+  // Users can also opt-out by setting the policy to always allow or always
+  // forbid SHA-1, or by resetting the preference after this add-on has changed
+  // it (in that case, this will be reported the next time this add-on is
+  // updated).
+  let currentPrefValue = Preferences.get(PREF_SHA1_POLICY,
+                                         SHA1_MODE_CURRENT_DEFAULT);
+  let userResetPref = Preferences.get(PREF_SHA1_POLICY_RESET_BY_USER, false);
+  return currentPrefValue == SHA1_MODE_ALLOW ||
+         currentPrefValue == SHA1_MODE_FORBID ||
+         userResetPref;
+}
+
+function delocalizeAlgorithm(localizedString) {
+  let bundle = Services.strings.createBundle(
+    "chrome://pipnss/locale/pipnss.properties");
+  let algorithmStringIdsToOIDDescriptionMap = {
+    "CertDumpMD2WithRSA":                       "md2WithRSAEncryption",
+    "CertDumpMD5WithRSA":                       "md5WithRSAEncryption",
+    "CertDumpSHA1WithRSA":                      "sha1WithRSAEncryption",
+    "CertDumpSHA256WithRSA":                    "sha256WithRSAEncryption",
+    "CertDumpSHA384WithRSA":                    "sha384WithRSAEncryption",
+    "CertDumpSHA512WithRSA":                    "sha512WithRSAEncryption",
+    "CertDumpAnsiX962ECDsaSignatureWithSha1":   "ecdsaWithSHA1",
+    "CertDumpAnsiX962ECDsaSignatureWithSha224": "ecdsaWithSHA224",
+    "CertDumpAnsiX962ECDsaSignatureWithSha256": "ecdsaWithSHA256",
+    "CertDumpAnsiX962ECDsaSignatureWithSha384": "ecdsaWithSHA384",
+    "CertDumpAnsiX962ECDsaSignatureWithSha512": "ecdsaWithSHA512",
+  };
+
+  let description;
+  Object.keys(algorithmStringIdsToOIDDescriptionMap).forEach((l10nID) => {
+    let candidateLocalizedString = bundle.GetStringFromName(l10nID);
+    if (localizedString == candidateLocalizedString) {
+      description = algorithmStringIdsToOIDDescriptionMap[l10nID];
+    }
+  });
+  if (!description) {
+    return "unknown";
+  }
+  return description;
+}
+
+function getSignatureAlgorithm(cert) {
+  // Certificate  ::=  SEQUENCE  {
+  //      tbsCertificate       TBSCertificate,
+  //      signatureAlgorithm   AlgorithmIdentifier,
+  //      signatureValue       BIT STRING  }
+  let certificate = cert.ASN1Structure.QueryInterface(Ci.nsIASN1Sequence);
+  let signatureAlgorithm = certificate.ASN1Objects
+                                      .queryElementAt(1, Ci.nsIASN1Sequence);
+  // AlgorithmIdentifier  ::=  SEQUENCE  {
+  //      algorithm               OBJECT IDENTIFIER,
+  //      parameters              ANY DEFINED BY algorithm OPTIONAL  }
+
+  // If parameters is NULL (or empty), signatureAlgorithm won't be a container
+  // under this implementation. Just get its displayValue.
+  if (!signatureAlgorithm.isValidContainer) {
+    return signatureAlgorithm.displayValue;
+  }
+  let oid = signatureAlgorithm.ASN1Objects.queryElementAt(0, Ci.nsIASN1Object);
+  return oid.displayValue;
+}
+
+function processCertChain(chain) {
+  let output = [];
+  let enumerator = chain.getEnumerator();
+  while (enumerator.hasMoreElements()) {
+    let cert = enumerator.getNext().QueryInterface(Ci.nsIX509Cert);
+    output.push({
+      sha256Fingerprint: cert.sha256Fingerprint.replace(/:/g, "").toLowerCase(),
+      isBuiltInRoot: cert.isBuiltInRoot,
+      signatureAlgorithm: delocalizeAlgorithm(getSignatureAlgorithm(cert)),
+    });
+  }
+  return output;
 }
 
-/* If this function returns a non-empty string, it
- * means that this particular user should be temporarily
- * disqualified due to some particular reason.
- * If a user shouldn't be disqualified, then an empty
- * string must be returned.
- */
-function getTemporaryDisqualification() {
+class CertificateVerificationResult {
+  constructor(resolve) {
+    this.resolve = resolve;
+  }
+
+  verifyCertFinished(aPRErrorCode, aVerifiedChain, aEVStatus) {
+    let result = { errorCode: aPRErrorCode, error: "", chain: [] };
+    if (aPRErrorCode == 0) {
+      result.chain = processCertChain(aVerifiedChain);
+    } else {
+      result.error = "certificate reverification";
+    }
+    this.resolve(result);
+  }
+}
+
+function makeRequest() {
+  return new Promise((resolve) => {
+    let hostname = "telemetry.mozilla.org";
+    let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+                .createInstance(Ci.nsIXMLHttpRequest);
+    req.open("GET", "https://" + hostname);
+    req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+    req.timeout = 30000;
+    req.addEventListener("error", (evt) => {
+      // If we can't connect to telemetry.mozilla.org, then how did we even
+      // download the experiment? In any case, we may still be able to get some
+      // information.
+      let result = { error: "connection error" };
+      if (evt.target.channel && evt.target.channel.securityInfo) {
+        let securityInfo = evt.target.channel.securityInfo
+                             .QueryInterface(Ci.nsITransportSecurityInfo);
+        if (securityInfo) {
+          result.errorCode = securityInfo.errorCode;
+        }
+        if (securityInfo && securityInfo.failedCertChain) {
+          result.chain = processCertChain(securityInfo.failedCertChain);
+        }
+      }
+      resolve(result);
+    });
+    req.addEventListener("timeout", (evt) => {
+      resolve({ error: "timeout" });
+    });
+    req.addEventListener("load", (evt) => {
+      let securityInfo = evt.target.channel.securityInfo
+                           .QueryInterface(Ci.nsITransportSecurityInfo);
+      if (securityInfo.securityState &
+          Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN) {
+        resolve({ error: "user override" });
+        return;
+      }
+      let sslStatus = securityInfo.QueryInterface(Ci.nsISSLStatusProvider)
+                        .SSLStatus;
+      let certdb = Cc["@mozilla.org/security/x509certdb;1"]
+                     .getService(Ci.nsIX509CertDB);
+      let result = new CertificateVerificationResult(resolve);
+      // Unfortunately, we don't have direct access to the verified certificate
+      // chain as built by the AuthCertificate hook, so we have to re-build it
+      // here. In theory we are likely to get the same result.
+      certdb.asyncVerifyCertAtTime(sslStatus.serverCert,
+                                   2, // certificateUsageSSLServer
+                                   0, // flags
+                                   hostname,
+                                   Date.now() / 1000,
+                                   result);
+    });
+    req.send();
+  });
+}
+
+// As best we know, it is safe to disable SHA1 if the connection was successful
+// and either the connection was MITM'd by a root not in the public PKI or the
+// chain is part of the public PKI and is the one served by the real
+// telemetry.mozilla.org.
+// This will return a short string description of why it might not be safe to
+// disable SHA1 or an empty string if it is safe to disable SHA1.
+function reasonToNotDisableSHA1(result) {
+  if (!("errorCode" in result) || result.errorCode != 0) {
+    return "connection error";
+  }
+  if (!("chain" in result)) {
+    return "code error";
+  }
+  if (!result.chain[result.chain.length - 1].isBuiltInRoot) {
+    return "";
+  }
+  if (result.chain.length != 3) {
+    return "MITM";
+  }
+  const kEndEntityFingerprint = "197feaf3faa0f0ad637a89c97cb91336bfc114b6b3018203cbd9c3d10c7fa86c";
+  const kIntermediateFingerprint = "154c433c491929c5ef686e838e323664a00e6a0d822ccc958fb4dab03e49a08f";
+  const kRootFingerprint = "4348a0e9444c78cb265e058d5e8944b4d84f9662bd26db257f8934a443c70161";
+  if (result.chain[0].sha256Fingerprint != kEndEntityFingerprint ||
+      result.chain[1].sha256Fingerprint != kIntermediateFingerprint ||
+      result.chain[2].sha256Fingerprint != kRootFingerprint) {
+    return "MITM";
+  }
   return "";
 }
copy from browser/extensions/e10srollout/install.rdf.in
copy to browser/extensions/disableSHA1rollout/install.rdf.in
--- a/browser/extensions/e10srollout/install.rdf.in
+++ b/browser/extensions/disableSHA1rollout/install.rdf.in
@@ -4,29 +4,29 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 #filter substitution
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
      xmlns:em="http://www.mozilla.org/2004/em-rdf#">
 
   <Description about="urn:mozilla:install-manifest">
-    <em:id>e10srollout@mozilla.org</em:id>
-    <em:version>1.7</em:version>
+    <em:id>disableSHA1rollout@mozilla.org</em:id>
+    <em:version>1.0</em:version>
     <em:type>2</em:type>
     <em:bootstrap>true</em:bootstrap>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
 
     <!-- Target Application this theme can install into,
         with minimum and maximum supported versions. -->
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
         <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
       </Description>
     </em:targetApplication>
 
     <!-- Front End MetaData -->
-    <em:name>Multi-process staged rollout</em:name>
-    <em:description>Staged rollout of Firefox multi-process feature.</em:description>
+    <em:name>SHA-1 deprecation staged rollout</em:name>
+    <em:description>Staged rollout deprecating SHA-1 in certificate signatures.</em:description>
   </Description>
 </RDF>
copy from browser/extensions/e10srollout/moz.build
copy to browser/extensions/disableSHA1rollout/moz.build
--- a/browser/extensions/e10srollout/moz.build
+++ b/browser/extensions/disableSHA1rollout/moz.build
@@ -2,15 +2,15 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
 DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
 
-FINAL_TARGET_FILES.features['e10srollout@mozilla.org'] += [
+FINAL_TARGET_FILES.features['disableSHA1rollout@mozilla.org'] += [
   'bootstrap.js'
 ]
 
-FINAL_TARGET_PP_FILES.features['e10srollout@mozilla.org'] += [
+FINAL_TARGET_PP_FILES.features['disableSHA1rollout@mozilla.org'] += [
   'install.rdf.in'
 ]
--- a/browser/extensions/moz.build
+++ b/browser/extensions/moz.build
@@ -1,16 +1,17 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
     'aushelper',
+    'disableSHA1rollout',
     'e10srollout',
     'pdfjs',
     'pocket',
     'webcompat',
     'shield-recipe-client',
 ]
 
 # Only include the following system add-ons if building Aurora or Nightly