bug 1240173 - improve nsIX509Cert.dbKey r=Cykesiopka

This commit is contained in:
David Keeler 2016-01-15 14:33:56 -08:00
parent 83f1f25714
commit da78a4c1e5
9 changed files with 263 additions and 141 deletions

View File

@ -398,19 +398,10 @@ nsCertOverrideService::RememberValidityOverride(const nsACString& aHostName,
if (NS_FAILED(rv))
return rv;
char *dbkey = nullptr;
rv = aCert->GetDbKey(&dbkey);
if (NS_FAILED(rv) || !dbkey)
nsAutoCString dbkey;
rv = aCert->GetDbKey(dbkey);
if (NS_FAILED(rv)) {
return rv;
// change \n and \r to spaces in the possibly multi-line-base64-encoded key
for (char *dbkey_walk = dbkey;
*dbkey_walk;
++dbkey_walk) {
char c = *dbkey_walk;
if (c == '\r' || c == '\n') {
*dbkey_walk = ' ';
}
}
{
@ -421,11 +412,10 @@ nsCertOverrideService::RememberValidityOverride(const nsACString& aHostName,
aTemporary,
mDottedOidForStoringNewHashes, fpStr,
(nsCertOverride::OverrideBits)aOverrideBits,
nsDependentCString(dbkey));
dbkey);
Write();
}
PR_Free(dbkey);
return NS_OK;
}
@ -553,6 +543,8 @@ nsCertOverrideService::AddEntryToList(const nsACString &aHostName, int32_t aPort
settings.mFingerprint = fingerprint;
settings.mOverrideBits = ob;
settings.mDBKey = dbKey;
// remove whitespace from stored dbKey for backwards compatibility
settings.mDBKey.StripWhitespace();
settings.mCert = aCert;
}
@ -599,51 +591,14 @@ nsCertOverrideService::CountPermanentOverrideTelemetry()
}
static bool
matchesDBKey(nsIX509Cert *cert, const char *match_dbkey)
matchesDBKey(nsIX509Cert* cert, const nsCString& matchDbKey)
{
char *dbkey = nullptr;
nsresult rv = cert->GetDbKey(&dbkey);
if (NS_FAILED(rv) || !dbkey)
nsAutoCString dbKey;
nsresult rv = cert->GetDbKey(dbKey);
if (NS_FAILED(rv)) {
return false;
bool found_mismatch = false;
const char *key1 = dbkey;
const char *key2 = match_dbkey;
// skip over any whitespace when comparing
while (*key1 && *key2) {
char c1 = *key1;
char c2 = *key2;
switch (c1) {
case ' ':
case '\t':
case '\n':
case '\r':
++key1;
continue;
}
switch (c2) {
case ' ':
case '\t':
case '\n':
case '\r':
++key2;
continue;
}
if (c1 != c2) {
found_mismatch = true;
break;
}
++key1;
++key2;
}
PR_Free(dbkey);
return !found_mismatch;
return dbKey.Equals(matchDbKey);
}
NS_IMETHODIMP
@ -667,7 +622,7 @@ nsCertOverrideService::IsCertUsedForOverrides(nsIX509Cert *aCert,
still_ok = false;
}
if (still_ok && matchesDBKey(aCert, settings.mDBKey.get())) {
if (still_ok && matchesDBKey(aCert, settings.mDBKey)) {
nsAutoCString cert_fingerprint;
nsresult rv = NS_ERROR_UNEXPECTED;
if (settings.mFingerprintAlgOID.Equals(mDottedOidForStoringNewHashes)) {
@ -697,7 +652,7 @@ nsCertOverrideService::EnumerateCertOverrides(nsIX509Cert *aCert,
if (!aCert) {
aEnumerator(settings, aUserData);
} else {
if (matchesDBKey(aCert, settings.mDBKey.get())) {
if (matchesDBKey(aCert, settings.mDBKey)) {
nsAutoCString cert_fingerprint;
nsresult rv = NS_ERROR_UNEXPECTED;
if (settings.mFingerprintAlgOID.Equals(mDottedOidForStoringNewHashes)) {

View File

@ -102,29 +102,26 @@ nsClientAuthRememberService::RememberDecision(const nsACString & aHostName,
{
// aClientCert == nullptr means: remember that user does not want to use a cert
NS_ENSURE_ARG_POINTER(aServerCert);
if (aHostName.IsEmpty())
if (aHostName.IsEmpty()) {
return NS_ERROR_INVALID_ARG;
}
nsAutoCString fpStr;
nsresult rv = GetCertFingerprintByOidTag(aServerCert, SEC_OID_SHA256, fpStr);
if (NS_FAILED(rv))
if (NS_FAILED(rv)) {
return rv;
}
{
ReentrantMonitorAutoEnter lock(monitor);
if (aClientCert) {
RefPtr<nsNSSCertificate> pipCert(new nsNSSCertificate(aClientCert));
char *dbkey = nullptr;
rv = pipCert->GetDbKey(&dbkey);
if (NS_SUCCEEDED(rv) && dbkey) {
AddEntryToList(aHostName, fpStr,
nsDependentCString(dbkey));
nsAutoCString dbkey;
rv = pipCert->GetDbKey(dbkey);
if (NS_SUCCEEDED(rv)) {
AddEntryToList(aHostName, fpStr, dbkey);
}
if (dbkey) {
PORT_Free(dbkey);
}
}
else {
} else {
nsCString empty;
AddEntryToList(aHostName, fpStr, empty);
}

View File

@ -20,7 +20,7 @@ interface nsICertVerificationListener;
/**
* This represents a X.509 certificate.
*/
[scriptable, uuid(f8ed8364-ced9-4c6e-86ba-48af53c393e6)]
[scriptable, uuid(bdc3979a-5422-4cd5-8589-696b6e96ea83)]
interface nsIX509Cert : nsISupports {
/**
@ -130,7 +130,7 @@ interface nsIX509Cert : nsISupports {
/**
* A unique identifier of this certificate within the local storage.
*/
readonly attribute string dbKey;
readonly attribute ACString dbKey;
/**
* A human readable identifier to label this certificate.

View File

@ -50,6 +50,10 @@
#include "ssl.h"
#include "plbase64.h"
#ifdef XP_WIN
#include <winsock.h> // for htonl
#endif
using namespace mozilla;
using namespace mozilla::psm;
@ -486,32 +490,37 @@ nsNSSCertificate::FormatUIStrings(const nsAutoString& nickname,
}
NS_IMETHODIMP
nsNSSCertificate::GetDbKey(char** aDbKey)
nsNSSCertificate::GetDbKey(nsACString& aDbKey)
{
nsNSSShutDownPreventionLock locker;
if (isAlreadyShutDown())
if (isAlreadyShutDown()) {
return NS_ERROR_NOT_AVAILABLE;
}
SECItem key;
static_assert(sizeof(uint64_t) == 8, "type size sanity check");
static_assert(sizeof(uint32_t) == 4, "type size sanity check");
// The format of the key is the base64 encoding of the following:
// 4 bytes: {0, 0, 0, 0} (this was intended to be the module ID, but it was
// never implemented)
// 4 bytes: {0, 0, 0, 0} (this was intended to be the slot ID, but it was
// never implemented)
// 4 bytes: <serial number length in big-endian order>
// 4 bytes: <DER-encoded issuer distinguished name length in big-endian order>
// n bytes: <bytes of serial number>
// m bytes: <DER-encoded issuer distinguished name>
nsAutoCString buf;
const char leadingZeroes[] = {0, 0, 0, 0, 0, 0, 0, 0};
buf.Append(leadingZeroes, sizeof(leadingZeroes));
uint32_t serialNumberLen = htonl(mCert->serialNumber.len);
buf.Append(reinterpret_cast<const char*>(&serialNumberLen), sizeof(uint32_t));
uint32_t issuerLen = htonl(mCert->derIssuer.len);
buf.Append(reinterpret_cast<const char*>(&issuerLen), sizeof(uint32_t));
buf.Append(reinterpret_cast<const char*>(mCert->serialNumber.data),
mCert->serialNumber.len);
buf.Append(reinterpret_cast<const char*>(mCert->derIssuer.data),
mCert->derIssuer.len);
NS_ENSURE_ARG(aDbKey);
*aDbKey = nullptr;
key.len = NS_NSS_LONG*4+mCert->serialNumber.len+mCert->derIssuer.len;
key.data = (unsigned char*) moz_xmalloc(key.len);
if (!key.data)
return NS_ERROR_OUT_OF_MEMORY;
NS_NSS_PUT_LONG(0,key.data); // later put moduleID
NS_NSS_PUT_LONG(0,&key.data[NS_NSS_LONG]); // later put slotID
NS_NSS_PUT_LONG(mCert->serialNumber.len,&key.data[NS_NSS_LONG*2]);
NS_NSS_PUT_LONG(mCert->derIssuer.len,&key.data[NS_NSS_LONG*3]);
memcpy(&key.data[NS_NSS_LONG*4], mCert->serialNumber.data,
mCert->serialNumber.len);
memcpy(&key.data[NS_NSS_LONG*4+mCert->serialNumber.len],
mCert->derIssuer.data, mCert->derIssuer.len);
*aDbKey = NSSBase64_EncodeItem(nullptr, nullptr, 0, &key);
free(key.data); // SECItem is a 'c' type without a destructor
return (*aDbKey) ? NS_OK : NS_ERROR_FAILURE;
return Base64Encode(buf, aDbKey);
}
NS_IMETHODIMP

View File

@ -132,17 +132,6 @@ private:
void operator=(const nsNSSCertListEnumerator&) = delete;
};
#define NS_NSS_LONG 4
#define NS_NSS_GET_LONG(x) ((((unsigned long)((x)[0])) << 24) | \
(((unsigned long)((x)[1])) << 16) | \
(((unsigned long)((x)[2])) << 8) | \
((unsigned long)((x)[3])) )
#define NS_NSS_PUT_LONG(src,dest) (dest)[0] = (((src) >> 24) & 0xff); \
(dest)[1] = (((src) >> 16) & 0xff); \
(dest)[2] = (((src) >> 8) & 0xff); \
(dest)[3] = ((src) & 0xff);
#define NS_X509CERT_CID { /* 660a3226-915c-4ffb-bb20-8985a632df05 */ \
0x660a3226, \
0x915c, \

View File

@ -48,6 +48,10 @@
#include "ssl.h"
#include "plbase64.h"
#ifdef XP_WIN
#include <winsock.h> // for ntohl
#endif
using namespace mozilla;
using namespace mozilla::psm;
using mozilla::psm::SharedSSLState;
@ -137,42 +141,58 @@ nsNSSCertificateDB::FindCertByDBKey(const char *aDBkey, nsISupports *aToken,
return NS_ERROR_NOT_AVAILABLE;
}
SECItem keyItem = {siBuffer, nullptr, 0};
SECItem *dummy;
static_assert(sizeof(uint64_t) == 8, "type size sanity check");
static_assert(sizeof(uint32_t) == 4, "type size sanity check");
// (From nsNSSCertificate::GetDbKey)
// The format of the key is the base64 encoding of the following:
// 4 bytes: {0, 0, 0, 0} (this was intended to be the module ID, but it was
// never implemented)
// 4 bytes: {0, 0, 0, 0} (this was intended to be the slot ID, but it was
// never implemented)
// 4 bytes: <serial number length in big-endian order>
// 4 bytes: <DER-encoded issuer distinguished name length in big-endian order>
// n bytes: <bytes of serial number>
// m bytes: <DER-encoded issuer distinguished name>
nsAutoCString decoded;
nsAutoCString tmpDBKey(aDBkey);
// Filter out any whitespace for backwards compatibility.
tmpDBKey.StripWhitespace();
nsresult rv = Base64Decode(tmpDBKey, decoded);
if (NS_FAILED(rv)) {
return rv;
}
if (decoded.Length() < 16) {
return NS_ERROR_ILLEGAL_INPUT;
}
const char* reader = decoded.BeginReading();
uint64_t zeroes = *reinterpret_cast<const uint64_t*>(reader);
if (zeroes != 0) {
return NS_ERROR_ILLEGAL_INPUT;
}
reader += sizeof(uint64_t);
uint32_t serialNumberLen = ntohl(*reinterpret_cast<const uint32_t*>(reader));
reader += sizeof(uint32_t);
uint32_t issuerLen = ntohl(*reinterpret_cast<const uint32_t*>(reader));
reader += sizeof(uint32_t);
if (decoded.Length() != 16ULL + serialNumberLen + issuerLen) {
return NS_ERROR_ILLEGAL_INPUT;
}
CERTIssuerAndSN issuerSN;
//unsigned long moduleID,slotID;
issuerSN.serialNumber.len = serialNumberLen;
issuerSN.serialNumber.data = (unsigned char*)reader;
reader += serialNumberLen;
issuerSN.derIssuer.len = issuerLen;
issuerSN.derIssuer.data = (unsigned char*)reader;
reader += issuerLen;
MOZ_ASSERT(reader == decoded.EndReading());
dummy = NSSBase64_DecodeBuffer(nullptr, &keyItem, aDBkey,
(uint32_t)strlen(aDBkey));
if (!dummy || keyItem.len < NS_NSS_LONG*4) {
PR_FREEIF(keyItem.data);
return NS_ERROR_INVALID_ARG;
}
ScopedCERTCertificate cert;
// someday maybe we can speed up the search using the moduleID and slotID
// moduleID = NS_NSS_GET_LONG(keyItem.data);
// slotID = NS_NSS_GET_LONG(&keyItem.data[NS_NSS_LONG]);
// build the issuer/SN structure
issuerSN.serialNumber.len = NS_NSS_GET_LONG(&keyItem.data[NS_NSS_LONG*2]);
issuerSN.derIssuer.len = NS_NSS_GET_LONG(&keyItem.data[NS_NSS_LONG*3]);
if (issuerSN.serialNumber.len == 0 || issuerSN.derIssuer.len == 0
|| issuerSN.serialNumber.len + issuerSN.derIssuer.len
!= keyItem.len - NS_NSS_LONG*4) {
PR_FREEIF(keyItem.data);
return NS_ERROR_INVALID_ARG;
}
issuerSN.serialNumber.data= &keyItem.data[NS_NSS_LONG*4];
issuerSN.derIssuer.data= &keyItem.data[NS_NSS_LONG*4+
issuerSN.serialNumber.len];
cert = CERT_FindCertByIssuerAndSN(CERT_GetDefaultCertDB(), &issuerSN);
PR_FREEIF(keyItem.data);
ScopedCERTCertificate cert(
CERT_FindCertByIssuerAndSN(CERT_GetDefaultCertDB(), &issuerSN));
if (cert) {
nsCOMPtr<nsIX509Cert> nssCert = nsNSSCertificate::Create(cert.get());
if (!nssCert)
if (!nssCert) {
return NS_ERROR_OUT_OF_MEMORY;
}
nssCert.forget(_cert);
}
return NS_OK;
@ -1221,12 +1241,10 @@ nsNSSCertificateDB::getCertNames(CERTCertList *certList,
node = CERT_LIST_NEXT(node)) {
if (getCertType(node->cert) == type) {
RefPtr<nsNSSCertificate> pipCert(new nsNSSCertificate(node->cert));
char *dbkey = nullptr;
char *namestr = nullptr;
nsAutoString certstr;
pipCert->GetDbKey(&dbkey);
nsAutoCString dbkey;
pipCert->GetDbKey(dbkey);
nsAutoString keystr = NS_ConvertASCIItoUTF16(dbkey);
PR_FREEIF(dbkey);
char *namestr = nullptr;
if (type == nsIX509Cert::EMAIL_CERT) {
namestr = node->cert->emailAddr;
} else {
@ -1237,6 +1255,7 @@ nsNSSCertificateDB::getCertNames(CERTCertList *certList,
}
}
nsAutoString certname = NS_ConvertASCIItoUTF16(namestr ? namestr : "");
nsAutoString certstr;
certstr.Append(char16_t(DELIM));
certstr += certname;
certstr.Append(char16_t(DELIM));

View File

@ -31,7 +31,7 @@ nsNSSCertificateFakeTransport::~nsNSSCertificateFakeTransport()
}
NS_IMETHODIMP
nsNSSCertificateFakeTransport::GetDbKey(char**)
nsNSSCertificateFakeTransport::GetDbKey(nsACString&)
{
NS_NOTREACHED("Unimplemented on content process");
return NS_ERROR_NOT_IMPLEMENTED;

View File

@ -0,0 +1,152 @@
// -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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";
// This test tests that the nsIX509Cert.dbKey and nsIX509CertDB.findCertByDBKey
// APIs work as expected. That is, getting a certificate's dbKey and using it
// in findCertByDBKey should return the same certificate. Also, for backwards
// compatibility, findCertByDBKey should ignore any whitespace in its input
// (even though now nsIX509Cert.dbKey will never have whitespace in it).
function hexStringToBytes(hex) {
let bytes = [];
for (let hexByteStr of hex.split(":")) {
bytes.push(parseInt(hexByteStr, 16));
}
return bytes;
}
function encodeCommonNameAsBytes(commonName) {
// The encoding will look something like this (in hex):
// 30 (SEQUENCE) <length of contents>
// 31 (SET) <length of contents>
// 30 (SEQUENCE) <length of contents>
// 06 (OID) 03 (length)
// 55 04 03 (id-at-commonName)
// 0C (UTF8String) <length of common name>
// <common name bytes>
// To make things simple, it would be nice to have the length of each
// component be less than 128 bytes (so we can have single-byte lengths).
// For this to hold, the maximum length of the contents of the outermost
// SEQUENCE must be 127. Everything not in the contents of the common name
// will take up 11 bytes, so the value of the common name itself can be at
// most 116 bytes.
ok(commonName.length <= 116,
"test assumption: common name can't be longer than 116 bytes (makes " +
"DER encoding easier)");
let commonNameOIDBytes = [ 0x06, 0x03, 0x55, 0x04, 0x03 ];
let commonNameBytes = [ 0x0C, commonName.length ];
for (let i = 0; i < commonName.length; i++) {
commonNameBytes.push(commonName.charCodeAt(i));
}
let bytes = commonNameOIDBytes.concat(commonNameBytes);
bytes.unshift(bytes.length);
bytes.unshift(0x30); // SEQUENCE
bytes.unshift(bytes.length);
bytes.unshift(0x31); // SET
bytes.unshift(bytes.length);
bytes.unshift(0x30); // SEQUENCE
return bytes;
}
function testInvalidDBKey(certDB, dbKey) {
let exceptionCaught = false;
try {
let cert = certDB.findCertByDBKey(dbKey, null);
} catch(e) {
do_print(e);
exceptionCaught = true;
}
ok(exceptionCaught, "should have thrown and caught an exception");
}
function testDBKeyForNonexistentCert(certDB, dbKey) {
let cert = certDB.findCertByDBKey(dbKey, null);
ok(!cert, "shouldn't find cert for given dbKey");
}
function byteArrayToByteString(bytes) {
let byteString = "";
for (let b of bytes) {
byteString += String.fromCharCode(b);
}
return byteString;
}
function run_test() {
do_get_profile();
let certDB = Cc["@mozilla.org/security/x509certdb;1"]
.getService(Ci.nsIX509CertDB);
let cert = constructCertFromFile("bad_certs/test-ca.pem");
equal(cert.issuerName, "CN=" + cert.issuerCommonName,
"test assumption: this certificate's issuer distinguished name " +
"consists only of a common name");
let issuerBytes = encodeCommonNameAsBytes(cert.issuerCommonName);
ok(issuerBytes.length < 256,
"test assumption: length of encoded issuer is less than 256 bytes");
let serialNumberBytes = hexStringToBytes(cert.serialNumber);
ok(serialNumberBytes.length < 256,
"test assumption: length of encoded serial number is less than 256 bytes");
let dbKeyHeader = [ 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, serialNumberBytes.length,
0, 0, 0, issuerBytes.length ];
let expectedDbKeyBytes = dbKeyHeader.concat(serialNumberBytes, issuerBytes);
let expectedDbKey = btoa(byteArrayToByteString(expectedDbKeyBytes));
equal(cert.dbKey, expectedDbKey,
"actual and expected dbKey values should match");
let certFromDbKey = certDB.findCertByDBKey(expectedDbKey, null);
ok(certFromDbKey.equals(cert),
"nsIX509CertDB.findCertByDBKey should find the right certificate");
ok(expectedDbKey.length > 64,
"test assumption: dbKey should be longer than 64 characters");
let expectedDbKeyWithCRLF = expectedDbKey.replace(/(.{64})/, "$1\r\n");
ok(expectedDbKeyWithCRLF.indexOf("\r\n") == 64,
"test self-check: adding CRLF to dbKey should succeed");
certFromDbKey = certDB.findCertByDBKey(expectedDbKeyWithCRLF, null);
ok(certFromDbKey.equals(cert),
"nsIX509CertDB.findCertByDBKey should work with dbKey with CRLF");
let expectedDbKeyWithSpaces = expectedDbKey.replace(/(.{64})/, "$1 ");
ok(expectedDbKeyWithSpaces.indexOf(" ") == 64,
"test self-check: adding spaces to dbKey should succeed");
certFromDbKey = certDB.findCertByDBKey(expectedDbKeyWithSpaces, null);
ok(certFromDbKey.equals(cert),
"nsIX509CertDB.findCertByDBKey should work with dbKey with spaces");
// Test some invalid dbKey values.
testInvalidDBKey(certDB, "AAAA"); // Not long enough.
// No header.
testInvalidDBKey(certDB, btoa(byteArrayToByteString(
[ 0, 0, 0, serialNumberBytes.length,
0, 0, 0, issuerBytes.length ].concat(serialNumberBytes, issuerBytes))));
testInvalidDBKey(certDB, btoa(byteArrayToByteString(
[ 0, 0, 0, 0, 0, 0, 0, 0,
255, 255, 255, 255, // serial number length is way too long
255, 255, 255, 255, // issuer length is way too long
0, 0, 0, 0 ])));
// Truncated issuer.
testInvalidDBKey(certDB, btoa(byteArrayToByteString(
[ 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1,
0, 0, 0, 10,
1,
1, 2, 3 ])));
// Issuer doesn't decode to valid common name.
testDBKeyForNonexistentCert(certDB, btoa(byteArrayToByteString(
[ 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1,
0, 0, 0, 3,
1,
1, 2, 3 ])));
// zero-length serial number and issuer -> no such certificate
testDBKeyForNonexistentCert(certDB, btoa(byteArrayToByteString(
[ 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0 ])));
}

View File

@ -53,6 +53,7 @@ skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_pinning_dynamic.js]
[test_pinning_header_parsing.js]
[test_cert_dbKey.js]
[test_cert_keyUsage.js]
[test_logoutAndTeardown.js]
run-sequentially = hardcoded ports