bug 1328718 - implement system add-on to facilitate rollout of disabling SHA-1 r?jcj,felipe draft
authorDavid Keeler <dkeeler@mozilla.com>
Thu, 12 Jan 2017 11:54:22 -0800
changeset 463323 3833d3b0e8bed78c9bbe973e0d385cf3401bf9b0
parent 458610 e68cbc3b5b3d3fba4fe3e17e234713020f44e4a0
child 542636 3baec16a577e71ca5e6abca7a4b31fb1eead923e
push id42020
push userdkeeler@mozilla.com
push dateWed, 18 Jan 2017 20:59:23 +0000
reviewersjcj, felipe
bugs1328718
milestone53.0a1
bug 1328718 - implement system add-on to facilitate rollout of disabling SHA-1 r?jcj,felipe 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,100 @@
+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. "unsupportedChannel" if the user's update channel is not beta
+  2. "notSafeToDisableSHA1" if the user is behind a MITM proxy using a
+     publicly-trusted root
+  3. "optedOut" if the user already set the SHA-1 policy to always allow or
+     always forbid
+  4. "optedIn" if the user already set the SHA-1 policy to only allow for
+     non-built-in roots
+  5. "test" if the user is in the test cohort (and SHA-1 will be disabled)
+  6. "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,304 @@
  * 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" : "50allmpc", // Any WebExtension or addon with mpc = true
+  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();
+  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);
+  if (currentPrefValue == SHA1_MODE_CURRENT_DEFAULT) {
+    Preferences.set(PREF_SHA1_POLICY_RESET_BY_USER, true);
   }
 }
 
+function defineCohort(result) {
+  let updateChannel = UpdateUtils.getUpdateChannel(false);
+  let supportedUpdateChannel = updateChannel in TEST_THRESHOLD;
+  let userOptedOut = optedOut();
+  let userOptedIn = optedIn();
+  let shouldNotDisableSHA1Because = reasonToNotDisableSHA1(result);
+  let safeToDisableSHA1 = shouldNotDisableSHA1Because.length == 0;
+  let testGroup = supportedUpdateChannel
+                ? getUserSample() < TEST_THRESHOLD[updateChannel]
+                : false;
+
+  let cohortName;
+  if (!supportedUpdateChannel) {
+    cohortName = "unsupportedChannel";
+  } else 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.6</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