Bug 1320566 - Certificate Transparency - implement CT Policy. r=Dolske,keeler
authorSergei Chernov <sergei.cv@ndivi.com>
Mon, 09 Jan 2017 08:22:28 +0200
changeset 377825 9faf10e794e61d45e42246f101681ceb9f10cced
parent 377824 f9f4879c272783c17a3e50a50437fff1e68212ec
child 377826 8709d89ccc0e13fad3cf28febd036b8072a62ab4
push id7198
push userjlorenzo@mozilla.com
push dateTue, 18 Apr 2017 12:07:49 +0000
treeherdermozilla-beta@d57aa49c3948 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersDolske, keeler
bugs1320566
milestone54.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1320566 - Certificate Transparency - implement CT Policy. r=Dolske,keeler MozReview-Commit-ID: LcMdKcgBStG
browser/base/content/pageinfo/security.js
security/certverifier/CTDiversityPolicy.cpp
security/certverifier/CTDiversityPolicy.h
security/certverifier/CTLog.h
security/certverifier/CTLogVerifier.h
security/certverifier/CTPolicyEnforcer.cpp
security/certverifier/CTPolicyEnforcer.h
security/certverifier/CertVerifier.cpp
security/certverifier/CertVerifier.h
security/certverifier/moz.build
security/certverifier/tests/gtest/CTDiversityPolicyTest.cpp
security/certverifier/tests/gtest/CTPolicyEnforcerTest.cpp
security/certverifier/tests/gtest/moz.build
security/manager/locales/en-US/chrome/pippki/pippki.properties
security/manager/ssl/SSLServerCertVerification.cpp
security/manager/ssl/nsISSLStatus.idl
security/manager/ssl/nsSSLStatus.cpp
--- a/browser/base/content/pageinfo/security.js
+++ b/browser/base/content/pageinfo/security.js
@@ -87,34 +87,28 @@ var security = {
         case nsISSLStatus.TLS_VERSION_1_2:
           retval.version = "TLS 1.2"
           break;
         case nsISSLStatus.TLS_VERSION_1_3:
           retval.version = "TLS 1.3"
           break;
       }
 
-      // Select status text to display for Certificate Transparency.
+      // Select the status text to display for Certificate Transparency.
+      // Since we do not yet enforce the CT Policy on secure connections,
+      // we must not complain on policy discompliance (it might be viewed
+      // as a security issue by the user).
       switch (status.certificateTransparencyStatus) {
         case nsISSLStatus.CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE:
-          // CT compliance checks were not performed,
-          // do not display any status text.
+        case nsISSLStatus.CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS:
+        case nsISSLStatus.CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS:
           retval.certificateTransparency = null;
           break;
-        case nsISSLStatus.CERTIFICATE_TRANSPARENCY_NONE:
-          retval.certificateTransparency = "None";
-          break;
-        case nsISSLStatus.CERTIFICATE_TRANSPARENCY_OK:
-          retval.certificateTransparency = "OK";
-          break;
-        case nsISSLStatus.CERTIFICATE_TRANSPARENCY_UNKNOWN_LOG:
-          retval.certificateTransparency = "UnknownLog";
-          break;
-        case nsISSLStatus.CERTIFICATE_TRANSPARENCY_INVALID:
-          retval.certificateTransparency = "Invalid";
+        case nsISSLStatus.CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT:
+          retval.certificateTransparency = "Compliant";
           break;
       }
 
       return retval;
     }
     return {
       hostName,
       cAName : "",
new file mode 100644
--- /dev/null
+++ b/security/certverifier/CTDiversityPolicy.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "CTDiversityPolicy.h"
+
+namespace mozilla { namespace ct {
+
+typedef mozilla::pkix::Result Result;
+
+Result
+GetCTLogOperatorsFromVerifiedSCTList(const VerifiedSCTList& list,
+                                     CTLogOperatorList& operators)
+{
+  operators.clear();
+  for (const VerifiedSCT& verifiedSct : list) {
+    CTLogOperatorId sctLogOperatorId = verifiedSct.logOperatorId;
+    bool alreadyAdded = false;
+    for (CTLogOperatorId id : operators) {
+      if (id == sctLogOperatorId) {
+        alreadyAdded = true;
+        break;
+      }
+    }
+    if (!alreadyAdded) {
+      if (!operators.append(sctLogOperatorId)) {
+        return Result::FATAL_ERROR_NO_MEMORY;
+      }
+    }
+  }
+  return Success;
+}
+
+Result
+CTDiversityPolicy::GetDependentOperators(const UniqueCERTCertList& builtChain,
+                                         const CTLogOperatorList& operators,
+                                         CTLogOperatorList& dependentOperators)
+{
+  return Success;
+}
+
+} } // namespace mozilla::ct
new file mode 100644
--- /dev/null
+++ b/security/certverifier/CTDiversityPolicy.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef CTDiversityPolicy_h
+#define CTDiversityPolicy_h
+
+#include "CTLog.h"
+#include "CTVerifyResult.h"
+#include "mozilla/Vector.h"
+#include "pkix/Result.h"
+#include "ScopedNSSTypes.h"
+
+namespace mozilla { namespace ct {
+
+// Retuns the list of unique CT log operator IDs appearing in the provided
+// list of verified SCTs.
+pkix::Result GetCTLogOperatorsFromVerifiedSCTList(const VerifiedSCTList& list,
+                                                  CTLogOperatorList& operators);
+
+// Helper class used by CTPolicyEnforcer to check the CT log operators
+// diversity requirements of the CT Policy.
+// See CTPolicyEnforcer.h for more details.
+class CTDiversityPolicy
+{
+public:
+  // Given a certificate chain and a set of CT log operators,
+  // returns the subset of log operators that are dependent on the CA
+  // issuing the certificate (as defined by the CT Policy).
+  //
+  // NOTE: TBD, PENDING FINALIZATION OF MOZILLA CT POLICY.
+  pkix::Result GetDependentOperators(const UniqueCERTCertList& builtChain,
+                                     const CTLogOperatorList& operators,
+                                     CTLogOperatorList& dependentOperators);
+};
+
+} } // namespace mozilla::ct
+
+#endif // CTDiversityPolicy_h
--- a/security/certverifier/CTLog.h
+++ b/security/certverifier/CTLog.h
@@ -4,24 +4,28 @@
  * 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/. */
 
 #ifndef CTLog_h
 #define CTLog_h
 
 #include <stdint.h>
 
+#include "mozilla/Vector.h"
+
 namespace mozilla { namespace ct {
 
 // Signed integer sufficient to store the numeric ID of CT log operators
 // as assigned at https://www.certificate-transparency.org/known-logs .
 // The assigned IDs are 0-based positive integers, so you can use special
 // values (such as -1) to indicate a "null" or unknown log ID.
 typedef int16_t CTLogOperatorId;
 
+typedef Vector<CTLogOperatorId, 8> CTLogOperatorList;
+
 // Current status of a CT log in regard to its inclusion in the
 // Known Logs List such as https://www.certificate-transparency.org/known-logs
 enum class CTLogStatus
 {
   // Status unknown or unavailable.
   Unknown,
   // Included in the list of known logs.
   Included,
--- a/security/certverifier/CTLogVerifier.h
+++ b/security/certverifier/CTLogVerifier.h
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef CTLogVerifier_h
 #define CTLogVerifier_h
 
 #include "CTLog.h"
 #include "pkix/Input.h"
 #include "pkix/pkix.h"
+#include "pkix/Result.h"
 #include "SignedCertificateTimestamp.h"
 #include "SignedTreeHead.h"
 
 namespace mozilla { namespace ct {
 
 // Verifies Signed Certificate Timestamps (SCTs) provided by a specific log
 // using the public key of that log. Assumes the SCT being verified
 // matches the log by log key ID and signature parameters (an error is returned
new file mode 100644
--- /dev/null
+++ b/security/certverifier/CTPolicyEnforcer.cpp
@@ -0,0 +1,357 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "CTPolicyEnforcer.h"
+
+#include <algorithm>
+#include <stdint.h>
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Vector.h"
+
+extern mozilla::LazyLogModule gCertVerifierLog;
+
+namespace mozilla { namespace ct {
+
+using namespace mozilla::pkix;
+
+// Returns the number of embedded SCTs required to be present on a
+// certificate for Qualification Case #2 (embedded SCTs).
+static size_t
+GetRequiredEmbeddedSctsCount(size_t certLifetimeInFullCalendarMonths)
+{
+  // "there are Embedded SCTs from AT LEAST N+1 once or currently qualified
+  // logs, where N is the lifetime of the certificate in years (normally
+  // rounding up, but rounding down when up to 3 months over), and must be
+  // at least 1"
+  return 1 + (certLifetimeInFullCalendarMonths + 9) / 12;
+}
+
+// Whether a valid embedded SCT is present in the list.
+static bool
+HasValidEmbeddedSct(const VerifiedSCTList& verifiedScts)
+{
+  for (const VerifiedSCT& verifiedSct : verifiedScts) {
+    if (verifiedSct.status == VerifiedSCT::Status::Valid &&
+        verifiedSct.origin == VerifiedSCT::Origin::Embedded) {
+      return true;
+    }
+  }
+  return false;
+}
+
+// Whether a valid non-embedded SCT is present in the list.
+static bool
+HasValidNonEmbeddedSct(const VerifiedSCTList& verifiedScts)
+{
+  for (const VerifiedSCT& verifiedSct : verifiedScts) {
+    if (verifiedSct.status == VerifiedSCT::Status::Valid &&
+         (verifiedSct.origin == VerifiedSCT::Origin::TLSExtension ||
+          verifiedSct.origin == VerifiedSCT::Origin::OCSPResponse)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+// Given a list of verified SCTs, counts the number of distinct CA-independent
+// log operators running the CT logs that issued the SCTs which satisfy
+// the provided boolean predicate.
+template <typename SelectFunc>
+static Result
+CountIndependentLogOperatorsForSelectedScts(const VerifiedSCTList& verifiedScts,
+  const CTLogOperatorList& dependentOperators,
+  size_t& count,
+  SelectFunc selected)
+{
+  CTLogOperatorList operatorIds;
+  for (const VerifiedSCT& verifiedSct : verifiedScts) {
+    CTLogOperatorId sctLogOperatorId = verifiedSct.logOperatorId;
+    // Check if |sctLogOperatorId| is CA-dependent.
+    bool isDependentOperator = false;
+    for (CTLogOperatorId dependentOperator : dependentOperators) {
+      if (sctLogOperatorId == dependentOperator) {
+        isDependentOperator = true;
+        break;
+      }
+    }
+    if (isDependentOperator || !selected(verifiedSct)) {
+      continue;
+    }
+    // Check if |sctLogOperatorId| is in |operatorIds|...
+    bool alreadyAdded = false;
+    for (CTLogOperatorId id : operatorIds) {
+      if (id == sctLogOperatorId) {
+        alreadyAdded = true;
+        break;
+      }
+    }
+    // ...and if not, add it.
+    if (!alreadyAdded) {
+      if (!operatorIds.append(sctLogOperatorId)) {
+        return Result::FATAL_ERROR_NO_MEMORY;
+      }
+    }
+  }
+  count = operatorIds.length();
+  return Success;
+}
+
+// Given a list of verified SCTs, counts the number of distinct CT logs
+// that issued the SCTs that satisfy the |selected| predicate.
+template <typename SelectFunc>
+static Result
+CountLogsForSelectedScts(const VerifiedSCTList& verifiedScts,
+                         size_t& count,
+                         SelectFunc selected)
+{
+  // Keep pointers to log ids (of type Buffer) from |verifiedScts| to save on
+  // memory allocations.
+  Vector<const Buffer*, 8> logIds;
+  for (const VerifiedSCT& verifiedSct : verifiedScts) {
+    if (!selected(verifiedSct)) {
+      continue;
+    }
+
+    const Buffer* sctLogId = &verifiedSct.sct.logId;
+    // Check if |sctLogId| points to data already in |logIds|...
+    bool alreadyAdded = false;
+    for (const Buffer* logId : logIds) {
+      if (*logId == *sctLogId) {
+        alreadyAdded = true;
+        break;
+      }
+    }
+    // ...and if not, add it.
+    if (!alreadyAdded) {
+      if (!logIds.append(sctLogId)) {
+        return Result::FATAL_ERROR_NO_MEMORY;
+      }
+    }
+  }
+  count = logIds.length();
+  return Success;
+}
+
+// Calculates the effective issuance time of connection's certificate using
+// the SCTs present on the connection (we can't rely on notBefore validity
+// field of the certificate since it can be backdated).
+// Used to determine whether to accept SCTs issued by past qualified logs.
+// The effective issuance time is defined as the earliest of all SCTs,
+// rather than the latest of embedded SCTs, in order to give CAs the benefit
+// of the doubt in the event a log is revoked in the midst of processing
+// a precertificate and issuing the certificate.
+// It is acceptable to ignore the origin of the SCTs because SCTs
+// delivered via OCSP/TLS extension will cover the full certificate,
+// which necessarily will exist only after the precertificate
+// has been logged and the actual certificate issued.
+static uint64_t
+GetEffectiveCertIssuanceTime(const VerifiedSCTList& verifiedScts)
+{
+  uint64_t result = UINT64_MAX;
+  for (const VerifiedSCT& verifiedSct : verifiedScts) {
+    if (verifiedSct.status == VerifiedSCT::Status::Valid) {
+      result = std::min(result, verifiedSct.sct.timestamp);
+    }
+  }
+  return result;
+}
+
+// Checks if the log that issued the given SCT is "once or currently qualified"
+// (i.e. was qualified at the time of the certificate issuance). In addition,
+// makes sure the SCT is before the disqualification.
+static bool
+LogWasQualifiedForSct(const VerifiedSCT& verifiedSct, uint64_t certIssuanceTime)
+{
+  if (verifiedSct.status == VerifiedSCT::Status::Valid) {
+    return true;
+  }
+  if (verifiedSct.status == VerifiedSCT::Status::ValidFromDisqualifiedLog) {
+    uint64_t logDisqualificationTime = verifiedSct.logDisqualificationTime;
+    return certIssuanceTime < logDisqualificationTime &&
+      verifiedSct.sct.timestamp < logDisqualificationTime;
+  }
+  return false;
+}
+
+// "A certificate is CT Qualified if it is presented with at least two SCTs
+// from once or currently qualified logs run by a minimum of two entities
+// independent of the CA and of each other."
+// By the grandfathering provision (not currently implemented), certificates
+// "are CT Qualified if they are presented with SCTs from once or
+// currently qualified logs run by a minimum of one entity independent
+// of the CA."
+static Result
+CheckOperatorDiversityCompliance(const VerifiedSCTList& verifiedScts,
+                                 uint64_t certIssuanceTime,
+                                 const CTLogOperatorList& dependentOperators,
+                                 bool& compliant)
+{
+  size_t independentOperatorsCount;
+  Result rv = CountIndependentLogOperatorsForSelectedScts(verifiedScts,
+    dependentOperators, independentOperatorsCount,
+    [certIssuanceTime](const VerifiedSCT& verifiedSct)->bool {
+      return LogWasQualifiedForSct(verifiedSct, certIssuanceTime);
+    });
+  if (rv != Success) {
+    return rv;
+  }
+  // Having at least 2 operators implies we have at least 2 SCTs.
+  // For the grandfathering provision (1 operator) we will need to include
+  // an additional SCTs count check using
+  // rv = CountLogsForSelectedScts(verifiedScts, sctsCount,
+  //   [certIssuanceTime](const VerifiedSCT& verifiedSct)->bool {
+  //     return LogWasQualifiedForSct(verifiedSct, certIssuanceTime);
+  // });
+  compliant = independentOperatorsCount >= 2;
+  return Success;
+}
+
+// Qualification Case #1 (non-embedded SCTs) - the following must hold:
+// a. An SCT from a log qualified at the time of check is presented via the
+// TLS extension OR is embedded within a stapled OCSP response;
+// AND
+// b. There are at least two SCTs from logs qualified at the time of check,
+// presented via any method.
+static Result
+CheckNonEmbeddedCompliance(const VerifiedSCTList& verifiedScts, bool& compliant)
+{
+  if (!HasValidNonEmbeddedSct(verifiedScts)) {
+    compliant = false;
+    return Success;
+  }
+
+  size_t validSctsCount;
+  Result rv = CountLogsForSelectedScts(verifiedScts, validSctsCount,
+    [](const VerifiedSCT& verifiedSct)->bool {
+      return verifiedSct.status == VerifiedSCT::Status::Valid;
+    });
+  if (rv != Success) {
+    return rv;
+  }
+
+  MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+          ("CT Policy non-embedded case status: validSCTs=%zu\n",
+           validSctsCount));
+  compliant = validSctsCount >= 2;
+  return Success;
+}
+
+// Qualification Case #2 (embedded SCTs) - the following must hold:
+// a. An Embedded SCT from a log qualified at the time of check is presented;
+// AND
+// b. There are Embedded SCTs from AT LEAST N + 1 once or currently qualified
+// logs, where N is the lifetime of the certificate in years (normally
+// rounding up, but rounding down when up to 3 months over), and must be
+// at least 1.
+static Result
+CheckEmbeddedCompliance(const VerifiedSCTList& verifiedScts,
+                        size_t certLifetimeInCalendarMonths,
+                        uint64_t certIssuanceTime,
+                        bool& compliant)
+{
+  if (!HasValidEmbeddedSct(verifiedScts)) {
+    compliant = false;
+    return Success;
+  }
+
+  // Count the compliant embedded SCTs. Only a single SCT from each log
+  // is accepted. Note that a given log might return several different SCTs
+  // for the same precertificate (it is permitted, but advised against).
+  size_t embeddedSctsCount;
+  Result rv = CountLogsForSelectedScts(verifiedScts, embeddedSctsCount,
+    [certIssuanceTime](const VerifiedSCT& verifiedSct)->bool {
+      return verifiedSct.origin == VerifiedSCT::Origin::Embedded &&
+        LogWasQualifiedForSct(verifiedSct, certIssuanceTime);
+  });
+  if (rv != Success) {
+    return rv;
+  }
+
+  size_t requiredSctsCount =
+    GetRequiredEmbeddedSctsCount(certLifetimeInCalendarMonths);
+
+  MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+          ("CT Policy embedded case status: "
+           "requiredSCTs=%zu embeddedSCTs=%zu\n",
+           requiredSctsCount, embeddedSctsCount));
+
+  compliant = embeddedSctsCount >= requiredSctsCount;
+  return Success;
+}
+
+Result
+CTPolicyEnforcer::CheckCompliance(const VerifiedSCTList& verifiedScts,
+                                  size_t certLifetimeInCalendarMonths,
+                                  const CTLogOperatorList& dependentOperators,
+                                  CTPolicyCompliance& compliance)
+{
+  uint64_t certIssuanceTime = GetEffectiveCertIssuanceTime(verifiedScts);
+
+  bool diversityOK;
+  Result rv = CheckOperatorDiversityCompliance(verifiedScts, certIssuanceTime,
+                                               dependentOperators,
+                                               diversityOK);
+  if (rv != Success) {
+    return rv;
+  }
+  if (diversityOK) {
+    MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+            ("CT Policy: diversity satisfied\n"));
+  }
+
+  bool nonEmbeddedCaseOK;
+  rv = CheckNonEmbeddedCompliance(verifiedScts, nonEmbeddedCaseOK);
+  if (rv != Success) {
+    return rv;
+  }
+  if (nonEmbeddedCaseOK) {
+    MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+            ("CT Policy: non-embedded case satisfied)\n"));
+  }
+
+  bool embeddedCaseOK;
+  rv = CheckEmbeddedCompliance(verifiedScts, certLifetimeInCalendarMonths,
+                               certIssuanceTime, embeddedCaseOK);
+  if (rv != Success) {
+    return rv;
+  }
+  if (embeddedCaseOK) {
+    MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+            ("CT Policy: embedded case satisfied\n"));
+  }
+
+  if (nonEmbeddedCaseOK || embeddedCaseOK) {
+    compliance = diversityOK ? CTPolicyCompliance::Compliant
+                             : CTPolicyCompliance::NotDiverseScts;
+  } else {
+    // Not enough SCTs are present to satisfy either case of the policy.
+    compliance = CTPolicyCompliance::NotEnoughScts;
+  }
+
+  switch (compliance) {
+    case CTPolicyCompliance::Compliant:
+      MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+                ("CT Policy compliance: Compliant\n"));
+      break;
+    case CTPolicyCompliance::NotEnoughScts:
+      MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+                ("CT Policy compliance: NotEnoughScts\n"));
+      break;
+    case CTPolicyCompliance::NotDiverseScts:
+      MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+                ("CT Policy compliance: NotDiverseScts\n"));
+      break;
+    case CTPolicyCompliance::Unknown:
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unexpected CTPolicyCompliance type");
+  }
+  return Success;
+}
+
+} } // namespace mozilla::ct
new file mode 100644
--- /dev/null
+++ b/security/certverifier/CTPolicyEnforcer.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef CTPolicyEnforcer_h
+#define CTPolicyEnforcer_h
+
+#include "CTLog.h"
+#include "CTVerifyResult.h"
+#include "pkix/Result.h"
+
+namespace mozilla { namespace ct {
+
+// Information about the compliance of the TLS connection with the
+// Certificate Transparency policy.
+enum class CTPolicyCompliance
+{
+  // Compliance not checked or not applicable.
+  Unknown,
+  // The connection complied with the certificate policy
+  // by including SCTs that satisfy the policy.
+  Compliant,
+  // The connection did not have enough valid SCTs to comply.
+  NotEnoughScts,
+  // The connection had enough valid SCTs, but the diversity requirement
+  // was not met (the number of CT log operators independent of the CA
+  // and of each other is too low).
+  NotDiverseScts,
+};
+
+// Checks whether a TLS connection complies with the current CT policy.
+// The implemented policy is based on the Mozilla CT Policy draft 0.1.0
+// (see https://docs.google.com/document/d/
+// 1rnqYYwscAx8WhS-MCdTiNzYQus9e37HuVyafQvEeNro/edit).
+//
+// NOTE: CT DIVERSITY REQUIREMENT IS TBD, PENDING FINALIZATION
+// OF MOZILLA CT POLICY. Specifically:
+// 1. CT log operators being CA-dependent is not currently taken into account
+// (see CTDiversityPolicy.h).
+// 2. The grandfathering provision of the operator diversity requirement
+// is not implemented (see "CT Qualified" section of the policy and
+// CheckOperatorDiversityCompliance in CTPolicyEnforcer.cpp).
+class CTPolicyEnforcer
+{
+public:
+  // |verifiedSct| - SCTs present on the connection along with their
+  // verification status.
+  // |certLifetimeInCalendarMonths| - certificate lifetime in full calendar
+  // months (i.e. rounded down), based on the notBefore/notAfter fields.
+  // |dependentOperators| - which CT log operators are dependent on the CA
+  // that issued the certificate. SCTs issued by logs associated with such
+  // operators are treated differenly when evaluating the policy.
+  // See CTDiversityPolicy class.
+  // |compliance| - the result of the compliance check.
+  pkix::Result CheckCompliance(const VerifiedSCTList& verifiedScts,
+                               size_t certLifetimeInCalendarMonths,
+                               const CTLogOperatorList& dependentOperators,
+                               CTPolicyCompliance& compliance);
+};
+
+} } // namespace mozilla::ct
+
+#endif // CTPolicyEnforcer_h
--- a/security/certverifier/CertVerifier.cpp
+++ b/security/certverifier/CertVerifier.cpp
@@ -3,16 +3,17 @@
 /* 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/. */
 
 #include "CertVerifier.h"
 
 #include <stdint.h>
 
+#include "CTDiversityPolicy.h"
 #include "CTKnownLogs.h"
 #include "CTLogVerifier.h"
 #include "ExtendedValidation.h"
 #include "MultiLogCTVerifier.h"
 #include "NSSCertDBTrustDomain.h"
 #include "NSSErrorsService.h"
 #include "cert.h"
 #include "mozilla/Assertions.h"
@@ -25,22 +26,66 @@
 #include "secmod.h"
 
 using namespace mozilla::ct;
 using namespace mozilla::pkix;
 using namespace mozilla::psm;
 
 mozilla::LazyLogModule gCertVerifierLog("certverifier");
 
+// Returns the certificate validity period in calendar months (rounded down).
+// "extern" to allow unit tests in CTPolicyEnforcerTest.cpp.
+extern mozilla::pkix::Result
+GetCertLifetimeInFullMonths(PRTime certNotBefore,
+                            PRTime certNotAfter,
+                            size_t& months)
+{
+  if (certNotBefore >= certNotAfter) {
+    MOZ_ASSERT_UNREACHABLE("Expected notBefore < notAfter");
+    return mozilla::pkix::Result::FATAL_ERROR_INVALID_ARGS;
+  }
+
+  PRExplodedTime explodedNotBefore;
+  PRExplodedTime explodedNotAfter;
+
+  PR_ExplodeTime(certNotBefore, PR_LocalTimeParameters, &explodedNotBefore);
+  PR_ExplodeTime(certNotAfter, PR_LocalTimeParameters, &explodedNotAfter);
+
+  PRInt32 signedMonths =
+    (explodedNotAfter.tm_year - explodedNotBefore.tm_year) * 12 +
+    (explodedNotAfter.tm_month - explodedNotBefore.tm_month);
+  if (explodedNotAfter.tm_mday < explodedNotBefore.tm_mday) {
+    --signedMonths;
+  }
+
+  // Can't use `mozilla::AssertedCast<size_t>(signedMonths)` below
+  // since it currently generates a warning on Win x64 debug.
+  if (signedMonths < 0) {
+    MOZ_ASSERT_UNREACHABLE("Expected explodedNotBefore < explodedNotAfter");
+    return mozilla::pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+  }
+  months = static_cast<size_t>(signedMonths);
+
+  return Success;
+}
+
 namespace mozilla { namespace psm {
 
 const CertVerifier::Flags CertVerifier::FLAG_LOCAL_ONLY = 1;
 const CertVerifier::Flags CertVerifier::FLAG_MUST_BE_EV = 2;
 const CertVerifier::Flags CertVerifier::FLAG_TLS_IGNORE_STATUS_REQUEST = 4;
 
+void
+CertificateTransparencyInfo::Reset()
+{
+  enabled = false;
+  verifyResult.Reset();
+  policyCompliance = CTPolicyCompliance::Unknown;
+}
+
 CertVerifier::CertVerifier(OcspDownloadConfig odc,
                            OcspStrictConfig osc,
                            OcspGetConfig ogc,
                            uint32_t certShortLifetimeInDays,
                            PinningMode pinningMode,
                            SHA1Mode sha1Mode,
                            BRNameMatchingPolicy::Mode nameMatchingMode,
                            NetscapeStepUpPolicy netscapeStepUpPolicy,
@@ -174,20 +219,23 @@ CertVerifier::LoadKnownCTLogs()
     }
 
     rv = mCTVerifier->AddLog(Move(logVerifier));
     if (rv != Success) {
       MOZ_ASSERT_UNREACHABLE("Failed activating a known CT Log");
       continue;
     }
   }
+  // TBD: Initialize mCTDiversityPolicy with the CA dependency map
+  // of the known CT logs operators.
+  mCTDiversityPolicy = MakeUnique<CTDiversityPolicy>();
 }
 
 Result
-CertVerifier::VerifySignedCertificateTimestamps(
+CertVerifier::VerifyCertificateTransparencyPolicy(
   NSSCertDBTrustDomain& trustDomain, const UniqueCERTCertList& builtChain,
   Input sctsFromTLS, Time time,
   /*optional out*/ CertificateTransparencyInfo* ctInfo)
 {
   if (ctInfo) {
     ctInfo->Reset();
   }
   if (mCTMode == CertificateTransparencyMode::Disabled) {
@@ -196,40 +244,33 @@ CertVerifier::VerifySignedCertificateTim
   if (ctInfo) {
     ctInfo->enabled = true;
   }
 
   if (!builtChain || CERT_LIST_EMPTY(builtChain)) {
     return Result::FATAL_ERROR_INVALID_ARGS;
   }
 
-  bool gotScts = false;
   Input embeddedSCTs = trustDomain.GetSCTListFromCertificate();
   if (embeddedSCTs.GetLength() > 0) {
-    gotScts = true;
     MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
             ("Got embedded SCT data of length %zu\n",
               static_cast<size_t>(embeddedSCTs.GetLength())));
   }
   Input sctsFromOCSP = trustDomain.GetSCTListFromOCSPStapling();
   if (sctsFromOCSP.GetLength() > 0) {
-    gotScts = true;
     MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
             ("Got OCSP SCT data of length %zu\n",
               static_cast<size_t>(sctsFromOCSP.GetLength())));
   }
   if (sctsFromTLS.GetLength() > 0) {
-    gotScts = true;
     MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
             ("Got TLS SCT data of length %zu\n",
               static_cast<size_t>(sctsFromTLS.GetLength())));
   }
-  if (!gotScts) {
-    return Success;
-  }
 
   CERTCertListNode* endEntityNode = CERT_LIST_HEAD(builtChain);
   if (!endEntityNode) {
     return Result::FATAL_ERROR_INVALID_ARGS;
   }
   CERTCertListNode* issuerNode = CERT_LIST_NEXT(endEntityNode);
   if (!issuerNode) {
     // Issuer certificate is required for SCT verification.
@@ -237,16 +278,22 @@ CertVerifier::VerifySignedCertificateTim
   }
 
   CERTCertificate* endEntity = endEntityNode->cert;
   CERTCertificate* issuer = issuerNode->cert;
   if (!endEntity || !issuer) {
     return Result::FATAL_ERROR_INVALID_ARGS;
   }
 
+  if (endEntity->subjectName) {
+    MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+            ("Verifying CT Policy compliance of subject %s\n",
+             endEntity->subjectName));
+  }
+
   Input endEntityDER;
   Result rv = endEntityDER.Init(endEntity->derCert.data,
                                 endEntity->derCert.len);
   if (rv != Success) {
     return rv;
   }
 
   Input issuerPublicKeyDER;
@@ -299,19 +346,54 @@ CertVerifier::VerifySignedCertificateTim
              "valid=%zu unknownLog=%zu disqualifiedLog=%zu "
              "invalidSignature=%zu invalidTimestamp=%zu "
              "decodingErrors=%zu\n",
              validCount, unknownLogCount, disqualifiedLogCount,
              invalidSignatureCount, invalidTimestampCount,
              result.decodingErrors));
   }
 
+  PRTime notBefore;
+  PRTime notAfter;
+  if (CERT_GetCertTimes(endEntity, &notBefore, &notAfter) != SECSuccess) {
+    return Result::FATAL_ERROR_LIBRARY_FAILURE;
+  }
+  size_t lifetimeInMonths;
+  rv = GetCertLifetimeInFullMonths(notBefore, notAfter, lifetimeInMonths);
+  if (rv != Success) {
+    return rv;
+  }
+
+  CTLogOperatorList allOperators;
+  rv = GetCTLogOperatorsFromVerifiedSCTList(result.verifiedScts,
+                                            allOperators);
+  if (rv != Success) {
+    return rv;
+  }
+
+  CTLogOperatorList dependentOperators;
+  rv = mCTDiversityPolicy->GetDependentOperators(builtChain, allOperators,
+                                                 dependentOperators);
+  if (rv != Success) {
+    return rv;
+  }
+
+  CTPolicyEnforcer ctPolicyEnforcer;
+  CTPolicyCompliance ctPolicyCompliance;
+  rv = ctPolicyEnforcer.CheckCompliance(result.verifiedScts, lifetimeInMonths,
+                                        dependentOperators, ctPolicyCompliance);
+  if (rv != Success) {
+    MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+            ("CT policy check failed with fatal error %i\n", rv));
+    return rv;
+  }
+
   if (ctInfo) {
-    ctInfo->processedSCTs = true;
     ctInfo->verifyResult = Move(result);
+    ctInfo->policyCompliance = ctPolicyCompliance;
   }
   return Success;
 }
 
 bool
 CertVerifier::SHA1ModeMoreRestrictiveThanGivenMode(SHA1Mode mode)
 {
   switch (mSHA1Mode) {
@@ -533,19 +615,19 @@ CertVerifier::VerifyCert(CERTCertificate
           MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
                   ("cert is EV with status %i\n", sha1ModeResults[i]));
           if (evOidPolicy) {
             *evOidPolicy = evPolicyOidTag;
           }
           if (sha1ModeResult) {
             *sha1ModeResult = sha1ModeResults[i];
           }
-          rv = VerifySignedCertificateTimestamps(trustDomain, builtChain,
-                                                 sctsFromTLSInput, time,
-                                                 ctInfo);
+          rv = VerifyCertificateTransparencyPolicy(trustDomain, builtChain,
+                                                   sctsFromTLSInput, time,
+                                                   ctInfo);
           if (rv != Success) {
             break;
           }
         }
       }
       if (rv == Success) {
         break;
       }
@@ -620,19 +702,19 @@ CertVerifier::VerifyCert(CERTCertificate
           }
           if (rv == Success) {
             if (keySizeStatus) {
               *keySizeStatus = keySizeStatuses[i];
             }
             if (sha1ModeResult) {
               *sha1ModeResult = sha1ModeResults[j];
             }
-            rv = VerifySignedCertificateTimestamps(trustDomain, builtChain,
-                                                   sctsFromTLSInput, time,
-                                                   ctInfo);
+            rv = VerifyCertificateTransparencyPolicy(trustDomain, builtChain,
+                                                     sctsFromTLSInput, time,
+                                                     ctInfo);
             if (rv != Success) {
               break;
             }
           }
         }
       }
 
       if (rv == Success) {
--- a/security/certverifier/CertVerifier.h
+++ b/security/certverifier/CertVerifier.h
@@ -3,16 +3,17 @@
 /* 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/. */
 
 #ifndef CertVerifier_h
 #define CertVerifier_h
 
 #include "BRNameMatchingPolicy.h"
+#include "CTPolicyEnforcer.h"
 #include "CTVerifyResult.h"
 #include "OCSPCache.h"
 #include "ScopedNSSTypes.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/UniquePtr.h"
 #include "pkix/pkixtypes.h"
 
 #if defined(_MSC_VER)
@@ -23,20 +24,21 @@
 #endif /* defined(_MSC_VER) */
 #include "mozilla/BasePrincipal.h"
 #if defined(_MSC_VER)
 #pragma warning(pop) /* popping the pragma in this file */
 #endif /* defined(_MSC_VER) */
 
 namespace mozilla { namespace ct {
 
-// Including MultiLogCTVerifier.h would bring along all of its dependent
-// headers and force us to export them in moz.build. Just forward-declare
-// the class here instead.
+// Including the headers of the classes below would bring along all of their
+// dependent headers and force us to export them in moz.build.
+// Just forward-declare the classes here instead.
 class MultiLogCTVerifier;
+class CTDiversityPolicy;
 
 } } // namespace mozilla::ct
 
 namespace mozilla { namespace psm {
 
 typedef mozilla::pkix::Result Result;
 
 // These values correspond to the CERT_CHAIN_KEY_SIZE_STATUS telemetry.
@@ -73,22 +75,22 @@ public:
   void Reset() { accumulateForRoot = false; accumulateResult = false; }
 };
 
 class CertificateTransparencyInfo
 {
 public:
   // Was CT enabled?
   bool enabled;
-  // Did we receive and process any binary SCT data from the supported sources?
-  bool processedSCTs;
   // Verification result of the processed SCTs.
   mozilla::ct::CTVerifyResult verifyResult;
+  // Connection compliance to the CT Policy.
+  mozilla::ct::CTPolicyCompliance policyCompliance;
 
-  void Reset() { enabled = false; processedSCTs = false; verifyResult.Reset(); }
+  void Reset();
 };
 
 class NSSCertDBTrustDomain;
 
 class CertVerifier
 {
 public:
   typedef unsigned int Flags;
@@ -197,22 +199,23 @@ public:
   const SHA1Mode mSHA1Mode;
   const BRNameMatchingPolicy::Mode mNameMatchingMode;
   const NetscapeStepUpPolicy mNetscapeStepUpPolicy;
   const CertificateTransparencyMode mCTMode;
 
 private:
   OCSPCache mOCSPCache;
 
-  // We only have a forward declaration of MultiLogCTVerifier (see above),
-  // so we keep a pointer to it and allocate dynamically.
+  // We only have a forward declarations of these classes (see above)
+  // so we must allocate dynamically.
   UniquePtr<mozilla::ct::MultiLogCTVerifier> mCTVerifier;
+  UniquePtr<mozilla::ct::CTDiversityPolicy> mCTDiversityPolicy;
 
   void LoadKnownCTLogs();
-  mozilla::pkix::Result VerifySignedCertificateTimestamps(
+  mozilla::pkix::Result VerifyCertificateTransparencyPolicy(
                      NSSCertDBTrustDomain& trustDomain,
                      const UniqueCERTCertList& builtChain,
                      mozilla::pkix::Input sctsFromTLS,
                      mozilla::pkix::Time time,
     /*optional out*/ CertificateTransparencyInfo* ctInfo);
 
   // Returns true if the configured SHA1 mode is more restrictive than the given
   // mode. SHA1Mode::Forbidden is more restrictive than any other mode except
--- a/security/certverifier/moz.build
+++ b/security/certverifier/moz.build
@@ -3,27 +3,30 @@
 # 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/.
 
 EXPORTS += [
     'BRNameMatchingPolicy.h',
     'CertVerifier.h',
     'CTLog.h',
+    'CTPolicyEnforcer.h',
     'CTVerifyResult.h',
     'OCSPCache.h',
     'SignedCertificateTimestamp.h',
     'SignedTreeHead.h',
 ]
 
 UNIFIED_SOURCES += [
     'BRNameMatchingPolicy.cpp',
     'CertVerifier.cpp',
+    'CTDiversityPolicy.cpp',
     'CTLogVerifier.cpp',
     'CTObjectsExtractor.cpp',
+    'CTPolicyEnforcer.cpp',
     'CTSerialization.cpp',
     'CTVerifyResult.cpp',
     'MultiLogCTVerifier.cpp',
     'NSSCertDBTrustDomain.cpp',
     'OCSPCache.cpp',
     'OCSPRequestor.cpp',
     'OCSPVerificationTrustDomain.cpp',
     'SignedCertificateTimestamp.cpp',
new file mode 100644
--- /dev/null
+++ b/security/certverifier/tests/gtest/CTDiversityPolicyTest.cpp
@@ -0,0 +1,13 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "CTDiversityPolicy.h"
+
+namespace mozilla { namespace ct {
+
+// TBD, PENDING FINALIZATION OF MOZILLA CT POLICY.
+
+} } // namespace mozilla::ct
new file mode 100644
--- /dev/null
+++ b/security/certverifier/tests/gtest/CTPolicyEnforcerTest.cpp
@@ -0,0 +1,427 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "CTPolicyEnforcer.h"
+
+#include <algorithm>
+#include <stdint.h>
+#include <stdio.h>
+
+#include "CTVerifyResult.h"
+#include "gtest/gtest.h"
+#include "hasht.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "prtime.h"
+#include "SignedCertificateTimestamp.h"
+
+// Implemented in CertVerifier.cpp.
+extern mozilla::pkix::Result
+GetCertLifetimeInFullMonths(PRTime certNotBefore,
+                            PRTime certNotAfter,
+                            size_t& months);
+
+namespace mozilla { namespace ct {
+
+using namespace mozilla::pkix;
+
+class CTPolicyEnforcerTest : public ::testing::Test
+{
+public:
+  void SetUp() override
+  {
+    MOZ_ALWAYS_TRUE(OPERATORS_1_AND_2.append(OPERATOR_1));
+    MOZ_ALWAYS_TRUE(OPERATORS_1_AND_2.append(OPERATOR_2));
+  }
+
+  void GetLogId(Buffer& logId, size_t logNo)
+  {
+    ASSERT_TRUE(logId.resize(SHA256_LENGTH));
+    std::fill(logId.begin(), logId.end(), 0);
+    // Just raw-copy |logId| into the output buffer.
+    MOZ_ASSERT(sizeof(logNo) <= logId.length());
+    memcpy(logId.begin(), &logNo, sizeof(logNo));
+  }
+
+  void AddSct(VerifiedSCTList& verifiedScts,
+              size_t logNo,
+              CTLogOperatorId operatorId,
+              VerifiedSCT::Origin origin,
+              uint64_t timestamp,
+              VerifiedSCT::Status status = VerifiedSCT::Status::Valid)
+  {
+    VerifiedSCT verifiedSct;
+    verifiedSct.status = status;
+    verifiedSct.origin = origin;
+    verifiedSct.logOperatorId = operatorId;
+    verifiedSct.logDisqualificationTime =
+      status == VerifiedSCT::Status::ValidFromDisqualifiedLog ?
+      DISQUALIFIED_AT : UINT64_MAX;
+    verifiedSct.sct.version = SignedCertificateTimestamp::Version::V1;
+    verifiedSct.sct.timestamp = timestamp;
+    Buffer logId;
+    GetLogId(logId, logNo);
+    verifiedSct.sct.logId = Move(logId);
+    ASSERT_TRUE(verifiedScts.append(Move(verifiedSct)));
+  }
+
+  void AddMultipleScts(VerifiedSCTList& verifiedScts,
+                       size_t logsCount,
+                       uint8_t operatorsCount,
+                       VerifiedSCT::Origin origin,
+                       uint64_t timestamp,
+                       VerifiedSCT::Status status = VerifiedSCT::Status::Valid)
+  {
+    for (size_t logNo = 0; logNo < logsCount; logNo++) {
+      CTLogOperatorId operatorId = logNo % operatorsCount;
+      AddSct(verifiedScts, logNo, operatorId, origin, timestamp, status);
+    }
+  }
+
+  void CheckCompliance(const VerifiedSCTList& verifiedSct,
+                       size_t certLifetimeInCalendarMonths,
+                       const CTLogOperatorList& dependentLogOperators,
+                       CTPolicyCompliance expectedCompliance)
+  {
+    CTPolicyCompliance compliance;
+    ASSERT_EQ(Success,
+              mPolicyEnforcer.CheckCompliance(verifiedSct,
+                                              certLifetimeInCalendarMonths,
+                                              dependentLogOperators,
+                                              compliance));
+    EXPECT_EQ(expectedCompliance, compliance);
+  }
+
+protected:
+  CTPolicyEnforcer mPolicyEnforcer;
+
+  const size_t LOG_1 = 1;
+  const size_t LOG_2 = 2;
+  const size_t LOG_3 = 3;
+  const size_t LOG_4 = 4;
+  const size_t LOG_5 = 5;
+
+  const CTLogOperatorId OPERATOR_1 = 1;
+  const CTLogOperatorId OPERATOR_2 = 2;
+  const CTLogOperatorId OPERATOR_3 = 3;
+
+  CTLogOperatorList NO_OPERATORS;
+  CTLogOperatorList OPERATORS_1_AND_2;
+
+  const VerifiedSCT::Origin ORIGIN_EMBEDDED = VerifiedSCT::Origin::Embedded;
+  const VerifiedSCT::Origin ORIGIN_TLS = VerifiedSCT::Origin::TLSExtension;
+  const VerifiedSCT::Origin ORIGIN_OCSP = VerifiedSCT::Origin::OCSPResponse;
+
+  // 4 years of cert lifetime requires 5 SCTs for the embedded case.
+  const size_t DEFAULT_MONTHS = 4 * 12L;
+
+  // Date.parse("2015-08-15T00:00:00Z")
+  const uint64_t TIMESTAMP_1 = 1439596800000L;
+
+  // Date.parse("2016-04-15T00:00:00Z")
+  const uint64_t DISQUALIFIED_AT = 1460678400000L;
+
+  // Date.parse("2016-04-01T00:00:00Z")
+  const uint64_t BEFORE_DISQUALIFIED = 1459468800000L;
+
+  // Date.parse("2016-04-16T00:00:00Z")
+  const uint64_t AFTER_DISQUALIFIED = 1460764800000L;
+};
+
+TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithNonEmbeddedSCTs)
+{
+  VerifiedSCTList scts;
+
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1);
+
+  CheckCompliance(scts,  DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::Compliant);
+}
+
+TEST_F(CTPolicyEnforcerTest, DoesNotConformNotEnoughDiverseNonEmbeddedSCTs)
+{
+  VerifiedSCTList scts;
+
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1);
+
+  CheckCompliance(scts, DEFAULT_MONTHS, OPERATORS_1_AND_2,
+                  CTPolicyCompliance::NotDiverseScts);
+}
+
+TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithEmbeddedSCTs)
+{
+  VerifiedSCTList scts;
+
+  // 5 embedded SCTs required for DEFAULT_MONTHS.
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+  CheckCompliance(scts,  DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::Compliant);
+}
+
+TEST_F(CTPolicyEnforcerTest, DoesNotConformNotEnoughDiverseEmbeddedSCTs)
+{
+  VerifiedSCTList scts;
+
+  // 5 embedded SCTs required for DEFAULT_MONTHS.
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+  CheckCompliance(scts,  DEFAULT_MONTHS, OPERATORS_1_AND_2,
+                  CTPolicyCompliance::NotDiverseScts);
+}
+
+TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithPooledNonEmbeddedSCTs)
+{
+  VerifiedSCTList scts;
+
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_OCSP, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1);
+
+  CheckCompliance(scts,  DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::Compliant);
+}
+
+TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithPooledEmbeddedSCTs)
+{
+  VerifiedSCTList scts;
+
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_OCSP, TIMESTAMP_1);
+
+  CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::Compliant);
+}
+
+TEST_F(CTPolicyEnforcerTest, DoesNotConformToCTPolicyNotEnoughSCTs)
+{
+  VerifiedSCTList scts;
+
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+  CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::NotEnoughScts);
+}
+
+TEST_F(CTPolicyEnforcerTest, DoesNotConformToCTPolicyNotEnoughFreshSCTs)
+{
+  VerifiedSCTList scts;
+
+  // The results should be the same before and after disqualification,
+  // regardless of the delivery method.
+
+  // SCT from before disqualification.
+  scts.clear();
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, BEFORE_DISQUALIFIED,
+         VerifiedSCT::Status::ValidFromDisqualifiedLog);
+  CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::NotEnoughScts);
+  // SCT from after disqualification.
+  scts.clear();
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, AFTER_DISQUALIFIED,
+         VerifiedSCT::Status::ValidFromDisqualifiedLog);
+  CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::NotEnoughScts);
+
+  // Embedded SCT from before disqualification.
+  scts.clear();
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, BEFORE_DISQUALIFIED,
+         VerifiedSCT::Status::ValidFromDisqualifiedLog);
+  CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::NotEnoughScts);
+
+  // Embedded SCT from after disqualification.
+  scts.clear();
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED,
+         VerifiedSCT::Status::ValidFromDisqualifiedLog);
+  CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::NotEnoughScts);
+}
+
+TEST_F(CTPolicyEnforcerTest,
+       ConformsWithDisqualifiedLogBeforeDisqualificationDate)
+{
+  VerifiedSCTList scts;
+
+  // 5 embedded SCTs required for DEFAULT_MONTHS.
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, BEFORE_DISQUALIFIED,
+         VerifiedSCT::Status::ValidFromDisqualifiedLog);
+
+  CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::Compliant);
+}
+
+TEST_F(CTPolicyEnforcerTest,
+       DoesNotConformWithDisqualifiedLogAfterDisqualificationDate)
+{
+  VerifiedSCTList scts;
+
+  // 5 embedded SCTs required for DEFAULT_MONTHS.
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED,
+         VerifiedSCT::Status::ValidFromDisqualifiedLog);
+
+  CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::NotEnoughScts);
+}
+
+TEST_F(CTPolicyEnforcerTest,
+       DoesNotConformWithIssuanceDateAfterDisqualificationDate)
+{
+  VerifiedSCTList scts;
+
+  // 5 embedded SCTs required for DEFAULT_MONTHS.
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED,
+         VerifiedSCT::Status::ValidFromDisqualifiedLog);
+  AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED);
+  AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED);
+  AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED);
+  AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED);
+
+  CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::NotEnoughScts);
+}
+
+TEST_F(CTPolicyEnforcerTest,
+       DoesNotConformToCTPolicyNotEnoughUniqueEmbeddedLogs)
+{
+  VerifiedSCTList scts;
+
+  // Operator #1
+  AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  // Operator #2, different logs
+  AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_3, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  // Operator #3, same log
+  AddSct(scts, LOG_4, OPERATOR_3, ORIGIN_EMBEDDED, TIMESTAMP_1);
+  AddSct(scts, LOG_4, OPERATOR_3, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+  // 5 embedded SCTs required. However, only 4 are from distinct logs.
+  CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+                  CTPolicyCompliance::NotEnoughScts);
+}
+
+TEST_F(CTPolicyEnforcerTest,
+       ConformsToPolicyExactNumberOfSCTsForValidityPeriod)
+{
+  // Test multiple validity periods.
+  const struct TestData {
+    size_t certLifetimeInCalendarMonths;
+    size_t sctsRequired;
+  } kTestData[] = {
+    { 3, 2 },
+    { 12 + 2, 2 },
+    { 12 + 3, 3 },
+    { 2*12 + 2, 3 },
+    { 2*12 + 3, 4 },
+    { 3*12 + 2, 4 },
+    { 3*12 + 4, 5 }
+  };
+
+  for (size_t i = 0; i < ArrayLength(kTestData); ++i) {
+    SCOPED_TRACE(i);
+
+    size_t months = kTestData[i].certLifetimeInCalendarMonths;
+    size_t sctsRequired = kTestData[i].sctsRequired;
+
+    // Less SCTs than required is not enough.
+    for (size_t sctsAvailable = 0; sctsAvailable < sctsRequired;
+         ++sctsAvailable) {
+      VerifiedSCTList scts;
+      AddMultipleScts(scts, sctsAvailable, 1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+      CTPolicyCompliance compliance;
+      ASSERT_EQ(Success,
+                mPolicyEnforcer.CheckCompliance(scts, months, NO_OPERATORS,
+                                                compliance))
+        << "i=" << i
+        << " sctsRequired=" << sctsRequired
+        << " sctsAvailable=" << sctsAvailable;
+      EXPECT_EQ(CTPolicyCompliance::NotEnoughScts, compliance)
+        << "i=" << i
+        << " sctsRequired=" << sctsRequired
+        << " sctsAvailable=" << sctsAvailable;
+    }
+
+    // Add exactly the required number of SCTs (from 2 operators).
+    VerifiedSCTList scts;
+    AddMultipleScts(scts, sctsRequired, 2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+    CTPolicyCompliance compliance;
+    ASSERT_EQ(Success,
+              mPolicyEnforcer.CheckCompliance(scts, months, NO_OPERATORS,
+                                              compliance))
+      << "i=" << i;
+    EXPECT_EQ(CTPolicyCompliance::Compliant, compliance)
+      << "i=" << i;
+  }
+}
+
+TEST_F(CTPolicyEnforcerTest, TestEdgeCasesOfGetCertLifetimeInFullMonths)
+{
+  const struct TestData {
+    PRTime notBefore;
+    PRTime notAfter;
+    size_t expectedMonths;
+  } kTestData[] = {
+    { // 1 second less than 1 month
+      1424863500000000, // Date.parse("2015-02-25T11:25:00Z") * 1000
+      1427196299000000, // Date.parse("2015-03-24T11:24:59Z") * 1000
+      0 },
+    { // exactly 1 month
+      1424863500000000, // Date.parse("2015-02-25T11:25:00Z") * 1000
+      1427282700000000, // Date.parse("2015-03-25T11:25:00Z") * 1000
+      1 },
+    { // 1 year, 1 month
+      1427282700000000, // Date.parse("2015-03-25T11:25:00Z") * 1000
+      1461583500000000, // Date.parse("2016-04-25T11:25:00Z") * 1000
+      13 },
+    { // 1 year, 1 month, first day of notBefore month, last of notAfter
+      1425209100000000, // Date.parse("2015-03-01T11:25:00Z") * 1000
+      1462015500000000, // Date.parse("2016-04-30T11:25:00Z") * 1000
+      13 },
+    { // 1 year, adjacent months, last day of notBefore month, first of notAfter
+      1427801100000000, // Date.parse("2015-03-31T11:25:00Z") * 1000
+      1459509900000000, // Date.parse("2016-04-01T11:25:00Z") * 1000
+      12 }
+  };
+
+  for (size_t i = 0; i < ArrayLength(kTestData); ++i) {
+    SCOPED_TRACE(i);
+
+    size_t months;
+    ASSERT_EQ(Success,
+              GetCertLifetimeInFullMonths(kTestData[i].notBefore,
+                                          kTestData[i].notAfter,
+                                          months))
+              << "i=" << i;
+    EXPECT_EQ(kTestData[i].expectedMonths, months)
+              << "i=" << i;
+  }
+}
+
+} } // namespace mozilla::ct
--- a/security/certverifier/tests/gtest/moz.build
+++ b/security/certverifier/tests/gtest/moz.build
@@ -1,17 +1,19 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 SOURCES += [
+    'CTDiversityPolicyTest.cpp',
     'CTLogVerifierTest.cpp',
     'CTObjectsExtractorTest.cpp',
+    'CTPolicyEnforcerTest.cpp',
     'CTSerializationTest.cpp',
     'CTTestUtils.cpp',
     'MultiLogCTVerifierTest.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '/security/certverifier',
     '/security/pkix/include',
--- a/security/manager/locales/en-US/chrome/pippki/pippki.properties
+++ b/security/manager/locales/en-US/chrome/pippki/pippki.properties
@@ -106,20 +106,17 @@ pageInfo_Privacy_None4=The page you are 
 # %3$S is protocol version like "SSL 3" or "TLS 1.2"
 pageInfo_EncryptionWithBitsAndProtocol=Connection Encrypted (%1$S, %2$S bit keys, %3$S)
 pageInfo_BrokenEncryption=Broken Encryption (%1$S, %2$S bit keys, %3$S)
 pageInfo_Privacy_Encrypted1=The page you are viewing was encrypted before being transmitted over the Internet.
 pageInfo_Privacy_Encrypted2=Encryption makes it difficult for unauthorized people to view information traveling between computers. It is therefore unlikely that anyone read this page as it traveled across the network.
 pageInfo_MixedContent=Connection Partially Encrypted
 pageInfo_MixedContent2=Parts of the page you are viewing were not encrypted before being transmitted over the Internet.
 pageInfo_WeakCipher=Your connection to this website uses weak encryption and is not private. Other people can view your information or modify the website’s behavior.
-pageInfo_CertificateTransparency_None=This website does not supply Certificate Transparency audit records.
-pageInfo_CertificateTransparency_OK=This website supplies publicly auditable Certificate Transparency records.
-pageInfo_CertificateTransparency_UnknownLog=This website claims to have Certificate Transparency audit records, but the records were issued by an unknown party and cannot be verified.
-pageInfo_CertificateTransparency_Invalid=This website supplies Certificate Transparency audit records, but the records failed verification.
+pageInfo_CertificateTransparency_Compliant=This website complies with the Certificate Transparency policy.
 
 # Cert Viewer
 # LOCALIZATION NOTE(certViewerTitle): Title used for the Certificate Viewer.
 # %1$S is a string representative of the certificate being viewed.
 certViewerTitle=Certificate Viewer: “%1$S”
 notPresent=<Not Part Of Certificate>
 
 # Token Manager
--- a/security/manager/ssl/SSLServerCertVerification.cpp
+++ b/security/manager/ssl/SSLServerCertVerification.cpp
@@ -1273,36 +1273,30 @@ void
 GatherCertificateTransparencyTelemetry(const UniqueCERTCertList& certList,
                                        const CertificateTransparencyInfo& info)
 {
   if (!info.enabled) {
     // No telemetry is gathered when CT is disabled.
     return;
   }
 
-  if (!info.processedSCTs) {
-    // We didn't receive any SCT data for this connection.
-    Telemetry::Accumulate(Telemetry::SSL_SCTS_PER_CONNECTION, 0);
-    return;
-  }
-
   for (const ct::VerifiedSCT& sct : info.verifyResult.verifiedScts) {
     GatherTelemetryForSingleSCT(sct);
   }
 
   // Decoding errors are reported to the 0th bucket
   // of the SSL_SCTS_VERIFICATION_STATUS enumerated probe.
   for (size_t i = 0; i < info.verifyResult.decodingErrors; ++i) {
     Telemetry::Accumulate(Telemetry::SSL_SCTS_VERIFICATION_STATUS, 0);
   }
 
   // Handle the histogram of SCTs counts.
   uint32_t sctsCount =
     static_cast<uint32_t>(info.verifyResult.verifiedScts.length());
-  // Note that sctsCount can be 0 in case we've received SCT binary data,
+  // Note that sctsCount can also be 0 in case we've received SCT binary data,
   // but it failed to parse (e.g. due to unsupported CT protocol version).
   Telemetry::Accumulate(Telemetry::SSL_SCTS_PER_CONNECTION, sctsCount);
 }
 
 // Note: Takes ownership of |peerCertChain| if SECSuccess is not returned.
 SECStatus
 AuthCertificate(CertVerifier& certVerifier,
                 nsNSSSocketInfo* infoObject,
--- a/security/manager/ssl/nsISSLStatus.idl
+++ b/security/manager/ssl/nsISSLStatus.idl
@@ -18,21 +18,20 @@ interface nsISSLStatus : nsISupports {
 
   const short SSL_VERSION_3   = 0;
   const short TLS_VERSION_1   = 1;
   const short TLS_VERSION_1_1 = 2;
   const short TLS_VERSION_1_2 = 3;
   const short TLS_VERSION_1_3 = 4;
   readonly attribute unsigned short protocolVersion;
 
-  const short CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE = 0;
-  const short CERTIFICATE_TRANSPARENCY_NONE           = 1;
-  const short CERTIFICATE_TRANSPARENCY_OK             = 2;
-  const short CERTIFICATE_TRANSPARENCY_UNKNOWN_LOG    = 3;
-  const short CERTIFICATE_TRANSPARENCY_INVALID        = 4;
+  const short CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE          = 0;
+  const short CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT        = 5;
+  const short CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS  = 6;
+  const short CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS = 7;
   readonly attribute unsigned short certificateTransparencyStatus;
 
   readonly attribute boolean isDomainMismatch;
   readonly attribute boolean isNotValidAtThisTime;
 
   /* Note: To distinguish between
    *         "unstrusted because missing or untrusted issuer"
    *       and
--- a/security/manager/ssl/nsSSLStatus.cpp
+++ b/security/manager/ssl/nsSSLStatus.cpp
@@ -325,56 +325,36 @@ nsSSLStatus::SetServerCert(nsNSSCertific
   mIsEV = (aEVStatus == EVStatus::EV);
   mHasIsEVStatus = true;
 }
 
 void
 nsSSLStatus::SetCertificateTransparencyInfo(
   const mozilla::psm::CertificateTransparencyInfo& info)
 {
-  using mozilla::ct::VerifiedSCT;
+  using mozilla::ct::CTPolicyCompliance;
+
+  mCertificateTransparencyStatus =
+    nsISSLStatus::CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE;
 
   if (!info.enabled) {
     // CT disabled.
-    mCertificateTransparencyStatus =
-      nsISSLStatus::CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE;
-    return;
-  }
-
-  if (!info.processedSCTs) {
-    // No SCTs processed on the connection.
-    mCertificateTransparencyStatus =
-      nsISSLStatus::CERTIFICATE_TRANSPARENCY_NONE;
     return;
   }
 
-  bool hasValidSCTs = false;
-  bool hasUnknownLogSCTs = false;
-  bool hasInvalidSCTs = false;
-  for (const VerifiedSCT& verifiedSct : info.verifyResult.verifiedScts) {
-    switch (verifiedSct.status) {
-      case VerifiedSCT::Status::Valid:
-        hasValidSCTs = true;
-        break;
-      case VerifiedSCT::Status::UnknownLog:
-      case VerifiedSCT::Status::ValidFromDisqualifiedLog:
-        hasUnknownLogSCTs = true;
-        break;
-      case VerifiedSCT::Status::InvalidSignature:
-      case VerifiedSCT::Status::InvalidTimestamp:
-        hasInvalidSCTs = true;
-        break;
-      default:
-        MOZ_ASSERT_UNREACHABLE("Unexpected VerifiedSCT::Status type");
-    }
-  }
-
-  if (hasValidSCTs) {
-    mCertificateTransparencyStatus =
-      nsISSLStatus::CERTIFICATE_TRANSPARENCY_OK;
-  } else if (hasUnknownLogSCTs) {
-    mCertificateTransparencyStatus =
-      nsISSLStatus::CERTIFICATE_TRANSPARENCY_UNKNOWN_LOG;
-  } else if (hasInvalidSCTs) {
-    mCertificateTransparencyStatus =
-      nsISSLStatus::CERTIFICATE_TRANSPARENCY_INVALID;
+  switch (info.policyCompliance) {
+    case CTPolicyCompliance::Compliant:
+      mCertificateTransparencyStatus =
+        nsISSLStatus::CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT;
+      break;
+    case CTPolicyCompliance::NotEnoughScts:
+      mCertificateTransparencyStatus =
+        nsISSLStatus::CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS;
+      break;
+    case CTPolicyCompliance::NotDiverseScts:
+      mCertificateTransparencyStatus =
+        nsISSLStatus::CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS;
+      break;
+    case CTPolicyCompliance::Unknown:
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unexpected CTPolicyCompliance type");
   }
 }