bug 1050546 - telemetry for baseline requirements sections 9.2.1 and 9.2.2 (subject alt names/common name) r=rbarnes

This commit is contained in:
David Keeler 2014-09-03 11:44:08 -07:00
parent 0b85b6d782
commit a549c1a3b3
3 changed files with 230 additions and 1 deletions

View File

@ -79,6 +79,8 @@ private:
};
void InitCertVerifierLog();
SECStatus IsCertBuiltInRoot(CERTCertificate* cert, bool& result);
} } // namespace mozilla::psm
#endif // mozilla_psm__CertVerifier_h

View File

@ -98,6 +98,7 @@
#include "pkix/pkixtypes.h"
#include "pkix/pkixnss.h"
#include "pkix/ScopedPtr.h"
#include "CertVerifier.h"
#include "CryptoTask.h"
#include "ExtendedValidation.h"
@ -112,6 +113,7 @@
#include "mozilla/Assertions.h"
#include "mozilla/Mutex.h"
#include "mozilla/Telemetry.h"
#include "mozilla/net/DNS.h"
#include "mozilla/unused.h"
#include "nsIThreadPool.h"
#include "nsNetUtil.h"
@ -121,6 +123,7 @@
#include "PSMRunnable.h"
#include "SharedSSLState.h"
#include "nsContentUtils.h"
#include "nsURLHelper.h"
#include "ssl.h"
#include "secerr.h"
@ -739,6 +742,216 @@ BlockServerCertChangeForSpdy(nsNSSSocketInfo* infoObject,
return SECFailure;
}
void
AccumulateSubjectCommonNameTelemetry(const char* commonName,
bool commonNameInSubjectAltNames)
{
if (!commonName) {
// 1 means no common name present
Telemetry::Accumulate(Telemetry::BR_9_2_2_SUBJECT_COMMON_NAME, 1);
} else if (!commonNameInSubjectAltNames) {
PR_LOG(gPIPNSSLog, PR_LOG_DEBUG,
("BR telemetry: common name '%s' not in subject alt. names "
"(or the subject alt. names extension is not present)\n",
commonName));
// 2 means the common name is not present in subject alt names
Telemetry::Accumulate(Telemetry::BR_9_2_2_SUBJECT_COMMON_NAME, 2);
} else {
// 0 means the common name is present in subject alt names
Telemetry::Accumulate(Telemetry::BR_9_2_2_SUBJECT_COMMON_NAME, 0);
}
}
// Returns true if and only if commonName ends with altName (minus its leading
// "*"). altName has already been checked to be of the form "*.<something>".
// commonName may be NULL.
static bool
TryMatchingWildcardSubjectAltName(const char* commonName,
nsDependentCString altName)
{
if (!commonName) {
return false;
}
// altNameSubstr is now ".<something>"
nsDependentCString altNameSubstr(altName.get() + 1, altName.Length() - 1);
nsDependentCString commonNameStr(commonName, strlen(commonName));
int32_t altNameIndex = commonNameStr.Find(altNameSubstr);
// This only matches if the end of commonNameStr is the altName without
// the '*'.
// Consider this.example.com and *.example.com:
// "this.example.com".Find(".example.com") is 4
// 4 + ".example.com".Length() == 4 + 12 == 16 == "this.example.com".Length()
// Now this.example.com and *.example:
// "this.example.com".Find(".example") is 4
// 4 + ".example".Length() == 4 + 8 == 12 != "this.example.com".Length()
return altNameIndex >= 0 &&
altNameIndex + altNameSubstr.Length() == commonNameStr.Length();
}
// Gathers telemetry on Baseline Requirements 9.2.1 (Subject Alternative
// Names Extension) and 9.2.2 (Subject Common Name Field).
// Specifically:
// - whether or not the subject common name field is present
// - whether or not the subject alternative names extension is present
// - if there is a malformed entry in the subject alt. names extension
// - if there is an entry in the subject alt. names extension corresponding
// to the subject common name
// Telemetry is only gathered for certificates that chain to a trusted root
// in Mozilla's Root CA program.
// certList consists of a validated certificate chain. The end-entity
// certificate is first and the root (trust anchor) is last.
void
GatherBaselineRequirementsTelemetry(const ScopedCERTCertList& certList)
{
CERTCertListNode* endEntityNode = CERT_LIST_HEAD(certList);
CERTCertListNode* rootNode = CERT_LIST_TAIL(certList);
PR_ASSERT(endEntityNode && rootNode);
if (!endEntityNode || !rootNode) {
return;
}
CERTCertificate* cert = endEntityNode->cert;
mozilla::pkix::ScopedPtr<char, PORT_Free_string> commonName(
CERT_GetCommonName(&cert->subject));
// This only applies to certificates issued by authorities in our root
// program.
bool isBuiltIn = false;
SECStatus rv = IsCertBuiltInRoot(rootNode->cert, isBuiltIn);
if (rv != SECSuccess || !isBuiltIn) {
PR_LOG(gPIPNSSLog, PR_LOG_DEBUG,
("BR telemetry: '%s' not a built-in root (or IsCertBuiltInRoot "
"failed)\n", commonName.get()));
return;
}
SECItem altNameExtension;
rv = CERT_FindCertExtension(cert, SEC_OID_X509_SUBJECT_ALT_NAME,
&altNameExtension);
if (rv != SECSuccess) {
PR_LOG(gPIPNSSLog, PR_LOG_DEBUG,
("BR telemetry: no subject alt names extension for '%s'\n",
commonName.get()));
// 1 means there is no subject alt names extension
Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 1);
AccumulateSubjectCommonNameTelemetry(commonName.get(), false);
return;
}
ScopedPLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE));
CERTGeneralName* subjectAltNames =
CERT_DecodeAltNameExtension(arena, &altNameExtension);
// CERT_FindCertExtension takes a pointer to a SECItem and allocates memory
// in its data field. This is a bad way to do this because we can't use a
// ScopedSECItem and neither is that memory tracked by an arena. We have to
// manually reach in and free the memory.
PORT_Free(altNameExtension.data);
if (!subjectAltNames) {
PR_LOG(gPIPNSSLog, PR_LOG_DEBUG,
("BR telemetry: could not decode subject alt names for '%s'\n",
commonName.get()));
// 2 means the subject alt names extension could not be decoded
Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 2);
AccumulateSubjectCommonNameTelemetry(commonName.get(), false);
return;
}
CERTGeneralName* currentName = subjectAltNames;
bool commonNameInSubjectAltNames = false;
bool nonDNSNameOrIPAddressPresent = false;
bool malformedDNSNameOrIPAddressPresent = false;
bool nonFQDNPresent = false;
do {
nsDependentCString altName;
if (currentName->type == certDNSName) {
altName.Assign(reinterpret_cast<char*>(currentName->name.other.data),
currentName->name.other.len);
nsDependentCString altNameWithoutWildcard(altName);
if (altNameWithoutWildcard.Find("*.") == 0) {
altNameWithoutWildcard.Assign(altName.get() + 2, altName.Length() - 2);
commonNameInSubjectAltNames |=
TryMatchingWildcardSubjectAltName(commonName.get(), altName);
}
// net_IsValidHostName appears to return true for valid IP addresses,
// which would be invalid for a DNS name.
// Note that the net_IsValidHostName check will catch things like
// "a.*.example.com".
if (!net_IsValidHostName(altNameWithoutWildcard) ||
net_IsValidIPv4Addr(altName.get(), altName.Length()) ||
net_IsValidIPv6Addr(altName.get(), altName.Length())) {
PR_LOG(gPIPNSSLog, PR_LOG_DEBUG,
("BR telemetry: DNSName '%s' not valid (for '%s')\n",
altName.get(), commonName.get()));
malformedDNSNameOrIPAddressPresent = true;
}
if (altName.FindChar('.') == kNotFound) {
nonFQDNPresent = true;
}
} else if (currentName->type == certIPAddress) {
// According to DNS.h, this includes space for the null-terminator
char buf[net::kNetAddrMaxCStrBufSize] = { 0 };
PRNetAddr addr;
if (currentName->name.other.len == 4) {
addr.inet.family = PR_AF_INET;
memcpy(&addr.inet.ip, currentName->name.other.data,
currentName->name.other.len);
if (PR_NetAddrToString(&addr, buf, sizeof(buf) - 1) != PR_SUCCESS) {
PR_LOG(gPIPNSSLog, PR_LOG_DEBUG,
("BR telemetry: IPAddress (v4) not valid (for '%s')\n",
commonName.get()));
malformedDNSNameOrIPAddressPresent = true;
} else {
altName.Assign(buf, strlen(buf));
}
} else if (currentName->name.other.len == 16) {
addr.inet.family = PR_AF_INET6;
memcpy(&addr.ipv6.ip, currentName->name.other.data,
currentName->name.other.len);
if (PR_NetAddrToString(&addr, buf, sizeof(buf) - 1) != PR_SUCCESS) {
PR_LOG(gPIPNSSLog, PR_LOG_DEBUG,
("BR telemetry: IPAddress (v6) not valid (for '%s')\n",
commonName.get()));
malformedDNSNameOrIPAddressPresent = true;
} else {
altName.Assign(buf, strlen(buf));
}
} else {
PR_LOG(gPIPNSSLog, PR_LOG_DEBUG,
("BR telemetry: IPAddress not valid (for '%s')\n",
commonName.get()));
malformedDNSNameOrIPAddressPresent = true;
}
} else {
PR_LOG(gPIPNSSLog, PR_LOG_DEBUG,
("BR telemetry: non-DNSName, non-IPAddress present for '%s'\n",
commonName.get()));
nonDNSNameOrIPAddressPresent = true;
}
if (commonName && altName.Equals(commonName.get())) {
commonNameInSubjectAltNames = true;
}
currentName = CERT_GetNextGeneralName(currentName);
} while (currentName && currentName != subjectAltNames);
if (nonDNSNameOrIPAddressPresent) {
// 3 means there's an entry that isn't an ip address or dns name
Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 3);
}
if (malformedDNSNameOrIPAddressPresent) {
// 4 means there's a malformed ip address or dns name entry
Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 4);
}
if (nonFQDNPresent) {
// 5 means there's a DNS name entry with a non-fully-qualified domain name
Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 5);
}
if (!nonDNSNameOrIPAddressPresent && !malformedDNSNameOrIPAddressPresent &&
!nonFQDNPresent) {
// 0 means the extension is acceptable
Telemetry::Accumulate(Telemetry::BR_9_2_1_SUBJECT_ALT_NAMES, 0);
}
AccumulateSubjectCommonNameTelemetry(commonName.get(),
commonNameInSubjectAltNames);
}
SECStatus
AuthCertificate(CertVerifier& certVerifier,
TransportSecurityInfo* infoObject,
@ -759,10 +972,11 @@ AuthCertificate(CertVerifier& certVerifier,
!(providerFlags & nsISocketProvider::NO_PERMANENT_STORAGE);
SECOidTag evOidPolicy;
ScopedCERTCertList certList;
rv = certVerifier.VerifySSLServerCert(cert, stapledOCSPResponse,
time, infoObject,
infoObject->GetHostNameRaw(),
saveIntermediates, 0, nullptr,
saveIntermediates, 0, &certList,
&evOidPolicy);
// We want to remember the CA certs in the temp db, so that the application can find the
@ -782,6 +996,7 @@ AuthCertificate(CertVerifier& certVerifier,
}
if (rv == SECSuccess) {
GatherBaselineRequirementsTelemetry(certList);
// The connection may get terminated, for example, if the server requires
// a client cert. Let's provide a minimal SSLStatus
// to the caller that contains at least the cert and its status.

View File

@ -6507,6 +6507,18 @@
"n_buckets": 10,
"description": "Sidebar showing: seconds that the sidebar has been opened"
},
"BR_9_2_1_SUBJECT_ALT_NAMES": {
"expires_in_version": "never",
"kind": "enumerated",
"n_values": 8,
"description": "Baseline Requirements section 9.2.1: subject alternative names extension (0: ok, 1 or more: error)"
},
"BR_9_2_2_SUBJECT_COMMON_NAME": {
"expires_in_version": "never",
"kind": "enumerated",
"n_values": 8,
"description": "Baseline Requirements section 9.2.2: subject common name field (0: present, in subject alt. names; 1: not present; 2: not present in subject alt. names)"
},
"TRACKING_PROTECTION_ENABLED": {
"expires_in_version": "never",
"kind": "boolean",