Bug 1876435 - gather telemetry on the sources of issuer certificates r=jschanck

Differential Revision: https://phabricator.services.mozilla.com/D199599
This commit is contained in:
Dana Keeler 2024-01-29 18:28:00 +00:00
Родитель 2694839f3b
Коммит 61cba1c84d
6 изменённых файлов: 236 добавлений и 88 удалений

Просмотреть файл

@ -452,7 +452,8 @@ Result CertVerifier::VerifyCert(
/*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo,
/*optional out*/ CertificateTransparencyInfo* ctInfo,
/*optional out*/ bool* isBuiltChainRootBuiltInRoot,
/*optional out*/ bool* madeOCSPRequests) {
/*optional out*/ bool* madeOCSPRequests,
/*optional out*/ IssuerSources* issuerSources) {
MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("Top of VerifyCert\n"));
MOZ_ASSERT(usage == certificateUsageSSLServer || !(flags & FLAG_MUST_BE_EV));
@ -494,6 +495,10 @@ Result CertVerifier::VerifyCert(
*madeOCSPRequests = false;
}
if (issuerSources) {
issuerSources->clear();
}
Input certDER;
Result rv = certDER.Init(certBytes.Elements(), certBytes.Length());
if (rv != Success) {
@ -584,6 +589,9 @@ Result CertVerifier::VerifyCert(
*madeOCSPRequests |=
trustDomain.GetOCSPFetchStatus() == OCSPFetchStatus::Fetched;
}
if (issuerSources) {
*issuerSources = trustDomain.GetIssuerSources();
}
if (rv == Success) {
rv = VerifyCertificateTransparencyPolicy(
trustDomain, builtChain, sctsFromTLSInput, time, ctInfo);
@ -643,6 +651,9 @@ Result CertVerifier::VerifyCert(
*madeOCSPRequests |=
trustDomain.GetOCSPFetchStatus() == OCSPFetchStatus::Fetched;
}
if (issuerSources) {
*issuerSources = trustDomain.GetIssuerSources();
}
if (rv != Success && !IsFatalError(rv) &&
rv != Result::ERROR_REVOKED_CERTIFICATE &&
trustDomain.GetIsErrorDueToDistrustedCAPolicy()) {
@ -807,7 +818,8 @@ Result CertVerifier::VerifySSLServerCert(
/*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo,
/*optional out*/ CertificateTransparencyInfo* ctInfo,
/*optional out*/ bool* isBuiltChainRootBuiltInRoot,
/*optional out*/ bool* madeOCSPRequests) {
/*optional out*/ bool* madeOCSPRequests,
/*optional out*/ IssuerSources* issuerSources) {
// XXX: MOZ_ASSERT(pinarg);
MOZ_ASSERT(!hostname.IsEmpty());
@ -832,12 +844,12 @@ Result CertVerifier::VerifySSLServerCert(
return rv;
}
bool isBuiltChainRootBuiltInRootLocal;
rv = VerifyCert(peerCertBytes, certificateUsageSSLServer, time, pinarg,
PromiseFlatCString(hostname).get(), builtChain, flags,
extraCertificates, stapledOCSPResponse, sctsFromTLS,
originAttributes, evStatus, ocspStaplingStatus, keySizeStatus,
pinningTelemetryInfo, ctInfo,
&isBuiltChainRootBuiltInRootLocal, madeOCSPRequests);
rv = VerifyCert(
peerCertBytes, certificateUsageSSLServer, time, pinarg,
PromiseFlatCString(hostname).get(), builtChain, flags, extraCertificates,
stapledOCSPResponse, sctsFromTLS, originAttributes, evStatus,
ocspStaplingStatus, keySizeStatus, pinningTelemetryInfo, ctInfo,
&isBuiltChainRootBuiltInRootLocal, madeOCSPRequests, issuerSources);
if (rv != Success) {
// we don't use the certificate for path building, so this parameter doesn't
// matter

Просмотреть файл

@ -13,6 +13,7 @@
#include "OCSPCache.h"
#include "RootCertificateTelemetryUtils.h"
#include "ScopedNSSTypes.h"
#include "mozilla/EnumSet.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtr.h"
@ -70,6 +71,17 @@ enum class CRLiteMode {
enum class NetscapeStepUpPolicy : uint32_t;
// Describes the source of the associated issuer.
enum class IssuerSource {
TLSHandshake, // included by the peer in the TLS handshake
PreloadedIntermediates, // a preloaded intermediate (via remote settings)
ThirdPartyCertificates, // a third-party certificate gleaned from the OS
NSSCertDB, // a certificate found in the profile's NSS certificate DB
BuiltInRootsModule, // a root from the built-in roots module
};
using IssuerSources = EnumSet<IssuerSource>;
class PinningTelemetryInfo {
public:
PinningTelemetryInfo()
@ -163,7 +175,8 @@ class CertVerifier {
/*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo = nullptr,
/*optional out*/ CertificateTransparencyInfo* ctInfo = nullptr,
/*optional out*/ bool* isBuiltChainRootBuiltInRoot = nullptr,
/*optional out*/ bool* madeOCSPRequests = nullptr);
/*optional out*/ bool* madeOCSPRequests = nullptr,
/*optional out*/ IssuerSources* = nullptr);
mozilla::pkix::Result VerifySSLServerCert(
const nsTArray<uint8_t>& peerCert, mozilla::pkix::Time time, void* pinarg,
@ -184,7 +197,8 @@ class CertVerifier {
/*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo = nullptr,
/*optional out*/ CertificateTransparencyInfo* ctInfo = nullptr,
/*optional out*/ bool* isBuiltChainRootBuiltInRoot = nullptr,
/*optional out*/ bool* madeOCSPRequests = nullptr);
/*optional out*/ bool* madeOCSPRequests = nullptr,
/*optional out*/ IssuerSources* = nullptr);
enum OcspDownloadConfig { ocspOff = 0, ocspOn = 1, ocspEVOnly = 2 };
enum OcspStrictConfig { ocspRelaxed = 0, ocspStrict };

Просмотреть файл

@ -137,8 +137,7 @@ static void FindRootsWithSubject(UniqueSECMODModule& rootsModule,
// certificate extensions we support are restrictive rather than additive in
// terms of the rest of the chain (for example, we don't support policy mapping
// and we ignore any SCT information in intermediates).
static bool ShouldSkipSelfSignedNonTrustAnchor(TrustDomain& trustDomain,
Input certDER) {
bool NSSCertDBTrustDomain::ShouldSkipSelfSignedNonTrustAnchor(Input certDER) {
BackCert cert(certDER, EndEntityOrCA::MustBeCA, nullptr);
if (cert.Init() != Success) {
return false; // turn any failures into "don't skip trying this cert"
@ -148,8 +147,8 @@ static bool ShouldSkipSelfSignedNonTrustAnchor(TrustDomain& trustDomain,
return false;
}
TrustLevel trust;
if (trustDomain.GetCertTrust(EndEntityOrCA::MustBeCA, CertPolicyId::anyPolicy,
certDER, trust) != Success) {
if (GetCertTrust(EndEntityOrCA::MustBeCA, CertPolicyId::anyPolicy, certDER,
trust) != Success) {
return false;
}
// If the trust for this certificate is anything other than "inherit", we want
@ -157,7 +156,7 @@ static bool ShouldSkipSelfSignedNonTrustAnchor(TrustDomain& trustDomain,
if (trust != TrustLevel::InheritsTrust) {
return false;
}
if (VerifySignedData(trustDomain, cert.GetSignedData(),
if (VerifySignedData(*this, cert.GetSignedData(),
cert.GetSubjectPublicKeyInfo()) != Success) {
return false;
}
@ -166,24 +165,25 @@ static bool ShouldSkipSelfSignedNonTrustAnchor(TrustDomain& trustDomain,
return true;
}
static Result CheckCandidates(TrustDomain& trustDomain,
TrustDomain::IssuerChecker& checker,
nsTArray<Input>& candidates,
Input* nameConstraintsInputPtr, bool& keepGoing) {
for (Input candidate : candidates) {
Result NSSCertDBTrustDomain::CheckCandidates(
IssuerChecker& checker, nsTArray<IssuerCandidateWithSource>& candidates,
Input* nameConstraintsInputPtr, bool& keepGoing) {
for (const auto& candidate : candidates) {
// Stop path building if the program is shutting down.
if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
keepGoing = false;
return Success;
}
if (ShouldSkipSelfSignedNonTrustAnchor(trustDomain, candidate)) {
if (ShouldSkipSelfSignedNonTrustAnchor(candidate.mDER)) {
continue;
}
Result rv = checker.Check(candidate, nameConstraintsInputPtr, keepGoing);
Result rv =
checker.Check(candidate.mDER, nameConstraintsInputPtr, keepGoing);
if (rv != Success) {
return rv;
}
if (!keepGoing) {
mIssuerSources += candidate.mIssuerSource;
return Success;
}
}
@ -212,29 +212,8 @@ Result NSSCertDBTrustDomain::FindIssuer(Input encodedIssuerName,
// First try all relevant certificates known to Gecko, which avoids calling
// CERT_CreateSubjectCertList, because that can be expensive.
nsTArray<Input> geckoRootCandidates;
nsTArray<Input> geckoIntermediateCandidates;
if (!mCertStorage) {
return Result::FATAL_ERROR_LIBRARY_FAILURE;
}
nsTArray<uint8_t> subject;
subject.AppendElements(encodedIssuerName.UnsafeGetData(),
encodedIssuerName.GetLength());
nsTArray<nsTArray<uint8_t>> certs;
nsresult rv = mCertStorage->FindCertsBySubject(subject, certs);
if (NS_FAILED(rv)) {
return Result::FATAL_ERROR_LIBRARY_FAILURE;
}
for (auto& cert : certs) {
Input certDER;
Result rv = certDER.Init(cert.Elements(), cert.Length());
if (rv != Success) {
continue; // probably too big
}
// Currently we're only expecting intermediate certificates in cert storage.
geckoIntermediateCandidates.AppendElement(std::move(certDER));
}
nsTArray<IssuerCandidateWithSource> geckoRootCandidates;
nsTArray<IssuerCandidateWithSource> geckoIntermediateCandidates;
// We might not have this module if e.g. we're on a Linux distribution that
// does something unexpected.
@ -248,45 +227,14 @@ Result NSSCertDBTrustDomain::FindIssuer(Input encodedIssuerName,
if (rv != Success) {
continue; // probably too big
}
geckoRootCandidates.AppendElement(rootInput);
geckoRootCandidates.AppendElement(IssuerCandidateWithSource{
rootInput, IssuerSource::BuiltInRootsModule});
}
} else {
MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
("NSSCertDBTrustDomain::FindIssuer: no built-in roots module"));
}
for (const auto& thirdPartyRootInput : mThirdPartyRootInputs) {
BackCert root(thirdPartyRootInput, EndEntityOrCA::MustBeCA, nullptr);
Result rv = root.Init();
if (rv != Success) {
continue;
}
// Filter out 3rd party roots that can't be issuers we're looking for
// because the subject distinguished name doesn't match. This prevents
// mozilla::pkix from accumulating spurious errors during path building.
if (!InputsAreEqual(encodedIssuerName, root.GetSubject())) {
continue;
}
geckoRootCandidates.AppendElement(thirdPartyRootInput);
}
for (const auto& thirdPartyIntermediateInput :
mThirdPartyIntermediateInputs) {
BackCert intermediate(thirdPartyIntermediateInput, EndEntityOrCA::MustBeCA,
nullptr);
Result rv = intermediate.Init();
if (rv != Success) {
continue;
}
// Filter out 3rd party intermediates that can't be issuers we're looking
// for because the subject distinguished name doesn't match. This prevents
// mozilla::pkix from accumulating spurious errors during path building.
if (!InputsAreEqual(encodedIssuerName, intermediate.GetSubject())) {
continue;
}
geckoIntermediateCandidates.AppendElement(thirdPartyIntermediateInput);
}
if (mExtraCertificates.isSome()) {
for (const auto& extraCert : *mExtraCertificates) {
Input certInput;
@ -307,15 +255,72 @@ Result NSSCertDBTrustDomain::FindIssuer(Input encodedIssuerName,
}
// We assume that extra certificates (presumably from the TLS handshake)
// are intermediates, since sending trust anchors would be superfluous.
geckoIntermediateCandidates.AppendElement(certInput);
geckoIntermediateCandidates.AppendElement(
IssuerCandidateWithSource{certInput, IssuerSource::TLSHandshake});
}
}
for (const auto& thirdPartyRootInput : mThirdPartyRootInputs) {
BackCert root(thirdPartyRootInput, EndEntityOrCA::MustBeCA, nullptr);
Result rv = root.Init();
if (rv != Success) {
continue;
}
// Filter out 3rd party roots that can't be issuers we're looking for
// because the subject distinguished name doesn't match. This prevents
// mozilla::pkix from accumulating spurious errors during path building.
if (!InputsAreEqual(encodedIssuerName, root.GetSubject())) {
continue;
}
geckoRootCandidates.AppendElement(IssuerCandidateWithSource{
thirdPartyRootInput, IssuerSource::ThirdPartyCertificates});
}
for (const auto& thirdPartyIntermediateInput :
mThirdPartyIntermediateInputs) {
BackCert intermediate(thirdPartyIntermediateInput, EndEntityOrCA::MustBeCA,
nullptr);
Result rv = intermediate.Init();
if (rv != Success) {
continue;
}
// Filter out 3rd party intermediates that can't be issuers we're looking
// for because the subject distinguished name doesn't match. This prevents
// mozilla::pkix from accumulating spurious errors during path building.
if (!InputsAreEqual(encodedIssuerName, intermediate.GetSubject())) {
continue;
}
geckoIntermediateCandidates.AppendElement(IssuerCandidateWithSource{
thirdPartyIntermediateInput, IssuerSource::ThirdPartyCertificates});
}
if (!mCertStorage) {
return Result::FATAL_ERROR_LIBRARY_FAILURE;
}
nsTArray<uint8_t> subject;
subject.AppendElements(encodedIssuerName.UnsafeGetData(),
encodedIssuerName.GetLength());
nsTArray<nsTArray<uint8_t>> certs;
nsresult rv = mCertStorage->FindCertsBySubject(subject, certs);
if (NS_FAILED(rv)) {
return Result::FATAL_ERROR_LIBRARY_FAILURE;
}
for (auto& cert : certs) {
Input certDER;
Result rv = certDER.Init(cert.Elements(), cert.Length());
if (rv != Success) {
continue; // probably too big
}
// Currently we're only expecting intermediate certificates in cert storage.
geckoIntermediateCandidates.AppendElement(IssuerCandidateWithSource{
std::move(certDER), IssuerSource::PreloadedIntermediates});
}
// Try all root certs first and then all (presumably) intermediates.
geckoRootCandidates.AppendElements(std::move(geckoIntermediateCandidates));
bool keepGoing = true;
Result result = CheckCandidates(*this, checker, geckoRootCandidates,
Result result = CheckCandidates(checker, geckoRootCandidates,
nameConstraintsInputPtr, keepGoing);
if (result != Success) {
return result;
@ -365,14 +370,15 @@ Result NSSCertDBTrustDomain::FindIssuer(Input encodedIssuerName,
return Result::FATAL_ERROR_LIBRARY_FAILURE;
}
nsTArray<Input> nssCandidates;
nsTArray<IssuerCandidateWithSource> nssCandidates;
for (const auto& rootCandidate : nssRootCandidates) {
Input certDER;
Result rv = certDER.Init(rootCandidate.Elements(), rootCandidate.Length());
if (rv != Success) {
continue; // probably too big
}
nssCandidates.AppendElement(std::move(certDER));
nssCandidates.AppendElement(
IssuerCandidateWithSource{std::move(certDER), IssuerSource::NSSCertDB});
}
for (const auto& intermediateCandidate : nssIntermediateCandidates) {
Input certDER;
@ -381,10 +387,11 @@ Result NSSCertDBTrustDomain::FindIssuer(Input encodedIssuerName,
if (rv != Success) {
continue; // probably too big
}
nssCandidates.AppendElement(std::move(certDER));
nssCandidates.AppendElement(
IssuerCandidateWithSource{std::move(certDER), IssuerSource::NSSCertDB});
}
return CheckCandidates(*this, checker, nssCandidates, nameConstraintsInputPtr,
return CheckCandidates(checker, nssCandidates, nameConstraintsInputPtr,
keepGoing);
}
@ -1606,6 +1613,7 @@ void NSSCertDBTrustDomain::ResetAccumulatedState() {
mSCTListFromCertificate = nullptr;
mSawDistrustedCAByPolicyError = false;
mIsBuiltChainRootBuiltInRoot = false;
mIssuerSources.clear();
}
static Input SECItemToInput(const UniqueSECItem& item) {

Просмотреть файл

@ -54,6 +54,13 @@ enum class OCSPFetchStatus : uint16_t {
Fetched = 1,
};
// Helper struct to associate the DER bytes of a potential issuer certificate
// with its source (i.e. where it came from).
struct IssuerCandidateWithSource {
mozilla::pkix::Input mDER; // non-owning
IssuerSource mIssuerSource;
};
SECStatus InitializeNSS(const nsACString& dir, NSSDBConfig nssDbConfig,
PKCS11DBConfig pkcs11DbConfig);
@ -245,6 +252,7 @@ class NSSCertDBTrustDomain : public mozilla::pkix::TrustDomain {
bool GetIsErrorDueToDistrustedCAPolicy() const;
OCSPFetchStatus GetOCSPFetchStatus() { return mOCSPFetchStatus; }
IssuerSources GetIssuerSources() { return mIssuerSources; }
private:
Result CheckCRLiteStash(
@ -290,6 +298,12 @@ class NSSCertDBTrustDomain : public mozilla::pkix::TrustDomain {
const Result error,
/*out*/ bool& softFailure);
bool ShouldSkipSelfSignedNonTrustAnchor(mozilla::pkix::Input certDER);
Result CheckCandidates(IssuerChecker& checker,
nsTArray<IssuerCandidateWithSource>& candidates,
mozilla::pkix::Input* nameConstraintsInputPtr,
bool& keepGoing);
const SECTrustType mCertDBTrustType;
const OCSPFetching mOCSPFetching;
OCSPCache& mOCSPCache; // non-owning!
@ -321,6 +335,7 @@ class NSSCertDBTrustDomain : public mozilla::pkix::TrustDomain {
UniqueSECMODModule mBuiltInRootsModule;
OCSPFetchStatus mOCSPFetchStatus;
IssuerSources mIssuerSources;
};
} // namespace psm

Просмотреть файл

@ -112,6 +112,7 @@
#include "mozilla/Telemetry.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/glean/GleanMetrics.h"
#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsICertOverrideService.h"
@ -529,7 +530,8 @@ static void CollectCertTelemetry(
KeySizeStatus aKeySizeStatus,
const PinningTelemetryInfo& aPinningTelemetryInfo,
const nsTArray<nsTArray<uint8_t>>& aBuiltCertChain,
const CertificateTransparencyInfo& aCertificateTransparencyInfo) {
const CertificateTransparencyInfo& aCertificateTransparencyInfo,
const IssuerSources& issuerSources) {
uint32_t evStatus = (aCertVerificationResult != Success) ? 0 // 0 = Failure
: (aEVStatus != EVStatus::EV) ? 1 // 1 = DV
: 2; // 2 = EV
@ -562,6 +564,28 @@ static void CollectCertTelemetry(
rootCert);
GatherCertificateTransparencyTelemetry(rootCert, aEVStatus == EVStatus::EV,
aCertificateTransparencyInfo);
mozilla::glean::tls::certificate_verifications.Add(1);
if (issuerSources.contains(IssuerSource::TLSHandshake)) {
mozilla::glean::verification_used_cert_from::tls_handshake.AddToNumerator(
1);
}
if (issuerSources.contains(IssuerSource::PreloadedIntermediates)) {
mozilla::glean::verification_used_cert_from::preloaded_intermediates
.AddToNumerator(1);
}
if (issuerSources.contains(IssuerSource::ThirdPartyCertificates)) {
mozilla::glean::verification_used_cert_from::third_party_certificates
.AddToNumerator(1);
}
if (issuerSources.contains(IssuerSource::NSSCertDB)) {
mozilla::glean::verification_used_cert_from::nss_cert_db.AddToNumerator(
1);
}
if (issuerSources.contains(IssuerSource::BuiltInRootsModule)) {
mozilla::glean::verification_used_cert_from::built_in_roots_module
.AddToNumerator(1);
}
}
}
@ -594,17 +618,18 @@ Result AuthCertificate(
[](const auto& elementArray) { return elementArray.Clone(); });
}
IssuerSources issuerSources;
Result rv = certVerifier.VerifySSLServerCert(
certBytes, time, aPinArg, aHostName, builtCertChain, certVerifierFlags,
Some(std::move(peerCertsBytes)), stapledOCSPResponse,
sctsFromTLSExtension, dcInfo, aOriginAttributes, &evStatus,
&ocspStaplingStatus, &keySizeStatus, &pinningTelemetryInfo,
&certificateTransparencyInfo, &aIsBuiltCertChainRootBuiltInRoot,
&aMadeOCSPRequests);
&aMadeOCSPRequests, &issuerSources);
CollectCertTelemetry(rv, evStatus, ocspStaplingStatus, keySizeStatus,
pinningTelemetryInfo, builtCertChain,
certificateTransparencyInfo);
certificateTransparencyInfo, issuerSources);
return rv;
}

Просмотреть файл

@ -1,7 +1,6 @@
# 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/.
# Adding a new metric? We have docs for that!
# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
@ -73,3 +72,78 @@ oskeystore:
- available
- encrypt
- decrypt
tls:
certificate_verifications:
type: counter
description: >
The total number of successful TLS server certificate verifications.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
notification_emails:
- dkeeler@mozilla.com
expires: never
verification_used_cert_from:
tls_handshake:
type: rate
description: >
How many successfully-built certificate chains used a certificate from the TLS handshake.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
notification_emails:
- dkeeler@mozilla.com
expires: never
denominator_metric: tls.certificate_verifications
preloaded_intermediates:
type: rate
description: >
How many successfully-built certificate chains used a certificate from preloaded intermediates.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
notification_emails:
- dkeeler@mozilla.com
expires: never
denominator_metric: tls.certificate_verifications
third_party_certificates:
type: rate
description: >
How many successfully-built certificate chains used a third-party certificate from the OS.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
notification_emails:
- dkeeler@mozilla.com
expires: never
denominator_metric: tls.certificate_verifications
nss_cert_db:
type: rate
description: >
How many successfully-built certificate chains used a certificate from the NSS cert DB.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
notification_emails:
- dkeeler@mozilla.com
expires: never
denominator_metric: tls.certificate_verifications
built_in_roots_module:
type: rate
description: >
How many successfully-built certificate chains used a certificate from the built-in roots module.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1876435
notification_emails:
- dkeeler@mozilla.com
expires: never
denominator_metric: tls.certificate_verifications