Bug 1275238 - Certificate Transparency support in mozilla::pkix; r?keeler draft
authorSergei Chernov <sergei.cv@ndivi.com>
Wed, 15 Jun 2016 11:11:00 +0300
changeset 382469 bff9a854c67882033e7a9f3b54e31b11732934f4
parent 378181 14c5bf11d37b9e92d27f7089d9392de2ac339bb3
child 524196 0d8a216b25b0e07474924086882284a15a91861c
push id21725
push usersergei.cv@ndivi.com
push dateWed, 29 Jun 2016 18:04:55 +0000
reviewerskeeler
bugs1275238
milestone50.0a1
Bug 1275238 - Certificate Transparency support in mozilla::pkix; r?keeler MozReview-Commit-ID: HZwzSgxarTw
security/apps/AppTrustDomain.cpp
security/apps/AppTrustDomain.h
security/certverifier/NSSCertDBTrustDomain.cpp
security/certverifier/NSSCertDBTrustDomain.h
security/certverifier/OCSPVerificationTrustDomain.cpp
security/certverifier/OCSPVerificationTrustDomain.h
security/manager/ssl/CSTrustDomain.cpp
security/manager/ssl/CSTrustDomain.h
security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp
security/pkix/include/pkix/pkixtypes.h
security/pkix/lib/pkixbuild.cpp
security/pkix/lib/pkixcert.cpp
security/pkix/lib/pkixocsp.cpp
security/pkix/lib/pkixutil.h
security/pkix/test/gtest/pkixbuild_tests.cpp
security/pkix/test/gtest/pkixgtest.h
security/pkix/test/gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp
security/pkix/test/lib/pkixtestutil.cpp
security/pkix/test/lib/pkixtestutil.h
--- a/security/apps/AppTrustDomain.cpp
+++ b/security/apps/AppTrustDomain.cpp
@@ -373,9 +373,15 @@ AppTrustDomain::CheckValidityIsAcceptabl
 Result
 AppTrustDomain::NetscapeStepUpMatchesServerAuth(Time /*notBefore*/,
                                                 /*out*/ bool& matches)
 {
   matches = false;
   return Success;
 }
 
+void
+AppTrustDomain::NoteAuxiliaryExtension(AuxiliaryExtension /*extension*/,
+                                       Input /*extensionData*/)
+{
+}
+
 } } // namespace mozilla::psm
--- a/security/apps/AppTrustDomain.h
+++ b/security/apps/AppTrustDomain.h
@@ -59,16 +59,19 @@ public:
                    mozilla::pkix::Input subjectPublicKeyInfo) override;
   virtual Result CheckValidityIsAcceptable(
                    mozilla::pkix::Time notBefore, mozilla::pkix::Time notAfter,
                    mozilla::pkix::EndEntityOrCA endEntityOrCA,
                    mozilla::pkix::KeyPurposeId keyPurpose) override;
   virtual Result NetscapeStepUpMatchesServerAuth(
                    mozilla::pkix::Time notBefore,
                    /*out*/ bool& matches) override;
+  virtual void NoteAuxiliaryExtension(
+                   mozilla::pkix::AuxiliaryExtension extension,
+                   mozilla::pkix::Input extensionData) override;
   virtual Result DigestBuf(mozilla::pkix::Input item,
                            mozilla::pkix::DigestAlgorithm digestAlg,
                            /*out*/ uint8_t* digestBuf,
                            size_t digestBufLen) override;
 
 private:
   /*out*/ UniqueCERTCertList& mCertChain;
   void* mPinArg; // non-owning!
--- a/security/certverifier/NSSCertDBTrustDomain.cpp
+++ b/security/certverifier/NSSCertDBTrustDomain.cpp
@@ -953,16 +953,22 @@ NSSCertDBTrustDomain::NetscapeStepUpMatc
       matches = false;
       return Success;
     default:
       MOZ_ASSERT_UNREACHABLE("unhandled NetscapeStepUpPolicy type");
   }
   return Result::FATAL_ERROR_LIBRARY_FAILURE;
 }
 
+void
+NSSCertDBTrustDomain::NoteAuxiliaryExtension(AuxiliaryExtension /*extension*/,
+                                             Input /*extensionData*/)
+{
+}
+
 SECStatus
 InitializeNSS(const char* dir, bool readOnly, bool loadPKCS11Modules)
 {
   // The NSS_INIT_NOROOTINIT flag turns off the loading of the root certs
   // module by NSS_Initialize because we will load it in InstallLoadableRoots
   // later.  It also allows us to work around a bug in the system NSS in
   // Ubuntu 8.04, which loads any nonexistent "<configdir>/libnssckbi.so" as
   // "/usr/lib/nss/libnssckbi.so".
--- a/security/certverifier/NSSCertDBTrustDomain.h
+++ b/security/certverifier/NSSCertDBTrustDomain.h
@@ -136,16 +136,20 @@ public:
                    mozilla::pkix::Duration validityDuration,
       /*optional*/ const mozilla::pkix::Input* stapledOCSPResponse,
       /*optional*/ const mozilla::pkix::Input* aiaExtension)
                    override;
 
   virtual Result IsChainValid(const mozilla::pkix::DERArray& certChain,
                               mozilla::pkix::Time time) override;
 
+  virtual void NoteAuxiliaryExtension(
+                   mozilla::pkix::AuxiliaryExtension extension,
+                   mozilla::pkix::Input extensionData) override;
+
   CertVerifier::OCSPStaplingStatus GetOCSPStaplingStatus() const
   {
     return mOCSPStaplingStatus;
   }
   void ResetOCSPStaplingStatus()
   {
     mOCSPStaplingStatus = CertVerifier::OCSP_STAPLING_NEVER_CHECKED;
   }
--- a/security/certverifier/OCSPVerificationTrustDomain.cpp
+++ b/security/certverifier/OCSPVerificationTrustDomain.cpp
@@ -103,15 +103,22 @@ OCSPVerificationTrustDomain::CheckValidi
 
 Result
 OCSPVerificationTrustDomain::NetscapeStepUpMatchesServerAuth(Time notBefore,
                                                      /*out*/ bool& matches)
 {
   return mCertDBTrustDomain.NetscapeStepUpMatchesServerAuth(notBefore, matches);
 }
 
+void
+OCSPVerificationTrustDomain::NoteAuxiliaryExtension(
+  AuxiliaryExtension extension, Input extensionData)
+{
+  mCertDBTrustDomain.NoteAuxiliaryExtension(extension, extensionData);
+}
+
 Result
 OCSPVerificationTrustDomain::DigestBuf(
   Input item, DigestAlgorithm digestAlg,
   /*out*/ uint8_t* digestBuf, size_t digestBufLen)
 {
   return mCertDBTrustDomain.DigestBuf(item, digestAlg, digestBuf, digestBufLen);
 }
--- a/security/certverifier/OCSPVerificationTrustDomain.h
+++ b/security/certverifier/OCSPVerificationTrustDomain.h
@@ -68,16 +68,20 @@ public:
                    mozilla::pkix::Duration validityDuration,
       /*optional*/ const mozilla::pkix::Input* stapledOCSPResponse,
       /*optional*/ const mozilla::pkix::Input* aiaExtension)
                    override;
 
   virtual Result IsChainValid(const mozilla::pkix::DERArray& certChain,
                               mozilla::pkix::Time time) override;
 
+  virtual void NoteAuxiliaryExtension(
+                   mozilla::pkix::AuxiliaryExtension extension,
+                   mozilla::pkix::Input extensionData) override;
+
 private:
   NSSCertDBTrustDomain& mCertDBTrustDomain;
 };
 
 
 } } // namespace mozilla::psm
 
 #endif // mozilla_psm__OCSPVerificationTrustDomain_h
--- a/security/manager/ssl/CSTrustDomain.cpp
+++ b/security/manager/ssl/CSTrustDomain.cpp
@@ -210,16 +210,22 @@ CSTrustDomain::CheckValidityIsAcceptable
 Result
 CSTrustDomain::NetscapeStepUpMatchesServerAuth(Time notBefore,
                                                /*out*/ bool& matches)
 {
   matches = false;
   return Success;
 }
 
+void
+CSTrustDomain::NoteAuxiliaryExtension(AuxiliaryExtension /*extension*/,
+                                      Input /*extensionData*/)
+{
+}
+
 Result
 CSTrustDomain::DigestBuf(Input item, DigestAlgorithm digestAlg,
                          /*out*/ uint8_t* digestBuf, size_t digestBufLen)
 {
   return DigestBufNSS(item, digestAlg, digestBuf, digestBufLen);
 }
 
 } } // end namespace mozilla::psm
--- a/security/manager/ssl/CSTrustDomain.h
+++ b/security/manager/ssl/CSTrustDomain.h
@@ -57,16 +57,19 @@ public:
     const mozilla::pkix::SignedDigest& signedDigest,
     mozilla::pkix::Input subjectPublicKeyInfo) override;
   virtual Result CheckValidityIsAcceptable(
     mozilla::pkix::Time notBefore, mozilla::pkix::Time notAfter,
     mozilla::pkix::EndEntityOrCA endEntityOrCA,
     mozilla::pkix::KeyPurposeId keyPurpose) override;
   virtual Result NetscapeStepUpMatchesServerAuth(
     mozilla::pkix::Time notBefore, /*out*/ bool& matches) override;
+  virtual void NoteAuxiliaryExtension(
+    mozilla::pkix::AuxiliaryExtension extension,
+    mozilla::pkix::Input extensionData) override;
   virtual Result DigestBuf(mozilla::pkix::Input item,
                            mozilla::pkix::DigestAlgorithm digestAlg,
                            /*out*/ uint8_t* digestBuf,
                            size_t digestBufLen) override;
 
 private:
   /*out*/ UniqueCERTCertList& mCertChain;
   nsCOMPtr<nsICertBlocklist> mCertBlocklist;
--- a/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp
+++ b/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp
@@ -179,17 +179,17 @@ GetOCSPResponseForType(OCSPResponseType 
       0x1a, 0x85, 0x1a, 0x01, 0x83, 0x74, 0x09, 0x02
     };
 
     extension.id.assign(tlv_some_Mozilla_OID, sizeof(tlv_some_Mozilla_OID));
     extension.critical = (aORT == ORTCriticalExtension);
     extension.value.push_back(0x05); // tag: NULL
     extension.value.push_back(0x00); // length: 0
     extension.next = nullptr;
-    context.extensions = &extension;
+    context.responseExtensions = &extension;
   }
   if (aORT == ORTEmptyExtensions) {
     context.includeEmptyExtensions = true;
   }
 
   if (!signerCert) {
     signerCert.reset(CERT_DupCertificate(issuerCert.get()));
   }
--- a/security/pkix/include/pkix/pkixtypes.h
+++ b/security/pkix/include/pkix/pkixtypes.h
@@ -100,16 +100,31 @@ struct CertPolicyId final
 enum class TrustLevel
 {
   TrustAnchor = 1,        // certificate is a trusted root CA certificate or
                           // equivalent *for the given policy*.
   ActivelyDistrusted = 2, // certificate is known to be bad
   InheritsTrust = 3       // certificate must chain to a trust anchor
 };
 
+// Extensions extracted during the verification flow.
+// See TrustDomain::NoteAuxiliaryExtension.
+enum class AuxiliaryExtension
+{
+  // Certificate Transparency data, specifically Signed Certificate
+  // Timestamps (SCTs). See RFC 6962.
+
+  // SCT list embedded in the end entity certificate. Called by BuildCertChain
+  // after the certificate containing the SCTs has passed the revocation checks.
+  EmbeddedSCTList = 1,
+  // SCT list from OCSP response. Called by VerifyEncodedOCSPResponse
+  // when its result is a success and the SCT list is present.
+  SCTListFromOCSPResponse = 2
+};
+
 // CertID references the information needed to do revocation checking for the
 // certificate issued by the given issuer with the given serial number.
 //
 // issuer must be the DER-encoded issuer field from the certificate for which
 // revocation checking is being done, **NOT** the subject field of the issuer
 // certificate. (Those two fields must be equal to each other, but they may not
 // be encoded exactly the same, and the encoding matters for OCSP.)
 // issuerSubjectPublicKeyInfo is the entire DER-encoded subjectPublicKeyInfo
@@ -332,16 +347,23 @@ public:
   // contains the id-Netscape-stepUp OID but does not contain the
   // id-kp-serverAuth OID may be considered valid for issuing server auth
   // certificates. This function allows TrustDomain implementations to control
   // this setting based on the start of the validity period of the certificate
   // in question.
   virtual Result NetscapeStepUpMatchesServerAuth(Time notBefore,
                                                  /*out*/ bool& matches) = 0;
 
+  // Some certificate or OCSP response extensions do not directly participate
+  // in the verification flow, but might still be of interest to the clients
+  // (notably Certificate Transparency data, RFC 6962). Such extensions are
+  // extracted and passed to this function for further processing.
+  virtual void NoteAuxiliaryExtension(AuxiliaryExtension extension,
+                                      Input extensionData) = 0;
+
   // Compute a digest of the data in item using the given digest algorithm.
   //
   // item contains the data to hash.
   // digestBuf points to a buffer to where the digest will be written.
   // digestBufLen will be the size of the digest output (20 for SHA-1,
   // 32 for SHA-256, etc.).
   //
   // TODO: Taking the output buffer as (uint8_t*, size_t) is counter to our
--- a/security/pkix/lib/pkixbuild.cpp
+++ b/security/pkix/lib/pkixbuild.cpp
@@ -239,16 +239,30 @@ PathBuildingStep::Check(Input potentialI
     }
     Duration validityDuration(notAfter, notBefore);
     rv = trustDomain.CheckRevocation(subject.endEntityOrCA, certID, time,
                                      validityDuration, stapledOCSPResponse,
                                      subject.GetAuthorityInfoAccess());
     if (rv != Success) {
       return RecordResult(rv, keepGoing);
     }
+
+    if (subject.endEntityOrCA == EndEntityOrCA::MustBeEndEntity) {
+      const Input* sctExtension = subject.GetSignedCertificateTimestamps();
+      if (sctExtension) {
+        Input sctList;
+        rv = ExtractSignedCertificateTimestampListFromExtension(*sctExtension,
+                                                                sctList);
+        if (rv != Success) {
+          return RecordResult(rv, keepGoing);
+        }
+        trustDomain.NoteAuxiliaryExtension(AuxiliaryExtension::EmbeddedSCTList,
+                                           sctList);
+      }
+    }
   }
 
   return RecordResult(Success, keepGoing);
 }
 
 // Recursively build the path from the given subject certificate to the root.
 //
 // Be very careful about changing the order of checks. The order is significant
--- a/security/pkix/lib/pkixcert.cpp
+++ b/security/pkix/lib/pkixcert.cpp
@@ -218,16 +218,21 @@ BackCert::RememberExtension(Reader& extn
   // python DottedOIDToCode.py Netscape-certificate-type 2.16.840.1.113730.1.1
   static const uint8_t Netscape_certificate_type[] = {
     0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x01, 0x01
   };
   // python DottedOIDToCode.py id-pe-tlsfeature 1.3.6.1.5.5.7.1.24
   static const uint8_t id_pe_tlsfeature[] = {
     0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x18
   };
+  // python DottedOIDToCode.py id-embeddedSctList 1.3.6.1.4.1.11129.2.4.2
+  // See Section 3.3 of RFC 6962.
+  static const uint8_t id_embeddedSctList[] = {
+    0x2b, 0x06, 0x01, 0x04, 0x01, 0xd6, 0x79, 0x02, 0x04, 0x02
+  };
 
   Input* out = nullptr;
 
   // We already enforce the maximum possible constraints for policies so we
   // can safely ignore even critical policy constraint extensions.
   //
   // XXX: Doing it this way won't allow us to detect duplicate
   // policyConstraints extensions, but that's OK because (and only because) we
@@ -264,16 +269,18 @@ BackCert::RememberExtension(Reader& extn
   } else if (extnID.MatchRest(id_ce_extKeyUsage)) {
     out = &extKeyUsage;
   } else if (extnID.MatchRest(id_ce_inhibitAnyPolicy)) {
     out = &inhibitAnyPolicy;
   } else if (extnID.MatchRest(id_pe_authorityInfoAccess)) {
     out = &authorityInfoAccess;
   } else if (extnID.MatchRest(id_pe_tlsfeature)) {
     out = &requiredTLSFeatures;
+  } else if (extnID.MatchRest(id_embeddedSctList)) {
+    out = &signedCertificateTimestamps;
   } else if (extnID.MatchRest(id_pkix_ocsp_nocheck) && critical) {
     // We need to make sure we don't reject delegated OCSP response signing
     // certificates that contain the id-pkix-ocsp-nocheck extension marked as
     // critical when validating OCSP responses. Without this, an application
     // that implements soft-fail OCSP might ignore a valid Revoked or Unknown
     // response, and an application that implements hard-fail OCSP might fail
     // to connect to a server given a valid Good response.
     out = &dummyOCSPNocheck;
@@ -295,9 +302,22 @@ BackCert::RememberExtension(Reader& extn
       return Result::ERROR_EXTENSION_VALUE_INVALID;
     }
     understood = true;
   }
 
   return Success;
 }
 
+Result
+ExtractSignedCertificateTimestampListFromExtension(Input extnValue,
+                                                   Input& sctList)
+{
+  Reader decodedValue;
+  Result rv = der::ExpectTagAndGetValueAtEnd(extnValue, der::OCTET_STRING,
+                                             decodedValue);
+  if (rv != Success) {
+    return rv;
+  }
+  return decodedValue.SkipToEnd(sctList);
+}
+
 } } // namespace mozilla::pkix
--- a/security/pkix/lib/pkixocsp.cpp
+++ b/security/pkix/lib/pkixocsp.cpp
@@ -71,16 +71,18 @@ public:
   const CertID& certID;
   const Time time;
   const uint16_t maxLifetimeInDays;
   CertStatus certStatus;
   Time* thisUpdate;
   Time* validThrough;
   bool expired;
 
+  Input signedCertificateTimestamps;
+
   // Keep track of whether the OCSP response contains the status of the
   // certificate we're interested in. Responders might reply without
   // including the status of any of the requested certs, we should
   // indicate a server failure in those cases.
   bool matchFound;
 
   Context(const Context&) = delete;
   void operator=(const Context&) = delete;
@@ -163,16 +165,19 @@ static inline Result BasicResponse(Reade
 static inline Result ResponseData(
                        Reader& tbsResponseData,
                        Context& context,
                        const der::SignedDataWithSignature& signedResponseData,
                        const DERArray& certs);
 static inline Result SingleResponse(Reader& input, Context& context);
 static Result ExtensionNotUnderstood(Reader& extnID, Input extnValue,
                                      bool critical, /*out*/ bool& understood);
+static Result RememberSingleExtension(Context& context, Reader& extnID,
+                                      Input extnValue, bool critical,
+                                      /*out*/ bool& understood);
 static inline Result CertID(Reader& input,
                             const Context& context,
                             /*out*/ bool& match);
 static Result MatchKeyHash(TrustDomain& trustDomain,
                            Input issuerKeyHash,
                            Input issuerSubjectPublicKeyInfo,
                            /*out*/ bool& match);
 static Result KeyHash(TrustDomain& trustDomain,
@@ -325,16 +330,26 @@ VerifyEncodedOCSPResponse(TrustDomain& t
 
   expired = context.expired;
 
   switch (context.certStatus) {
     case CertStatus::Good:
       if (expired) {
         return Result::ERROR_OCSP_OLD_RESPONSE;
       }
+      if (context.signedCertificateTimestamps.GetLength()) {
+        Input sctList;
+        rv = ExtractSignedCertificateTimestampListFromExtension(
+          context.signedCertificateTimestamps, sctList);
+        if (rv != Success) {
+          return MapBadDERToMalformedOCSPResponse(rv);
+        }
+        context.trustDomain.NoteAuxiliaryExtension(
+          AuxiliaryExtension::SCTListFromOCSPResponse, sctList);
+      }
       return Success;
     case CertStatus::Revoked:
       return Result::ERROR_REVOKED_CERTIFICATE;
     case CertStatus::Unknown:
       return Result::ERROR_OCSP_UNKNOWN_CERT;
      MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM
   }
 }
@@ -646,19 +661,25 @@ SingleResponse(Reader& input, Context& c
     // This could only happen if we're dealing with times beyond the year
     // 10,000AD.
     return Result::ERROR_OCSP_FUTURE_RESPONSE;
   }
   if (context.time > notAfterPlusSlop) {
     context.expired = true;
   }
 
-  rv = der::OptionalExtensions(input,
-                               der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 1,
-                               ExtensionNotUnderstood);
+  rv = der::OptionalExtensions(
+    input,
+    der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 1,
+    [&context](Reader& extnID, const Input& extnValue, bool critical,
+               /*out*/ bool& understood) {
+      return RememberSingleExtension(context, extnID, extnValue, critical,
+                                     understood);
+    });
+
   if (rv != Success) {
     return rv;
   }
 
   if (context.thisUpdate) {
     *context.thisUpdate = thisUpdate;
   }
   if (context.validThrough) {
@@ -821,16 +842,46 @@ KeyHash(TrustDomain& trustDomain, const 
 Result
 ExtensionNotUnderstood(Reader& /*extnID*/, Input /*extnValue*/,
                        bool /*critical*/, /*out*/ bool& understood)
 {
   understood = false;
   return Success;
 }
 
+Result
+RememberSingleExtension(Context& context, Reader& extnID, Input extnValue,
+                        bool /*critical*/, /*out*/ bool& understood)
+{
+  understood = false;
+
+  // SingleExtension for Signed Certificate Timestamp List.
+  // See Section 3.3 of RFC 6962.
+  // python DottedOIDToCode.py
+  //   id_ocsp_singleExtensionSctList 1.3.6.1.4.1.11129.2.4.5
+  static const uint8_t id_ocsp_singleExtensionSctList[] = {
+    0x2b, 0x06, 0x01, 0x04, 0x01, 0xd6, 0x79, 0x02, 0x04, 0x05
+  };
+
+  if (extnID.MatchRest(id_ocsp_singleExtensionSctList)) {
+    // Empty values are not allowed for this extension. Note that
+    // we assume this later, when checking if the extension was present.
+    if (extnValue.GetLength() == 0) {
+      return Result::ERROR_EXTENSION_VALUE_INVALID;
+    }
+    if (context.signedCertificateTimestamps.Init(extnValue) != Success) {
+      // Duplicate extension.
+      return Result::ERROR_EXTENSION_VALUE_INVALID;
+    }
+    understood = true;
+  }
+
+  return Success;
+}
+
 //   1. The certificate identified in a received response corresponds to
 //      the certificate that was identified in the corresponding request;
 //   2. The signature on the response is valid;
 //   3. The identity of the signer matches the intended recipient of the
 //      request;
 //   4. The signer is currently authorized to provide a response for the
 //      certificate in question;
 //   5. The time at which the status being indicated is known to be
--- a/security/pkix/lib/pkixutil.h
+++ b/security/pkix/lib/pkixutil.h
@@ -101,16 +101,20 @@ public:
   const Input* GetSubjectAltName() const
   {
     return MaybeInput(subjectAltName);
   }
   const Input* GetRequiredTLSFeatures() const
   {
     return MaybeInput(requiredTLSFeatures);
   }
+  const Input* GetSignedCertificateTimestamps() const
+  {
+    return MaybeInput(signedCertificateTimestamps);
+  }
 
 private:
   const Input der;
 
 public:
   const EndEntityOrCA endEntityOrCA;
   BackCert const* const childCert;
 
@@ -144,16 +148,17 @@ private:
   Input certificatePolicies;
   Input extKeyUsage;
   Input inhibitAnyPolicy;
   Input keyUsage;
   Input nameConstraints;
   Input subjectAltName;
   Input criticalNetscapeCertificateType;
   Input requiredTLSFeatures;
+  Input signedCertificateTimestamps; // RFC 6962 (Certificate Transparency)
 
   Result RememberExtension(Reader& extnID, Input extnValue, bool critical,
                            /*out*/ bool& understood);
 
   BackCert(const BackCert&) = delete;
   void operator=(const BackCert&) = delete;
 };
 
@@ -192,16 +197,22 @@ public:
 private:
   Input items[MAX_LENGTH]; // avoids any heap allocations
   size_t numItems;
 
   NonOwningDERArray(const NonOwningDERArray&) = delete;
   void operator=(const NonOwningDERArray&) = delete;
 };
 
+// Extracts the SignedCertificateTimestampList structure which is encoded as an
+// OCTET STRING within the X.509v3 / OCSP extensions (see RFC 6962 section 3.3).
+Result
+ExtractSignedCertificateTimestampListFromExtension(Input extnValue,
+                                                   Input& sctList);
+
 inline unsigned int
 DaysBeforeYear(unsigned int year)
 {
   assert(year <= 9999);
   return ((year - 1u) * 365u)
        + ((year - 1u) / 4u)    // leap years are every 4 years,
        - ((year - 1u) / 100u)  // except years divisible by 100,
        + ((year - 1u) / 400u); // except years divisible by 400.
--- a/security/pkix/test/gtest/pkixbuild_tests.cpp
+++ b/security/pkix/test/gtest/pkixbuild_tests.cpp
@@ -26,53 +26,61 @@
 // When building with -D_HAS_EXCEPTIONS=0, MSVC's <xtree> header triggers
 // warning C4702: unreachable code.
 // https://connect.microsoft.com/VisualStudio/feedback/details/809962
 #pragma warning(push)
 #pragma warning(disable: 4702)
 #endif
 
 #include <map>
+#include <vector>
 
 #if defined(_MSC_VER) && _MSC_VER < 1900
 #pragma warning(pop)
 #endif
 
+#include "pkixder.h"
 #include "pkixgtest.h"
 
 using namespace mozilla::pkix;
 using namespace mozilla::pkix::test;
 
 static ByteString
 CreateCert(const char* issuerCN, // null means "empty name"
            const char* subjectCN, // null means "empty name"
            EndEntityOrCA endEntityOrCA,
            /*optional modified*/ std::map<ByteString, ByteString>*
-             subjectDERToCertDER = nullptr)
+             subjectDERToCertDER = nullptr,
+           /*optional*/ const ByteString* extension = nullptr)
 {
   static long serialNumberValue = 0;
   ++serialNumberValue;
   ByteString serialNumber(CreateEncodedSerialNumber(serialNumberValue));
   EXPECT_FALSE(ENCODING_FAILED(serialNumber));
 
   ByteString issuerDER(issuerCN ? CNToDERName(issuerCN) : Name(ByteString()));
   ByteString subjectDER(subjectCN ? CNToDERName(subjectCN) : Name(ByteString()));
 
-  ByteString extensions[2];
+  std::vector<ByteString> extensions;
   if (endEntityOrCA == EndEntityOrCA::MustBeCA) {
-    extensions[0] =
+    ByteString basicConstraints =
       CreateEncodedBasicConstraints(true, nullptr, Critical::Yes);
-    EXPECT_FALSE(ENCODING_FAILED(extensions[0]));
+    EXPECT_FALSE(ENCODING_FAILED(basicConstraints));
+    extensions.push_back(basicConstraints);
   }
+  if (extension) {
+    extensions.push_back(*extension);
+  }
+  extensions.push_back(ByteString()); // marks the end of the list
 
   ScopedTestKeyPair reusedKey(CloneReusedKeyPair());
   ByteString certDER(CreateEncodedCertificate(
                        v3, sha256WithRSAEncryption(), serialNumber, issuerDER,
                        oneDayBeforeNow, oneDayAfterNow, subjectDER,
-                       *reusedKey, extensions, *reusedKey,
+                       *reusedKey, extensions.data(), *reusedKey,
                        sha256WithRSAEncryption()));
   EXPECT_FALSE(ENCODING_FAILED(certDER));
 
   if (subjectDERToCertDER) {
     (*subjectDERToCertDER)[subjectDER] = certDER;
   }
 
   return certDER;
@@ -234,25 +242,25 @@ TEST_F(pkixbuild, BeyondMaxAcceptableCer
                              EndEntityOrCA::MustBeEndEntity,
                              KeyUsage::noParticularKeyUsageRequired,
                              KeyPurposeId::id_kp_serverAuth,
                              CertPolicyId::anyPolicy,
                              nullptr/*stapledOCSPResponse*/));
   }
 }
 
-// A TrustDomain that explicitly fails if CheckRevocation is called.
+// A TrustDomain that checks certificates against a given root certificate.
 // It is initialized with the DER encoding of a root certificate that
 // is treated as a trust anchor and is assumed to have issued all certificates
 // (i.e. FindIssuer always attempts to build the next step in the chain with
 // it).
-class ExpiredCertTrustDomain final : public DefaultCryptoTrustDomain
+class SingleRootTrustDomain : public DefaultCryptoTrustDomain
 {
 public:
-  explicit ExpiredCertTrustDomain(ByteString rootDER)
+  explicit SingleRootTrustDomain(ByteString rootDER)
     : rootDER(rootDER)
   {
   }
 
   // The CertPolicyId argument is unused because we don't care about EV.
   Result GetCertTrust(EndEntityOrCA, const CertPolicyId&, Input candidateCert,
                       /*out*/ TrustLevel& trustLevel) override
   {
@@ -283,20 +291,46 @@ public:
     return checker.Check(rootCert, nullptr, keepGoing);
   }
 
   Result IsChainValid(const DERArray&, Time) override
   {
     return Success;
   }
 
+  Result CheckRevocation(EndEntityOrCA, const CertID&, Time, Duration,
+                         /*optional*/ const Input*, /*optional*/ const Input*)
+                         override
+  {
+    return Success;
+  }
+
 private:
   ByteString rootDER;
 };
 
+// A TrustDomain that explicitly fails if CheckRevocation is called.
+class ExpiredCertTrustDomain final : public SingleRootTrustDomain
+{
+public:
+  explicit ExpiredCertTrustDomain(ByteString rootDER)
+    : SingleRootTrustDomain(rootDER)
+  {
+  }
+
+  Result CheckRevocation(EndEntityOrCA, const CertID&, Time, Duration,
+                         /*optional*/ const Input*, /*optional*/ const Input*)
+                         override
+  {
+    ADD_FAILURE();
+    return NotReached("CheckRevocation should not be called",
+                      Result::FATAL_ERROR_LIBRARY_FAILURE);
+  }
+};
+
 TEST_F(pkixbuild, NoRevocationCheckingForExpiredCert)
 {
   const char* rootCN = "Root CA";
   ByteString rootDER(CreateCert(rootCN, rootCN, EndEntityOrCA::MustBeCA,
                                 nullptr));
   EXPECT_FALSE(ENCODING_FAILED(rootDER));
   ExpiredCertTrustDomain expiredCertTrustDomain(rootDER);
 
@@ -469,8 +503,76 @@ TEST_P(pkixbuild_IssuerNameCheck, Matchi
                            KeyUsage::noParticularKeyUsageRequired,
                            KeyPurposeId::id_kp_serverAuth,
                            CertPolicyId::anyPolicy,
                            nullptr/*stapledOCSPResponse*/));
 }
 
 INSTANTIATE_TEST_CASE_P(pkixbuild_IssuerNameCheck, pkixbuild_IssuerNameCheck,
                         testing::ValuesIn(ISSUER_NAME_CHECK_PARAMS));
+
+
+// Records the embedded SCT list extension for later examination.
+class EmbeddedSCTListTestTrustDomain final : public SingleRootTrustDomain
+{
+public:
+  explicit EmbeddedSCTListTestTrustDomain(ByteString rootDER)
+    : SingleRootTrustDomain(rootDER)
+  {
+  }
+
+  virtual void NoteAuxiliaryExtension(AuxiliaryExtension extension,
+                                      Input extensionData) override
+  {
+    if (extension == AuxiliaryExtension::EmbeddedSCTList) {
+      signedCertificateTimestamps = InputToByteString(extensionData);
+    } else {
+      ADD_FAILURE();
+    }
+  }
+
+  ByteString signedCertificateTimestamps;
+};
+
+TEST_F(pkixbuild, CertificateTransparencyExtension)
+{
+  // python security/pkix/tools/DottedOIDToCode.py --tlv
+  //   id-embeddedSctList 1.3.6.1.4.1.11129.2.4.2
+  static const uint8_t tlv_id_embeddedSctList[] = {
+    0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xd6, 0x79, 0x02, 0x04, 0x02
+  };
+  static const uint8_t dummySctList[] = {
+    0x01, 0x02, 0x03, 0x04, 0x05
+  };
+
+  ByteString ctExtension = TLV(der::SEQUENCE,
+    BytesToByteString(tlv_id_embeddedSctList) +
+    Boolean(false) +
+    TLV(der::OCTET_STRING,
+      // SignedCertificateTimestampList structure is encoded as an OCTET STRING
+      // within the X.509v3 extension (see RFC 6962 section 3.3).
+      // pkix decodes it internally and returns the actual structure.
+      TLV(der::OCTET_STRING, BytesToByteString(dummySctList))));
+
+  const char* rootCN = "Root CA";
+  ByteString rootDER(CreateCert(rootCN, rootCN, EndEntityOrCA::MustBeCA));
+  ASSERT_FALSE(ENCODING_FAILED(rootDER));
+
+  ByteString certDER(CreateCert(rootCN, "Cert with SCT list",
+                                EndEntityOrCA::MustBeEndEntity,
+                                nullptr, /*subjectDERToCertDER*/
+                                &ctExtension));
+  ASSERT_FALSE(ENCODING_FAILED(certDER));
+
+  Input certInput;
+  ASSERT_EQ(Success, certInput.Init(certDER.data(), certDER.length()));
+
+  EmbeddedSCTListTestTrustDomain extTrustDomain(rootDER);
+  ASSERT_EQ(Success,
+            BuildCertChain(extTrustDomain, certInput, Now(),
+                           EndEntityOrCA::MustBeEndEntity,
+                           KeyUsage::noParticularKeyUsageRequired,
+                           KeyPurposeId::anyExtendedKeyUsage,
+                           CertPolicyId::anyPolicy,
+                           nullptr /*stapledOCSPResponse*/));
+  ASSERT_EQ(BytesToByteString(dummySctList),
+            extTrustDomain.signedCertificateTimestamps);
+}
--- a/security/pkix/test/gtest/pkixgtest.h
+++ b/security/pkix/test/gtest/pkixgtest.h
@@ -173,16 +173,21 @@ public:
   }
 
   Result NetscapeStepUpMatchesServerAuth(Time, bool&) override
   {
     ADD_FAILURE();
     return NotReached("NetscapeStepUpMatchesServerAuth should not be called",
                       Result::FATAL_ERROR_LIBRARY_FAILURE);
   }
+
+  virtual void NoteAuxiliaryExtension(AuxiliaryExtension, Input) override
+  {
+    ADD_FAILURE();
+  }
 };
 
 class DefaultCryptoTrustDomain : public EverythingFailsByDefaultTrustDomain
 {
   Result DigestBuf(Input item, DigestAlgorithm digestAlg,
                    /*out*/ uint8_t* digestBuf, size_t digestBufLen) override
   {
     return TestDigestBuf(item, digestAlg, digestBuf, digestBufLen);
@@ -223,16 +228,20 @@ class DefaultCryptoTrustDomain : public 
     return Success;
   }
 
   Result NetscapeStepUpMatchesServerAuth(Time, /*out*/ bool& matches) override
   {
     matches = true;
     return Success;
   }
+
+  void NoteAuxiliaryExtension(AuxiliaryExtension, Input) override
+  {
+  }
 };
 
 class DefaultNameMatchingPolicy : public NameMatchingPolicy
 {
 public:
   virtual Result FallBackToCommonName(
     Time, /*out*/ FallBackToSearchWithinSubject& fallBackToCommonName) override
   {
--- a/security/pkix/test/gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp
+++ b/security/pkix/test/gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp
@@ -17,16 +17,17 @@
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
+#include "pkixder.h"
 #include "pkixgtest.h"
 
 using namespace mozilla::pkix;
 using namespace mozilla::pkix::test;
 
 const uint16_t END_ENTITY_MAX_LIFETIME_IN_DAYS = 10;
 
 // Note that CheckRevocation is never called for OCSP signing certificates.
@@ -38,16 +39,29 @@ public:
   Result GetCertTrust(EndEntityOrCA endEntityOrCA, const CertPolicyId&,
                       Input, /*out*/ TrustLevel& trustLevel)
                       /*non-final*/ override
   {
     EXPECT_EQ(endEntityOrCA, EndEntityOrCA::MustBeEndEntity);
     trustLevel = TrustLevel::InheritsTrust;
     return Success;
   }
+
+  virtual void NoteAuxiliaryExtension(AuxiliaryExtension extension,
+                                      Input extensionData) override
+  {
+    if (extension == AuxiliaryExtension::SCTListFromOCSPResponse) {
+      signedCertificateTimestamps = InputToByteString(extensionData);
+    } else {
+      // We do not currently expect to receive any other extension here.
+      ADD_FAILURE();
+    }
+  }
+
+  ByteString signedCertificateTimestamps;
 };
 
 namespace {
 char const* const rootName = "Test CA 1";
 void deleteCertID(CertID* certID) { delete certID; }
 } // namespace
 
 class pkixocsp_VerifyEncodedResponse : public ::testing::Test
@@ -194,29 +208,33 @@ public:
   ByteString CreateEncodedOCSPSuccessfulResponse(
                     OCSPResponseContext::CertStatus certStatus,
                     const CertID& certID,
        /*optional*/ const char* signerName,
                     const TestKeyPair& signerKeyPair,
                     time_t producedAt, time_t thisUpdate,
        /*optional*/ const time_t* nextUpdate,
                     const TestSignatureAlgorithm& signatureAlgorithm,
-       /*optional*/ const ByteString* certs = nullptr)
+       /*optional*/ const ByteString* certs = nullptr,
+       /*optional*/ OCSPResponseExtension* singleExtensions = nullptr,
+       /*optional*/ OCSPResponseExtension* responseExtensions = nullptr)
   {
     OCSPResponseContext context(certID, producedAt);
     if (signerName) {
       context.signerNameDER = CNToDERName(signerName);
       EXPECT_FALSE(ENCODING_FAILED(context.signerNameDER));
     }
     context.signerKeyPair.reset(signerKeyPair.Clone());
     EXPECT_TRUE(context.signerKeyPair.get());
     context.responseStatus = OCSPResponseContext::successful;
     context.producedAt = producedAt;
     context.signatureAlgorithm = signatureAlgorithm;
     context.certs = certs;
+    context.singleExtensions = singleExtensions;
+    context.responseExtensions = responseExtensions;
 
     context.certStatus = static_cast<uint8_t>(certStatus);
     context.thisUpdate = thisUpdate;
     context.nextUpdate = nextUpdate ? *nextUpdate : 0;
     context.includeNextUpdate = nextUpdate != nullptr;
 
     return CreateEncodedOCSPResponse(context);
   }
@@ -392,16 +410,56 @@ TEST_F(pkixocsp_VerifyEncodedResponse_su
     ASSERT_EQ(Result::ERROR_OCSP_OLD_RESPONSE,
               VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID,
                                         noLongerValid, END_ENTITY_MAX_LIFETIME_IN_DAYS,
                                         response, expired));
     ASSERT_TRUE(expired);
   }
 }
 
+TEST_F(pkixocsp_VerifyEncodedResponse_successful, ct_extension)
+{
+  // python DottedOIDToCode.py --tlv
+  //   id_ocsp_singleExtensionSctList 1.3.6.1.4.1.11129.2.4.5
+  static const uint8_t tlv_id_ocsp_singleExtensionSctList[] = {
+    0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xd6, 0x79, 0x02, 0x04, 0x05
+  };
+  static const uint8_t dummySctList[] = {
+    0x01, 0x02, 0x03, 0x04, 0x05
+  };
+
+  OCSPResponseExtension ctExtension;
+  ctExtension.id = BytesToByteString(tlv_id_ocsp_singleExtensionSctList);
+  // SignedCertificateTimestampList structure is encoded as an OCTET STRING
+  // within the extension value (see RFC 6962 section 3.3).
+  // pkix decodes it internally and returns the actual structure.
+  ctExtension.value = TLV(der::OCTET_STRING, BytesToByteString(dummySctList));
+
+  ByteString responseString(
+               CreateEncodedOCSPSuccessfulResponse(
+                         OCSPResponseContext::good, *endEntityCertID, byKey,
+                         *rootKeyPair, oneDayBeforeNow,
+                         oneDayBeforeNow, &oneDayAfterNow,
+                         sha256WithRSAEncryption(),
+                         /*certs*/ nullptr,
+                         &ctExtension));
+  Input response;
+  ASSERT_EQ(Success,
+            response.Init(responseString.data(), responseString.length()));
+
+  bool expired;
+  ASSERT_EQ(Success,
+            VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID,
+                                      Now(), END_ENTITY_MAX_LIFETIME_IN_DAYS,
+                                      response, expired));
+  ASSERT_FALSE(expired);
+  ASSERT_EQ(BytesToByteString(dummySctList),
+            trustDomain.signedCertificateTimestamps);
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // indirect responses (signed by a delegated OCSP responder cert)
 
 class pkixocsp_VerifyEncodedResponse_DelegatedResponder
   : public pkixocsp_VerifyEncodedResponse_successful
 {
 protected:
   // certSubjectName should be unique for each call. This way, we avoid any
--- a/security/pkix/test/lib/pkixtestutil.cpp
+++ b/security/pkix/test/lib/pkixtestutil.cpp
@@ -139,22 +139,31 @@ TLV(uint8_t tag, size_t length, const By
     // It is MUCH more convenient for TLV to be infallible than for it to have
     // "proper" error handling.
     abort();
   }
   result.append(value);
   return result;
 }
 
+OCSPResponseExtension::OCSPResponseExtension()
+  : id()
+  , critical(false)
+  , value()
+  , next(nullptr)
+{
+}
+
 OCSPResponseContext::OCSPResponseContext(const CertID& certID, time_t time)
   : certID(certID)
   , responseStatus(successful)
   , skipResponseBytes(false)
   , producedAt(time)
-  , extensions(nullptr)
+  , singleExtensions(nullptr)
+  , responseExtensions(nullptr)
   , includeEmptyExtensions(false)
   , signatureAlgorithm(sha256WithRSAEncryption())
   , badSignature(false)
   , certs(nullptr)
 
   , certStatus(good)
   , revocationTime(0)
   , thisUpdate(time)
@@ -892,20 +901,20 @@ OCSPExtension(OCSPResponseExtension& ext
   encoded.append(value);
   return TLV(der::SEQUENCE, encoded);
 }
 
 // Extensions ::= [1] {
 //   SEQUENCE OF Extension
 // }
 static ByteString
-Extensions(OCSPResponseContext& context)
+OCSPExtensions(OCSPResponseExtension* extensions)
 {
   ByteString value;
-  for (OCSPResponseExtension* extension = context.extensions;
+  for (OCSPResponseExtension* extension = extensions;
        extension; extension = extension->next) {
     ByteString extensionEncoded(OCSPExtension(*extension));
     if (ENCODING_FAILED(extensionEncoded)) {
       return ByteString();
     }
     value.append(extensionEncoded);
   }
   ByteString sequence(TLV(der::SEQUENCE, value));
@@ -930,18 +939,18 @@ ResponseData(OCSPResponseContext& contex
     return ByteString();
   }
   ByteString response(SingleResponse(context));
   if (ENCODING_FAILED(response)) {
     return ByteString();
   }
   ByteString responses(TLV(der::SEQUENCE, response));
   ByteString responseExtensions;
-  if (context.extensions || context.includeEmptyExtensions) {
-    responseExtensions = Extensions(context);
+  if (context.responseExtensions || context.includeEmptyExtensions) {
+    responseExtensions = OCSPExtensions(context.responseExtensions);
   }
 
   ByteString value;
   value.append(responderID);
   value.append(producedAtEncoded);
   value.append(responses);
   value.append(responseExtensions);
   return TLV(der::SEQUENCE, value);
@@ -1010,22 +1019,27 @@ SingleResponse(OCSPResponseContext& cont
   if (context.includeNextUpdate) {
     ByteString nextUpdateEncoded(TimeToGeneralizedTime(context.nextUpdate));
     if (ENCODING_FAILED(nextUpdateEncoded)) {
       return ByteString();
     }
     nextUpdateEncodedNested = TLV(der::CONSTRUCTED | der::CONTEXT_SPECIFIC | 0,
                                   nextUpdateEncoded);
   }
+  ByteString singleExtensions;
+  if (context.singleExtensions || context.includeEmptyExtensions) {
+    singleExtensions = OCSPExtensions(context.singleExtensions);
+  }
 
   ByteString value;
   value.append(certID);
   value.append(certStatus);
   value.append(thisUpdateEncoded);
   value.append(nextUpdateEncodedNested);
+  value.append(singleExtensions);
   return TLV(der::SEQUENCE, value);
 }
 
 // CertID          ::=     SEQUENCE {
 //        hashAlgorithm       AlgorithmIdentifier,
 //        issuerNameHash      OCTET STRING, -- Hash of issuer's DN
 //        issuerKeyHash       OCTET STRING, -- Hash of issuer's public key
 //        serialNumber        CertificateSerialNumber }
--- a/security/pkix/test/lib/pkixtestutil.h
+++ b/security/pkix/test/lib/pkixtestutil.h
@@ -371,16 +371,18 @@ ByteString CreateEncodedBasicConstraints
 ByteString CreateEncodedEKUExtension(Input eku, Critical critical);
 
 ///////////////////////////////////////////////////////////////////////////////
 // Encode OCSP responses
 
 class OCSPResponseExtension final
 {
 public:
+  OCSPResponseExtension();
+
   ByteString id;
   bool critical;
   ByteString value;
   OCSPResponseExtension* next;
 };
 
 class OCSPResponseContext final
 {
@@ -407,17 +409,20 @@ public:
 
   // responderID
   ByteString signerNameDER; // If set, responderID will use the byName
                             // form; otherwise responderID will use the
                             // byKeyHash form.
 
   std::time_t producedAt;
 
-  OCSPResponseExtension* extensions;
+  // SingleResponse extensions (for the certID given in the constructor).
+  OCSPResponseExtension* singleExtensions;
+  // ResponseData extensions.
+  OCSPResponseExtension* responseExtensions;
   bool includeEmptyExtensions; // If true, include the extension wrapper
                                // regardless of if there are any actual
                                // extensions.
   ScopedTestKeyPair signerKeyPair;
   TestSignatureAlgorithm signatureAlgorithm;
   bool badSignature; // If true, alter the signature to fail verification
   const ByteString* certs; // optional; array terminated by an empty string