diff options
Diffstat (limited to 'security/manager/tools/makeCNNICHashes.js')
-rw-r--r-- | security/manager/tools/makeCNNICHashes.js | 282 |
1 files changed, 282 insertions, 0 deletions
diff --git a/security/manager/tools/makeCNNICHashes.js b/security/manager/tools/makeCNNICHashes.js new file mode 100644 index 000000000..1b558949e --- /dev/null +++ b/security/manager/tools/makeCNNICHashes.js @@ -0,0 +1,282 @@ +/* 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(); |