зеркало из https://github.com/mozilla/gecko-dev.git
283 строки
10 KiB
JavaScript
283 строки
10 KiB
JavaScript
/* 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";
|
|
|
|
// How to run this file:
|
|
// 1. [obtain CNNIC-issued certificates to be whitelisted]
|
|
// 2. [obtain firefox source code]
|
|
// 3. [build/obtain firefox binaries]
|
|
// 4. run `[path to]/run-mozilla.sh [path to]/xpcshell makeCNNICHashes.js \
|
|
// [path to]/intermediatesFile
|
|
// [path to]/certlist'
|
|
// Where |intermediatesFile| is a file containing PEM encoded intermediate
|
|
// certificates that the certificates in |certlist| may be issued by.
|
|
// where certlist is a file containing a list of paths to certificates to
|
|
// be included in the whitelist
|
|
|
|
var Cc = Components.classes;
|
|
var Ci = Components.interfaces;
|
|
var Cu = Components.utils;
|
|
|
|
var gCertDB = Cc["@mozilla.org/security/x509certdb;1"]
|
|
.getService(Ci.nsIX509CertDB);
|
|
|
|
var { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
|
|
var { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
|
|
|
|
const HEADER = "// This Source Code Form is subject to the terms of the Mozilla Public\n" +
|
|
"// License, v. 2.0. If a copy of the MPL was not distributed with this\n" +
|
|
"// file, You can obtain one at http://mozilla.org/MPL/2.0/.\n" +
|
|
"//\n" +
|
|
"//***************************************************************************\n" +
|
|
"// This file was automatically generated by makeCNNICHashes.js. It shouldn't\n" +
|
|
"// need to be manually edited.\n" +
|
|
"//***************************************************************************\n" +
|
|
"\n";
|
|
|
|
const PREAMBLE = "#define CNNIC_WHITELIST_HASH_LEN 32\n\n" +
|
|
"struct WhitelistedCNNICHash {\n" +
|
|
" const uint8_t hash[CNNIC_WHITELIST_HASH_LEN];\n" +
|
|
"};\n\n" +
|
|
"static const struct WhitelistedCNNICHash WhitelistedCNNICHashes[] = {\n";
|
|
|
|
const POSTAMBLE = "};\n";
|
|
|
|
function writeString(fos, string) {
|
|
fos.write(string, string.length);
|
|
}
|
|
|
|
// fingerprint is in the form "00:11:22:..."
|
|
function hexSlice(fingerprint, start, end) {
|
|
let hexBytes = fingerprint.split(":");
|
|
let ret = "";
|
|
for (let i = start; i < end; i++) {
|
|
let hex = hexBytes[i];
|
|
ret += "0x" + hex;
|
|
if (i < end - 1) {
|
|
ret += ", ";
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// Write the C++ header file
|
|
function writeHashes(certs, lastValidTime, fos) {
|
|
writeString(fos, HEADER);
|
|
writeString(fos, `// This file may be removed after ${new Date(lastValidTime)}\n\n`);
|
|
writeString(fos, PREAMBLE);
|
|
|
|
certs.forEach(function(cert) {
|
|
writeString(fos, " {\n");
|
|
writeString(fos, " { " + hexSlice(cert.sha256Fingerprint, 0, 16) + ",\n");
|
|
writeString(fos, " " + hexSlice(cert.sha256Fingerprint, 16, 32) + " },\n");
|
|
|
|
writeString(fos, " },\n");
|
|
});
|
|
writeString(fos, POSTAMBLE);
|
|
}
|
|
|
|
function readFileContents(file) {
|
|
let fstream = Cc["@mozilla.org/network/file-input-stream;1"]
|
|
.createInstance(Ci.nsIFileInputStream);
|
|
fstream.init(file, -1, 0, 0);
|
|
let data = NetUtil.readInputStreamToString(fstream, fstream.available());
|
|
fstream.close();
|
|
return data;
|
|
}
|
|
|
|
function relativePathToFile(path) {
|
|
let currentDirectory = Cc["@mozilla.org/file/directory_service;1"]
|
|
.getService(Ci.nsIProperties)
|
|
.get("CurWorkD", Ci.nsILocalFile);
|
|
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
|
|
file.initWithPath(currentDirectory.path + "/" + path);
|
|
return file;
|
|
}
|
|
|
|
function pathToFile(path) {
|
|
let file = relativePathToFile(path);
|
|
if (!file.exists()) {
|
|
// Fall back to trying absolute path
|
|
file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
|
|
file.initWithPath(path);
|
|
}
|
|
return file;
|
|
}
|
|
|
|
// punt on dealing with leap-years
|
|
const sixYearsInMilliseconds = 6 * 366 * 24 * 60 * 60 * 1000;
|
|
|
|
function loadCertificates(certFile, currentWhitelist) {
|
|
let nowInMilliseconds = (new Date()).getTime();
|
|
// months are 0-indexed, so April is month 3 :(
|
|
let april1InMilliseconds = (new Date(2015, 3, 1)).getTime();
|
|
let latestNotAfter = nowInMilliseconds;
|
|
let certs = [];
|
|
let certMap = {};
|
|
let invalidCerts = [];
|
|
let paths = readFileContents(certFile).split("\n");
|
|
for (let path of paths) {
|
|
if (!path) {
|
|
continue;
|
|
}
|
|
let certData = readFileContents(pathToFile(path));
|
|
let cert = null;
|
|
try {
|
|
cert = gCertDB.constructX509FromBase64(certData);
|
|
} catch (e) {}
|
|
if (!cert) {
|
|
cert = gCertDB.constructX509(certData, certData.length);
|
|
}
|
|
// Don't add multiple copies of any particular certificate.
|
|
if (cert.sha256Fingerprint in certMap) {
|
|
continue;
|
|
}
|
|
certMap[cert.sha256Fingerprint] = true;
|
|
// If we can't verify the certificate, don't include it. Unfortunately, if
|
|
// a CNNIC-issued certificate wasn't previously on the whitelist but it
|
|
// otherwise verifies successfully, verifyCertNow will return
|
|
// SEC_ERROR_REVOKED_CERTIFICATE, so we count that as verifying
|
|
// successfully. If the certificate is later revoked by CNNIC, the user
|
|
// will see that when they attempt to connect to a site using it and we do
|
|
// normal revocation checking.
|
|
let errorCode = gCertDB.verifyCertNow(cert, 2 /* SSL Server */,
|
|
Ci.nsIX509CertDB.LOCAL_ONLY, null,
|
|
{}, {});
|
|
if (errorCode != 0 &&
|
|
errorCode != -8180 /* SEC_ERROR_REVOKED_CERTIFICATE */) {
|
|
continue;
|
|
}
|
|
let durationMilliseconds = (cert.validity.notAfter - cert.validity.notBefore) / 1000;
|
|
let notBeforeMilliseconds = cert.validity.notBefore / 1000;
|
|
let notAfterMilliseconds = cert.validity.notAfter / 1000;
|
|
// Only consider certificates that were issued before 1 April 2015, haven't
|
|
// expired, and have a validity period shorter than 6 years (there is a
|
|
// delegated OCSP responder certificate with a validity period of 6 years
|
|
// that should be on the whitelist).
|
|
// Also only consider certificates that were already on the whitelist.
|
|
if (notBeforeMilliseconds < april1InMilliseconds &&
|
|
notAfterMilliseconds > nowInMilliseconds &&
|
|
durationMilliseconds < sixYearsInMilliseconds &&
|
|
currentWhitelist[cert.sha256Fingerprint]) {
|
|
certs.push(cert);
|
|
if (notAfterMilliseconds > latestNotAfter) {
|
|
latestNotAfter = notAfterMilliseconds;
|
|
}
|
|
}
|
|
if (durationMilliseconds >= sixYearsInMilliseconds) {
|
|
invalidCerts.push(cert);
|
|
}
|
|
}
|
|
return { certs: certs,
|
|
lastValidTime: latestNotAfter,
|
|
invalidCerts: invalidCerts };
|
|
}
|
|
|
|
// Expects something like "00:11:22:...", returns a string of bytes.
|
|
function hexToBinaryString(hexString) {
|
|
let hexBytes = hexString.split(":");
|
|
let result = "";
|
|
for (let hexByte of hexBytes) {
|
|
result += String.fromCharCode(parseInt(hexByte, 16));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function compareCertificatesByHash(certA, certB) {
|
|
let aBin = hexToBinaryString(certA.sha256Fingerprint);
|
|
let bBin = hexToBinaryString(certB.sha256Fingerprint);
|
|
|
|
if (aBin < bBin) {
|
|
return -1;
|
|
}
|
|
if (aBin > bBin) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function certToPEM(cert) {
|
|
let der = cert.getRawDER({});
|
|
let derString = "";
|
|
for (let i = 0; i < der.length; i++) {
|
|
derString += String.fromCharCode(der[i]);
|
|
}
|
|
let base64Lines = btoa(derString).replace(/(.{64})/g, "$1\n");
|
|
let output = "-----BEGIN CERTIFICATE-----\n";
|
|
for (let line of base64Lines.split("\n")) {
|
|
if (line.length > 0) {
|
|
output += line + "\n";
|
|
}
|
|
}
|
|
output += "-----END CERTIFICATE-----";
|
|
return output;
|
|
}
|
|
|
|
function loadIntermediates(intermediatesFile) {
|
|
let pem = readFileContents(intermediatesFile);
|
|
let intermediates = [];
|
|
let currentPEM = "";
|
|
for (let line of pem.split("\r\n")) {
|
|
if (line == "-----END CERTIFICATE-----") {
|
|
if (currentPEM) {
|
|
intermediates.push(gCertDB.constructX509FromBase64(currentPEM));
|
|
}
|
|
currentPEM = "";
|
|
continue;
|
|
}
|
|
if (line != "-----BEGIN CERTIFICATE-----") {
|
|
currentPEM += line;
|
|
}
|
|
}
|
|
return intermediates;
|
|
}
|
|
|
|
function readCurrentWhitelist(currentWhitelistFile) {
|
|
let contents = readFileContents(currentWhitelistFile).replace(/[\r\n ]/g, "");
|
|
let split = contents.split(/((?:0x[0-9A-F][0-9A-F],){31}0x[0-9A-F][0-9A-F])/);
|
|
// The hashes will be every odd-indexed element of the array.
|
|
let currentWhitelist = {};
|
|
for (let i = 1; i < split.length && i < split.length - 1; i += 2) {
|
|
let hash = split[i].replace(/0x/g, "").replace(/,/g, ":");
|
|
currentWhitelist[hash] = true;
|
|
}
|
|
return currentWhitelist;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
if (arguments.length != 3) {
|
|
throw new Error("Usage: makeCNNICHashes.js <PEM intermediates file> " +
|
|
"<path to list of certificates> <path to current whitelist file>");
|
|
}
|
|
|
|
Services.prefs.setIntPref("security.OCSP.enabled", 0);
|
|
var intermediatesFile = pathToFile(arguments[0]);
|
|
var intermediates = loadIntermediates(intermediatesFile);
|
|
var certFile = pathToFile(arguments[1]);
|
|
var currentWhitelistFile = pathToFile(arguments[2]);
|
|
var currentWhitelist = readCurrentWhitelist(currentWhitelistFile);
|
|
var { certs, lastValidTime, invalidCerts } = loadCertificates(certFile, currentWhitelist);
|
|
|
|
dump("The following certificates were not included due to overlong validity periods:\n");
|
|
for (let cert of invalidCerts) {
|
|
dump(certToPEM(cert) + "\n");
|
|
}
|
|
|
|
// Sort the key hashes to allow for binary search.
|
|
certs.sort(compareCertificatesByHash);
|
|
|
|
// Write the output file.
|
|
var outFile = relativePathToFile("CNNICHashWhitelist.inc");
|
|
if (!outFile.exists()) {
|
|
outFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
|
|
}
|
|
var outStream = Cc["@mozilla.org/network/file-output-stream;1"]
|
|
.createInstance(Ci.nsIFileOutputStream);
|
|
outStream.init(outFile, -1, 0, 0);
|
|
writeHashes(certs, lastValidTime, outStream);
|
|
outStream.close();
|