--- 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