Bug 1275238 - Certificate Transparency support in mozilla::pkix; r=keeler
authorSergei Chernov <sergei.cv@ndivi.com>
Wed, 15 Jun 2016 11:11:00 +0300
changeset 303510 990aca9e4d11f4973f71e438f324579e3f217cae
parent 303509 4069c4e3a0da07534acd4d6a82fd365aa43723f4
child 303511 66a937c6ca0e6264068a1b1426b0983e76f7d9c6
push id79111
push usercbook@mozilla.com
push dateMon, 04 Jul 2016 14:27:02 +0000
treeherdermozilla-inbound@990aca9e4d11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskeeler
bugs1275238
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 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