/* 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 " + " "); } 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();