diff --git a/security/apps/AppTrustDomain.cpp b/security/apps/AppTrustDomain.cpp index 81b0f3868b9f..9131a701b674 100644 --- a/security/apps/AppTrustDomain.cpp +++ b/security/apps/AppTrustDomain.cpp @@ -378,4 +378,10 @@ AppTrustDomain::NetscapeStepUpMatchesServerAuth(Time /*notBefore*/, return Success; } +void +AppTrustDomain::NoteAuxiliaryExtension(AuxiliaryExtension /*extension*/, + Input /*extensionData*/) +{ +} + } } // namespace mozilla::psm diff --git a/security/apps/AppTrustDomain.h b/security/apps/AppTrustDomain.h index 9306f7f62fe8..d435ed971286 100644 --- a/security/apps/AppTrustDomain.h +++ b/security/apps/AppTrustDomain.h @@ -64,6 +64,9 @@ public: 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, diff --git a/security/certverifier/NSSCertDBTrustDomain.cpp b/security/certverifier/NSSCertDBTrustDomain.cpp index 06cfa6b54c7f..5a754fac9274 100644 --- a/security/certverifier/NSSCertDBTrustDomain.cpp +++ b/security/certverifier/NSSCertDBTrustDomain.cpp @@ -958,6 +958,12 @@ NSSCertDBTrustDomain::NetscapeStepUpMatchesServerAuth(Time notBefore, return Result::FATAL_ERROR_LIBRARY_FAILURE; } +void +NSSCertDBTrustDomain::NoteAuxiliaryExtension(AuxiliaryExtension /*extension*/, + Input /*extensionData*/) +{ +} + SECStatus InitializeNSS(const char* dir, bool readOnly, bool loadPKCS11Modules) { diff --git a/security/certverifier/NSSCertDBTrustDomain.h b/security/certverifier/NSSCertDBTrustDomain.h index 3ceacd85b054..ed00e647ece1 100644 --- a/security/certverifier/NSSCertDBTrustDomain.h +++ b/security/certverifier/NSSCertDBTrustDomain.h @@ -141,6 +141,10 @@ public: 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; diff --git a/security/certverifier/OCSPVerificationTrustDomain.cpp b/security/certverifier/OCSPVerificationTrustDomain.cpp index 944c048be35e..b610690d2ed1 100644 --- a/security/certverifier/OCSPVerificationTrustDomain.cpp +++ b/security/certverifier/OCSPVerificationTrustDomain.cpp @@ -108,6 +108,13 @@ OCSPVerificationTrustDomain::NetscapeStepUpMatchesServerAuth(Time notBefore, return mCertDBTrustDomain.NetscapeStepUpMatchesServerAuth(notBefore, matches); } +void +OCSPVerificationTrustDomain::NoteAuxiliaryExtension( + AuxiliaryExtension extension, Input extensionData) +{ + mCertDBTrustDomain.NoteAuxiliaryExtension(extension, extensionData); +} + Result OCSPVerificationTrustDomain::DigestBuf( Input item, DigestAlgorithm digestAlg, diff --git a/security/certverifier/OCSPVerificationTrustDomain.h b/security/certverifier/OCSPVerificationTrustDomain.h index a7e24f54af89..c32dbe41b2d9 100644 --- a/security/certverifier/OCSPVerificationTrustDomain.h +++ b/security/certverifier/OCSPVerificationTrustDomain.h @@ -73,6 +73,10 @@ public: 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; }; diff --git a/security/manager/ssl/CSTrustDomain.cpp b/security/manager/ssl/CSTrustDomain.cpp index 18fe04c823d1..90008038e5f0 100644 --- a/security/manager/ssl/CSTrustDomain.cpp +++ b/security/manager/ssl/CSTrustDomain.cpp @@ -215,6 +215,12 @@ CSTrustDomain::NetscapeStepUpMatchesServerAuth(Time notBefore, return Success; } +void +CSTrustDomain::NoteAuxiliaryExtension(AuxiliaryExtension /*extension*/, + Input /*extensionData*/) +{ +} + Result CSTrustDomain::DigestBuf(Input item, DigestAlgorithm digestAlg, /*out*/ uint8_t* digestBuf, size_t digestBufLen) diff --git a/security/manager/ssl/CSTrustDomain.h b/security/manager/ssl/CSTrustDomain.h index 4d92785a23b1..29a82539b441 100644 --- a/security/manager/ssl/CSTrustDomain.h +++ b/security/manager/ssl/CSTrustDomain.h @@ -62,6 +62,9 @@ public: 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, diff --git a/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp b/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp index 5328162d0834..5c99edadeb93 100644 --- a/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp +++ b/security/manager/ssl/tests/unit/tlsserver/lib/OCSPCommon.cpp @@ -184,7 +184,7 @@ GetOCSPResponseForType(OCSPResponseType aORT, const UniqueCERTCertificate& aCert 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; diff --git a/security/pkix/include/pkix/pkixtypes.h b/security/pkix/include/pkix/pkixtypes.h index eeab664e0196..0a8f770a12cf 100644 --- a/security/pkix/include/pkix/pkixtypes.h +++ b/security/pkix/include/pkix/pkixtypes.h @@ -105,6 +105,21 @@ enum class TrustLevel 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. // @@ -337,6 +352,13 @@ public: 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. diff --git a/security/pkix/lib/pkixbuild.cpp b/security/pkix/lib/pkixbuild.cpp index 99942cfa13bb..fdbd9b59f083 100644 --- a/security/pkix/lib/pkixbuild.cpp +++ b/security/pkix/lib/pkixbuild.cpp @@ -244,6 +244,20 @@ PathBuildingStep::Check(Input potentialIssuerDER, 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); diff --git a/security/pkix/lib/pkixcert.cpp b/security/pkix/lib/pkixcert.cpp index 1cb452c208e9..ffa58fd3b515 100644 --- a/security/pkix/lib/pkixcert.cpp +++ b/security/pkix/lib/pkixcert.cpp @@ -223,6 +223,11 @@ BackCert::RememberExtension(Reader& extnID, Input extnValue, 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; @@ -269,6 +274,8 @@ BackCert::RememberExtension(Reader& extnID, Input extnValue, 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 @@ -300,4 +307,17 @@ BackCert::RememberExtension(Reader& extnID, Input extnValue, 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 diff --git a/security/pkix/lib/pkixocsp.cpp b/security/pkix/lib/pkixocsp.cpp index b22fd285a030..06cb53bc45ba 100644 --- a/security/pkix/lib/pkixocsp.cpp +++ b/security/pkix/lib/pkixocsp.cpp @@ -76,6 +76,8 @@ public: 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 @@ -168,6 +170,9 @@ static inline Result ResponseData( 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); @@ -330,6 +335,16 @@ VerifyEncodedOCSPResponse(TrustDomain& trustDomain, const struct CertID& certID, 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; @@ -651,9 +666,15 @@ SingleResponse(Reader& input, Context& context) 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; } @@ -826,6 +847,36 @@ ExtensionNotUnderstood(Reader& /*extnID*/, Input /*extnValue*/, 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; diff --git a/security/pkix/lib/pkixutil.h b/security/pkix/lib/pkixutil.h index 996ffe9b8a2a..08581e943bd8 100644 --- a/security/pkix/lib/pkixutil.h +++ b/security/pkix/lib/pkixutil.h @@ -106,6 +106,10 @@ public: { return MaybeInput(requiredTLSFeatures); } + const Input* GetSignedCertificateTimestamps() const + { + return MaybeInput(signedCertificateTimestamps); + } private: const Input der; @@ -149,6 +153,7 @@ private: Input subjectAltName; Input criticalNetscapeCertificateType; Input requiredTLSFeatures; + Input signedCertificateTimestamps; // RFC 6962 (Certificate Transparency) Result RememberExtension(Reader& extnID, Input extnValue, bool critical, /*out*/ bool& understood); @@ -197,6 +202,12 @@ private: 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) { diff --git a/security/pkix/test/gtest/pkixbuild_tests.cpp b/security/pkix/test/gtest/pkixbuild_tests.cpp index 52aebad65c87..0b7750ef64ff 100644 --- a/security/pkix/test/gtest/pkixbuild_tests.cpp +++ b/security/pkix/test/gtest/pkixbuild_tests.cpp @@ -31,11 +31,13 @@ #endif #include +#include #if defined(_MSC_VER) && _MSC_VER < 1900 #pragma warning(pop) #endif +#include "pkixder.h" #include "pkixgtest.h" using namespace mozilla::pkix; @@ -46,7 +48,8 @@ CreateCert(const char* issuerCN, // null means "empty name" const char* subjectCN, // null means "empty name" EndEntityOrCA endEntityOrCA, /*optional modified*/ std::map* - subjectDERToCertDER = nullptr) + subjectDERToCertDER = nullptr, + /*optional*/ const ByteString* extension = nullptr) { static long serialNumberValue = 0; ++serialNumberValue; @@ -56,18 +59,23 @@ CreateCert(const char* issuerCN, // null means "empty name" ByteString issuerDER(issuerCN ? CNToDERName(issuerCN) : Name(ByteString())); ByteString subjectDER(subjectCN ? CNToDERName(subjectCN) : Name(ByteString())); - ByteString extensions[2]; + std::vector 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)); @@ -239,15 +247,15 @@ TEST_F(pkixbuild, BeyondMaxAcceptableCertChainLength) } } -// 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) { } @@ -288,10 +296,36 @@ public: 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"; @@ -474,3 +508,71 @@ TEST_P(pkixbuild_IssuerNameCheck, MatchingName) 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); +} diff --git a/security/pkix/test/gtest/pkixgtest.h b/security/pkix/test/gtest/pkixgtest.h index cdc5b46e6439..1ec8727e24c0 100644 --- a/security/pkix/test/gtest/pkixgtest.h +++ b/security/pkix/test/gtest/pkixgtest.h @@ -178,6 +178,11 @@ public: return NotReached("NetscapeStepUpMatchesServerAuth should not be called", Result::FATAL_ERROR_LIBRARY_FAILURE); } + + virtual void NoteAuxiliaryExtension(AuxiliaryExtension, Input) override + { + ADD_FAILURE(); + } }; class DefaultCryptoTrustDomain : public EverythingFailsByDefaultTrustDomain @@ -228,6 +233,10 @@ class DefaultCryptoTrustDomain : public EverythingFailsByDefaultTrustDomain matches = true; return Success; } + + void NoteAuxiliaryExtension(AuxiliaryExtension, Input) override + { + } }; class DefaultNameMatchingPolicy : public NameMatchingPolicy diff --git a/security/pkix/test/gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp b/security/pkix/test/gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp index be9f109d9bef..d7dab09d90c1 100644 --- a/security/pkix/test/gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp +++ b/security/pkix/test/gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp @@ -22,6 +22,7 @@ * limitations under the License. */ +#include "pkixder.h" #include "pkixgtest.h" using namespace mozilla::pkix; @@ -43,6 +44,19 @@ public: 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 { @@ -199,7 +213,9 @@ public: 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) { @@ -212,6 +228,8 @@ public: context.producedAt = producedAt; context.signatureAlgorithm = signatureAlgorithm; context.certs = certs; + context.singleExtensions = singleExtensions; + context.responseExtensions = responseExtensions; context.certStatus = static_cast(certStatus); context.thisUpdate = thisUpdate; @@ -397,6 +415,46 @@ TEST_F(pkixocsp_VerifyEncodedResponse_successful, check_validThrough) } } +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) diff --git a/security/pkix/test/lib/pkixtestutil.cpp b/security/pkix/test/lib/pkixtestutil.cpp index 7fd94192dc97..decdee09a113 100644 --- a/security/pkix/test/lib/pkixtestutil.cpp +++ b/security/pkix/test/lib/pkixtestutil.cpp @@ -144,12 +144,21 @@ TLV(uint8_t tag, size_t length, const ByteString& 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) @@ -897,10 +906,10 @@ OCSPExtension(OCSPResponseExtension& extension) // 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)) { @@ -935,8 +944,8 @@ ResponseData(OCSPResponseContext& context) } 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; @@ -1015,12 +1024,17 @@ SingleResponse(OCSPResponseContext& context) 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); } diff --git a/security/pkix/test/lib/pkixtestutil.h b/security/pkix/test/lib/pkixtestutil.h index 5d5746f73de2..b36f1f8ad95a 100644 --- a/security/pkix/test/lib/pkixtestutil.h +++ b/security/pkix/test/lib/pkixtestutil.h @@ -376,6 +376,8 @@ ByteString CreateEncodedEKUExtension(Input eku, Critical critical); class OCSPResponseExtension final { public: + OCSPResponseExtension(); + ByteString id; bool critical; ByteString value; @@ -412,7 +414,10 @@ public: 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.