mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
bug 1151512 - only allow whitelisted certificates to be issued by CNNIC root certificates r=jcj r=rbarnes
This commit is contained in:
parent
107362ed54
commit
5f4152c364
6147
security/certverifier/CNNICHashWhitelist.inc
Normal file
6147
security/certverifier/CNNICHashWhitelist.inc
Normal file
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,8 @@
|
|||||||
#include "ScopedNSSTypes.h"
|
#include "ScopedNSSTypes.h"
|
||||||
#include "secerr.h"
|
#include "secerr.h"
|
||||||
|
|
||||||
|
#include "CNNICHashWhitelist.inc"
|
||||||
|
|
||||||
using namespace mozilla;
|
using namespace mozilla;
|
||||||
using namespace mozilla::pkix;
|
using namespace mozilla::pkix;
|
||||||
|
|
||||||
@ -679,6 +681,40 @@ NSSCertDBTrustDomain::VerifyAndMaybeCacheEncodedOCSPResponse(
|
|||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const uint8_t CNNIC_ROOT_CA_SUBJECT_DATA[] =
|
||||||
|
"\x30\x32\x31\x0B\x30\x09\x06\x03\x55\x04\x06\x13\x02\x43\x4E\x31\x0E\x30"
|
||||||
|
"\x0C\x06\x03\x55\x04\x0A\x13\x05\x43\x4E\x4E\x49\x43\x31\x13\x30\x11\x06"
|
||||||
|
"\x03\x55\x04\x03\x13\x0A\x43\x4E\x4E\x49\x43\x20\x52\x4F\x4F\x54";
|
||||||
|
|
||||||
|
static const uint8_t CNNIC_EV_ROOT_CA_SUBJECT_DATA[] =
|
||||||
|
"\x30\x81\x8A\x31\x0B\x30\x09\x06\x03\x55\x04\x06\x13\x02\x43\x4E\x31\x32"
|
||||||
|
"\x30\x30\x06\x03\x55\x04\x0A\x0C\x29\x43\x68\x69\x6E\x61\x20\x49\x6E\x74"
|
||||||
|
"\x65\x72\x6E\x65\x74\x20\x4E\x65\x74\x77\x6F\x72\x6B\x20\x49\x6E\x66\x6F"
|
||||||
|
"\x72\x6D\x61\x74\x69\x6F\x6E\x20\x43\x65\x6E\x74\x65\x72\x31\x47\x30\x45"
|
||||||
|
"\x06\x03\x55\x04\x03\x0C\x3E\x43\x68\x69\x6E\x61\x20\x49\x6E\x74\x65\x72"
|
||||||
|
"\x6E\x65\x74\x20\x4E\x65\x74\x77\x6F\x72\x6B\x20\x49\x6E\x66\x6F\x72\x6D"
|
||||||
|
"\x61\x74\x69\x6F\x6E\x20\x43\x65\x6E\x74\x65\x72\x20\x45\x56\x20\x43\x65"
|
||||||
|
"\x72\x74\x69\x66\x69\x63\x61\x74\x65\x73\x20\x52\x6F\x6F\x74";
|
||||||
|
|
||||||
|
class WhitelistedCNNICHashBinarySearchComparator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit WhitelistedCNNICHashBinarySearchComparator(const uint8_t* aTarget,
|
||||||
|
size_t aTargetLength)
|
||||||
|
: mTarget(aTarget)
|
||||||
|
{
|
||||||
|
MOZ_ASSERT(aTargetLength == CNNIC_WHITELIST_HASH_LEN,
|
||||||
|
"Hashes should be of the same length.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int operator()(const WhitelistedCNNICHash val) const {
|
||||||
|
return memcmp(mTarget, val.hash, CNNIC_WHITELIST_HASH_LEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const uint8_t* mTarget;
|
||||||
|
};
|
||||||
|
|
||||||
Result
|
Result
|
||||||
NSSCertDBTrustDomain::IsChainValid(const DERArray& certArray, Time time)
|
NSSCertDBTrustDomain::IsChainValid(const DERArray& certArray, Time time)
|
||||||
{
|
{
|
||||||
@ -692,6 +728,49 @@ NSSCertDBTrustDomain::IsChainValid(const DERArray& certArray, Time time)
|
|||||||
return MapPRErrorCodeToResult(PR_GetError());
|
return MapPRErrorCodeToResult(PR_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the certificate appears to have been issued by a CNNIC root, only allow
|
||||||
|
// it if it is on the whitelist.
|
||||||
|
CERTCertListNode* rootNode = CERT_LIST_TAIL(certList);
|
||||||
|
if (!rootNode) {
|
||||||
|
return Result::FATAL_ERROR_LIBRARY_FAILURE;
|
||||||
|
}
|
||||||
|
CERTCertificate* root = rootNode->cert;
|
||||||
|
if (!root) {
|
||||||
|
return Result::FATAL_ERROR_LIBRARY_FAILURE;
|
||||||
|
}
|
||||||
|
if ((root->derSubject.len == sizeof(CNNIC_ROOT_CA_SUBJECT_DATA) - 1 &&
|
||||||
|
memcmp(root->derSubject.data, CNNIC_ROOT_CA_SUBJECT_DATA,
|
||||||
|
root->derSubject.len) == 0) ||
|
||||||
|
(root->derSubject.len == sizeof(CNNIC_EV_ROOT_CA_SUBJECT_DATA) - 1 &&
|
||||||
|
memcmp(root->derSubject.data, CNNIC_EV_ROOT_CA_SUBJECT_DATA,
|
||||||
|
root->derSubject.len) == 0)) {
|
||||||
|
CERTCertListNode* certNode = CERT_LIST_HEAD(certList);
|
||||||
|
if (!certNode) {
|
||||||
|
return Result::FATAL_ERROR_LIBRARY_FAILURE;
|
||||||
|
}
|
||||||
|
CERTCertificate* cert = certNode->cert;
|
||||||
|
if (!cert) {
|
||||||
|
return Result::FATAL_ERROR_LIBRARY_FAILURE;
|
||||||
|
}
|
||||||
|
Digest digest;
|
||||||
|
nsresult nsrv = digest.DigestBuf(SEC_OID_SHA256, cert->derCert.data,
|
||||||
|
cert->derCert.len);
|
||||||
|
if (NS_FAILED(nsrv)) {
|
||||||
|
return Result::FATAL_ERROR_LIBRARY_FAILURE;
|
||||||
|
}
|
||||||
|
const uint8_t* certHash(
|
||||||
|
reinterpret_cast<const uint8_t*>(digest.get().data));
|
||||||
|
size_t certHashLen = digest.get().len;
|
||||||
|
size_t unused;
|
||||||
|
if (!mozilla::BinarySearchIf(WhitelistedCNNICHashes, 0,
|
||||||
|
ArrayLength(WhitelistedCNNICHashes),
|
||||||
|
WhitelistedCNNICHashBinarySearchComparator(
|
||||||
|
certHash, certHashLen),
|
||||||
|
&unused)) {
|
||||||
|
return Result::ERROR_REVOKED_CERTIFICATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Result result = CertListContainsExpectedKeys(certList, mHostname, time,
|
Result result = CertListContainsExpectedKeys(certList, mHostname, time,
|
||||||
mPinningMode);
|
mPinningMode);
|
||||||
if (result != Success) {
|
if (result != Success) {
|
||||||
|
214
security/manager/tools/makeCNNICHashes.js
Normal file
214
security/manager/tools/makeCNNICHashes.js
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
// 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]/certlist'
|
||||||
|
// where certlist is a file containing a list of paths to certificates to
|
||||||
|
// be included in the whitelist
|
||||||
|
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cu = Components.utils;
|
||||||
|
|
||||||
|
let gCertDB = Cc["@mozilla.org/security/x509certdb;1"]
|
||||||
|
.getService(Ci.nsIX509CertDB);
|
||||||
|
|
||||||
|
let { NetUtil } = Cu.import("resource://gre/modules/NetUtil.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) {
|
||||||
|
let nowInMilliseconds = (new Date()).getTime();
|
||||||
|
let latestNotAfter = nowInMilliseconds;
|
||||||
|
let certs = [];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
let durationMilliseconds = (cert.validity.notAfter - cert.validity.notBefore) / 1000;
|
||||||
|
let notAfterMilliseconds = cert.validity.notAfter / 1000;
|
||||||
|
// Only consider certificates that 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).
|
||||||
|
if (notAfterMilliseconds > nowInMilliseconds &&
|
||||||
|
durationMilliseconds < sixYearsInMilliseconds) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
if (arguments.length != 1) {
|
||||||
|
throw "Usage: makeCNNICHashes.js <path to list of certificates>";
|
||||||
|
}
|
||||||
|
|
||||||
|
let certFile = pathToFile(arguments[0]);
|
||||||
|
let { certs, lastValidTime, invalidCerts } = loadCertificates(certFile);
|
||||||
|
|
||||||
|
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.
|
||||||
|
let outFile = relativePathToFile("CNNICHashWhitelist.inc");
|
||||||
|
if (!outFile.exists()) {
|
||||||
|
outFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0644);
|
||||||
|
}
|
||||||
|
let outStream = Cc["@mozilla.org/network/file-output-stream;1"]
|
||||||
|
.createInstance(Ci.nsIFileOutputStream);
|
||||||
|
outStream.init(outFile, -1, 0, 0);
|
||||||
|
writeHashes(certs, lastValidTime, outStream);
|
||||||
|
outStream.close();
|
Loading…
Reference in New Issue
Block a user