--- 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, ¬Before, ¬After) != 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");
}
}