bug 1328718 - implement system add-on to facilitate rollout of disabling SHA-1 r=Felipe,jcj data-review=bsmedberg

MozReview-Commit-ID: L5Q5VGr6UPU

--HG--
rename : browser/extensions/e10srollout/bootstrap.js => browser/extensions/disableSHA1rollout/bootstrap.js
rename : browser/extensions/e10srollout/install.rdf.in => browser/extensions/disableSHA1rollout/install.rdf.in
rename : browser/extensions/e10srollout/moz.build => browser/extensions/disableSHA1rollout/moz.build
extra : rebase_source : 27d9691ac1a5cc14d7c2fafa5fc35d0139d297a7
This commit is contained in:
David Keeler 2017-01-12 11:54:22 -08:00
Родитель f55394ad68
Коммит 68de1e7bae
5 изменённых файлов: 454 добавлений и 0 удалений

Просмотреть файл

@ -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.

306
browser/extensions/disableSHA1rollout/bootstrap.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,306 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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/. */
"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");
// Percentage of the population to attempt to disable SHA-1 for, by channel.
const TEST_THRESHOLD = {
beta: 0.1, // 10%
};
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() {
Preferences.observe(PREF_SHA1_POLICY, policyPreferenceChanged);
}
function install() {
let updateChannel = UpdateUtils.getUpdateChannel(false);
if (updateChannel in TEST_THRESHOLD) {
makeRequest().then(defineCohort).catch((e) => console.error(e));
}
}
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;
}
value = Math.random();
Preferences.set(PREF_COHORT_SAMPLE, value.toString().substr(0, 8));
return value;
}
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";
}
}
result.didNotDisableSHA1Because = didNotDisableSHA1Because;
return TelemetryController.submitExternalPing("disableSHA1rollout", result,
{});
}
function optedIn() {
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 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;
}
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 "";
}

Просмотреть файл

@ -0,0 +1,32 @@
<?xml version="1.0"?>
<!-- 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/. -->
#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>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>SHA-1 deprecation staged rollout</em:name>
<em:description>Staged rollout deprecating SHA-1 in certificate signatures.</em:description>
</Description>
</RDF>

Просмотреть файл

@ -0,0 +1,16 @@
# -*- 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/.
DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
FINAL_TARGET_FILES.features['disableSHA1rollout@mozilla.org'] += [
'bootstrap.js'
]
FINAL_TARGET_PP_FILES.features['disableSHA1rollout@mozilla.org'] += [
'install.rdf.in'
]

Просмотреть файл

@ -6,6 +6,7 @@
DIRS += [
'aushelper',
'disableSHA1rollout',
'e10srollout',
'pdfjs',
'pocket',