diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /security/nss/gtests/mozpkix_gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'security/nss/gtests/mozpkix_gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp')
-rw-r--r-- | security/nss/gtests/mozpkix_gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp | 1154 |
1 files changed, 1154 insertions, 0 deletions
diff --git a/security/nss/gtests/mozpkix_gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp b/security/nss/gtests/mozpkix_gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp new file mode 100644 index 0000000000..c7e82368d6 --- /dev/null +++ b/security/nss/gtests/mozpkix_gtest/pkixocsp_VerifyEncodedOCSPResponse.cpp @@ -0,0 +1,1154 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This code is made available to you under your choice of the following sets + * of licensing terms: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +/* Copyright 2014 Mozilla Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 "pkixgtest.h" + +#include "mozpkix/pkixder.h" + +#include "secoid.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. +class OCSPTestTrustDomain : public DefaultCryptoTrustDomain +{ +public: + OCSPTestTrustDomain() { } + + 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"; +} // namespace + +class pkixocsp_VerifyEncodedResponse : public ::testing::Test +{ +public: + static void SetUpTestSuite() + { + rootKeyPair.reset(GenerateKeyPair()); + if (!rootKeyPair) { + abort(); + } + } + + void SetUp() + { + rootNameDER = CNToDERName(rootName); + if (ENCODING_FAILED(rootNameDER)) { + abort(); + } + Input rootNameDERInput; + if (rootNameDERInput.Init(rootNameDER.data(), rootNameDER.length()) + != Success) { + abort(); + } + + serialNumberDER = + CreateEncodedSerialNumber(static_cast<long>(++rootIssuedCount)); + if (ENCODING_FAILED(serialNumberDER)) { + abort(); + } + Input serialNumberDERInput; + if (serialNumberDERInput.Init(serialNumberDER.data(), + serialNumberDER.length()) != Success) { + abort(); + } + + Input rootSPKIDER; + if (rootSPKIDER.Init(rootKeyPair->subjectPublicKeyInfo.data(), + rootKeyPair->subjectPublicKeyInfo.length()) + != Success) { + abort(); + } + endEntityCertID.reset(new (std::nothrow) CertID(rootNameDERInput, rootSPKIDER, + serialNumberDERInput)); + if (!endEntityCertID) { + abort(); + } + } + + static ScopedTestKeyPair rootKeyPair; + static uint32_t rootIssuedCount; + OCSPTestTrustDomain trustDomain; + + // endEntityCertID references rootKeyPair, rootNameDER, and serialNumberDER. + ByteString rootNameDER; + ByteString serialNumberDER; + // endEntityCertID references rootKeyPair, rootNameDER, and serialNumberDER. + ScopedCertID endEntityCertID; +}; + +/*static*/ ScopedTestKeyPair pkixocsp_VerifyEncodedResponse::rootKeyPair; +/*static*/ uint32_t pkixocsp_VerifyEncodedResponse::rootIssuedCount = 0; + +/////////////////////////////////////////////////////////////////////////////// +// responseStatus + +struct WithoutResponseBytes +{ + uint8_t responseStatus; + Result expectedError; +}; + +static const WithoutResponseBytes WITHOUT_RESPONSEBYTES[] = { + { OCSPResponseContext::successful, Result::ERROR_OCSP_MALFORMED_RESPONSE }, + { OCSPResponseContext::malformedRequest, Result::ERROR_OCSP_MALFORMED_REQUEST }, + { OCSPResponseContext::internalError, Result::ERROR_OCSP_SERVER_ERROR }, + { OCSPResponseContext::tryLater, Result::ERROR_OCSP_TRY_SERVER_LATER }, + { 4/*unused*/, Result::ERROR_OCSP_UNKNOWN_RESPONSE_STATUS }, + { OCSPResponseContext::sigRequired, Result::ERROR_OCSP_REQUEST_NEEDS_SIG }, + { OCSPResponseContext::unauthorized, Result::ERROR_OCSP_UNAUTHORIZED_REQUEST }, + { OCSPResponseContext::unauthorized + 1, + Result::ERROR_OCSP_UNKNOWN_RESPONSE_STATUS + }, +}; + +class pkixocsp_VerifyEncodedResponse_WithoutResponseBytes + : public pkixocsp_VerifyEncodedResponse + , public ::testing::WithParamInterface<WithoutResponseBytes> +{ +protected: + ByteString CreateEncodedOCSPErrorResponse(uint8_t status) + { + static const Input EMPTY; + OCSPResponseContext context(CertID(EMPTY, EMPTY, EMPTY), + oneDayBeforeNow); + context.responseStatus = status; + context.skipResponseBytes = true; + return CreateEncodedOCSPResponse(context); + } +}; + +TEST_P(pkixocsp_VerifyEncodedResponse_WithoutResponseBytes, CorrectErrorCode) +{ + ByteString + responseString(CreateEncodedOCSPErrorResponse(GetParam().responseStatus)); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(GetParam().expectedError, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); +} + +INSTANTIATE_TEST_SUITE_P(pkixocsp_VerifyEncodedResponse_WithoutResponseBytes, + pkixocsp_VerifyEncodedResponse_WithoutResponseBytes, + testing::ValuesIn(WITHOUT_RESPONSEBYTES)); + +/////////////////////////////////////////////////////////////////////////////// +// "successful" responses + +namespace { + +// Alias for nullptr to aid readability in the code below. +static const char* byKey = nullptr; + +} // namespace + +class pkixocsp_VerifyEncodedResponse_successful + : public pkixocsp_VerifyEncodedResponse +{ +public: + void SetUp() + { + pkixocsp_VerifyEncodedResponse::SetUp(); + } + + static void SetUpTestSuite() + { + pkixocsp_VerifyEncodedResponse::SetUpTestSuite(); + } + + 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*/ OCSPResponseExtension* singleExtensions = nullptr, + /*optional*/ OCSPResponseExtension* responseExtensions = nullptr, + /*optional*/ DigestAlgorithm certIDHashAlgorithm = DigestAlgorithm::sha1, + /*optional*/ ByteString certIDHashAlgorithmEncoded = ByteString()) + { + OCSPResponseContext context(certID, producedAt); + context.certIDHashAlgorithm = certIDHashAlgorithm; + context.certIDHashAlgorithmEncoded = certIDHashAlgorithmEncoded; + 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); + } +}; + +TEST_F(pkixocsp_VerifyEncodedResponse_successful, good_byKey) +{ + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, byKey, + *rootKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + sha256WithRSAEncryption())); + 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); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_successful, good_byName) +{ + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, rootName, + *rootKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + sha256WithRSAEncryption())); + 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); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_successful, good_byKey_without_nextUpdate) +{ + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, byKey, + *rootKeyPair, oneDayBeforeNow, + oneDayBeforeNow, nullptr, + sha256WithRSAEncryption())); + 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); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_successful, revoked) +{ + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::revoked, *endEntityCertID, byKey, + *rootKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + sha256WithRSAEncryption())); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_REVOKED_CERTIFICATE, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_successful, unknown) +{ + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::unknown, *endEntityCertID, byKey, + *rootKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + sha256WithRSAEncryption())); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_UNKNOWN_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_successful, + good_unsupportedSignatureAlgorithm) +{ + PRUint32 policyMd5; + ASSERT_EQ(SECSuccess,NSS_GetAlgorithmPolicy(SEC_OID_MD5, &policyMd5)); + + /* our encode won't work if MD5 isn't allowed by policy */ + ASSERT_EQ(SECSuccess, + NSS_SetAlgorithmPolicy(SEC_OID_MD5, NSS_USE_ALG_IN_SIGNATURE, 0)); + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, byKey, + *rootKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + md5WithRSAEncryption())); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + /* now restore the existing policy */ + ASSERT_EQ(SECSuccess, + NSS_SetAlgorithmPolicy(SEC_OID_MD5, policyMd5, NSS_USE_ALG_IN_SIGNATURE)); + bool expired; + ASSERT_EQ(Result::ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, + Now(), END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +// Added for bug 1079436. The output variable validThrough represents the +// latest time for which VerifyEncodedOCSPResponse will succeed, which is +// different from the nextUpdate time in the OCSP response due to the slop we +// add for time comparisons to deal with clock skew. +TEST_F(pkixocsp_VerifyEncodedResponse_successful, check_validThrough) +{ + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, byKey, + *rootKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + sha256WithRSAEncryption())); + Time validThrough(Time::uninitialized); + { + 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, nullptr, + &validThrough)); + ASSERT_FALSE(expired); + // The response was created to be valid until one day after now, so the + // value we got for validThrough should be after that. + Time oneDayAfterNowAsPKIXTime( + TimeFromEpochInSeconds(static_cast<uint64_t>(oneDayAfterNow))); + ASSERT_TRUE(validThrough > oneDayAfterNowAsPKIXTime); + } + { + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + // Given validThrough from a previous verification, this response should be + // valid through that time. + ASSERT_EQ(Success, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, + validThrough, END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); + } + { + Time noLongerValid(validThrough); + ASSERT_EQ(Success, noLongerValid.AddSeconds(1)); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + // The verification time is now after when the response will be considered + // valid. + 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); +} + +struct CertIDHashAlgorithm +{ + DigestAlgorithm hashAlgorithm; + ByteString encodedHashAlgorithm; + Result expectedResult; +}; + +// python DottedOIDToCode.py --alg id-sha1 1.3.14.3.2.26 +static const uint8_t alg_id_sha1[] = { + 0x30, 0x07, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a +}; +// python DottedOIDToCode.py --alg id-sha256 2.16.840.1.101.3.4.2.1 +static const uint8_t alg_id_sha256[] = { + 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01 +}; +static const uint8_t not_an_encoded_hash_oid[] = { + 0x01, 0x02, 0x03, 0x04 +}; + +static const CertIDHashAlgorithm CERTID_HASH_ALGORITHMS[] = { + { DigestAlgorithm::sha1, ByteString(), Success }, + { DigestAlgorithm::sha256, ByteString(), Success }, + { DigestAlgorithm::sha384, ByteString(), Success }, + { DigestAlgorithm::sha512, ByteString(), Success }, + { DigestAlgorithm::sha256, BytesToByteString(alg_id_sha1), + Result::ERROR_OCSP_MALFORMED_RESPONSE }, + { DigestAlgorithm::sha1, BytesToByteString(alg_id_sha256), + Result::ERROR_OCSP_MALFORMED_RESPONSE }, + { DigestAlgorithm::sha1, BytesToByteString(not_an_encoded_hash_oid), + Result::ERROR_OCSP_MALFORMED_RESPONSE }, +}; + +class pkixocsp_VerifyEncodedResponse_CertIDHashAlgorithm + : public pkixocsp_VerifyEncodedResponse_successful + , public ::testing::WithParamInterface<CertIDHashAlgorithm> +{ +}; + +TEST_P(pkixocsp_VerifyEncodedResponse_CertIDHashAlgorithm, CertIDHashAlgorithm) +{ + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, byKey, + *rootKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + sha256WithRSAEncryption(), + nullptr, + nullptr, + nullptr, + GetParam().hashAlgorithm, + GetParam().encodedHashAlgorithm)); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(GetParam().expectedResult, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, + Now(), END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +INSTANTIATE_TEST_SUITE_P(pkixocsp_VerifyEncodedResponse_CertIDHashAlgorithm, + pkixocsp_VerifyEncodedResponse_CertIDHashAlgorithm, + testing::ValuesIn(CERTID_HASH_ALGORITHMS)); + +/////////////////////////////////////////////////////////////////////////////// +// 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 + // issues with NSS caching the certificates internally. For the same reason, + // we generate a new keypair on each call. Either one of these should be + // sufficient to avoid issues with the NSS cache, but we do both to be + // cautious. + // + // signerName should be byKey to use the byKey ResponderID construction, or + // another value (usually equal to certSubjectName) to use the byName + // ResponderID construction. + // + // certSignatureAlgorithm specifies the signature algorithm that the + // certificate will be signed with, not the OCSP response. + // + // If signerEKU is omitted, then the certificate will have the + // id-kp-OCSPSigning EKU. If signerEKU is SEC_OID_UNKNOWN then it will not + // have any EKU extension. Otherwise, the certificate will have the given + // EKU. + ByteString CreateEncodedIndirectOCSPSuccessfulResponse( + const char* certSubjectName, + OCSPResponseContext::CertStatus certStatus, + const char* signerName, + const TestSignatureAlgorithm& certSignatureAlgorithm, + /*optional*/ const Input* signerEKUDER = &OCSPSigningEKUDER, + /*optional, out*/ ByteString* signerDEROut = nullptr) + { + assert(certSubjectName); + + const ByteString extensions[] = { + signerEKUDER + ? CreateEncodedEKUExtension(*signerEKUDER, Critical::No) + : ByteString(), + ByteString() + }; + ScopedTestKeyPair signerKeyPair(GenerateKeyPair()); + ByteString signerDER(CreateEncodedCertificate( + ++rootIssuedCount, certSignatureAlgorithm, + rootName, oneDayBeforeNow, oneDayAfterNow, + certSubjectName, *signerKeyPair, + signerEKUDER ? extensions : nullptr, + *rootKeyPair)); + EXPECT_FALSE(ENCODING_FAILED(signerDER)); + if (signerDEROut) { + *signerDEROut = signerDER; + } + + ByteString signerNameDER; + if (signerName) { + signerNameDER = CNToDERName(signerName); + EXPECT_FALSE(ENCODING_FAILED(signerNameDER)); + } + ByteString certs[] = { signerDER, ByteString() }; + return CreateEncodedOCSPSuccessfulResponse(certStatus, *endEntityCertID, + signerName, *signerKeyPair, + oneDayBeforeNow, + oneDayBeforeNow, + &oneDayAfterNow, + sha256WithRSAEncryption(), + certs); + } + + static ByteString CreateEncodedCertificate(uint32_t serialNumber, + const TestSignatureAlgorithm& signatureAlg, + const char* issuer, + time_t notBefore, + time_t notAfter, + const char* subject, + const TestKeyPair& subjectKeyPair, + /*optional*/ const ByteString* extensions, + const TestKeyPair& signerKeyPair) + { + ByteString serialNumberDER(CreateEncodedSerialNumber( + static_cast<long>(serialNumber))); + if (ENCODING_FAILED(serialNumberDER)) { + return ByteString(); + } + ByteString issuerDER(CNToDERName(issuer)); + if (ENCODING_FAILED(issuerDER)) { + return ByteString(); + } + ByteString subjectDER(CNToDERName(subject)); + if (ENCODING_FAILED(subjectDER)) { + return ByteString(); + } + return ::mozilla::pkix::test::CreateEncodedCertificate( + v3, signatureAlg, serialNumberDER, + issuerDER, notBefore, notAfter, + subjectDER, subjectKeyPair, extensions, + signerKeyPair, signatureAlg); + } + + static const Input OCSPSigningEKUDER; +}; + +/*static*/ const Input pkixocsp_VerifyEncodedResponse_DelegatedResponder:: + OCSPSigningEKUDER(tlv_id_kp_OCSPSigning); + +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, good_byKey) +{ + ByteString responseString( + CreateEncodedIndirectOCSPSuccessfulResponse( + "good_indirect_byKey", OCSPResponseContext::good, + byKey, sha256WithRSAEncryption())); + 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); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, good_byName) +{ + ByteString responseString( + CreateEncodedIndirectOCSPSuccessfulResponse( + "good_indirect_byName", OCSPResponseContext::good, + "good_indirect_byName", sha256WithRSAEncryption())); + 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); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, + good_byKey_missing_signer) +{ + ScopedTestKeyPair missingSignerKeyPair(GenerateKeyPair()); + ASSERT_TRUE(missingSignerKeyPair.get()); + + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, byKey, + *missingSignerKeyPair, oneDayBeforeNow, + oneDayBeforeNow, nullptr, + sha256WithRSAEncryption())); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, + good_byName_missing_signer) +{ + ScopedTestKeyPair missingSignerKeyPair(GenerateKeyPair()); + ASSERT_TRUE(missingSignerKeyPair.get()); + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, + "missing", *missingSignerKeyPair, + oneDayBeforeNow, oneDayBeforeNow, nullptr, + sha256WithRSAEncryption())); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, good_expired) +{ + static const char* signerName = "good_indirect_expired"; + + const ByteString extensions[] = { + CreateEncodedEKUExtension(OCSPSigningEKUDER, Critical::No), + ByteString() + }; + + ScopedTestKeyPair signerKeyPair(GenerateKeyPair()); + ByteString signerDER(CreateEncodedCertificate( + ++rootIssuedCount, sha256WithRSAEncryption(), + rootName, + tenDaysBeforeNow, + twoDaysBeforeNow, + signerName, *signerKeyPair, extensions, + *rootKeyPair)); + ASSERT_FALSE(ENCODING_FAILED(signerDER)); + + ByteString certs[] = { signerDER, ByteString() }; + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, + signerName, *signerKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + sha256WithRSAEncryption(), certs)); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, good_future) +{ + static const char* signerName = "good_indirect_future"; + + const ByteString extensions[] = { + CreateEncodedEKUExtension(OCSPSigningEKUDER, Critical::No), + ByteString() + }; + + ScopedTestKeyPair signerKeyPair(GenerateKeyPair()); + ByteString signerDER(CreateEncodedCertificate( + ++rootIssuedCount, sha256WithRSAEncryption(), + rootName, + twoDaysAfterNow, + tenDaysAfterNow, + signerName, *signerKeyPair, extensions, + *rootKeyPair)); + ASSERT_FALSE(ENCODING_FAILED(signerDER)); + + ByteString certs[] = { signerDER, ByteString() }; + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, + signerName, *signerKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + sha256WithRSAEncryption(), certs)); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, good_no_eku) +{ + ByteString responseString( + CreateEncodedIndirectOCSPSuccessfulResponse( + "good_indirect_wrong_eku", + OCSPResponseContext::good, byKey, + sha256WithRSAEncryption(), nullptr)); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +static const Input serverAuthEKUDER(tlv_id_kp_serverAuth); + +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, + good_indirect_wrong_eku) +{ + ByteString responseString( + CreateEncodedIndirectOCSPSuccessfulResponse( + "good_indirect_wrong_eku", + OCSPResponseContext::good, byKey, + sha256WithRSAEncryption(), &serverAuthEKUDER)); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +// Test that signature of OCSP response signer cert is verified +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, good_tampered_eku) +{ + ByteString tamperedResponse( + CreateEncodedIndirectOCSPSuccessfulResponse( + "good_indirect_tampered_eku", + OCSPResponseContext::good, byKey, + sha256WithRSAEncryption(), &serverAuthEKUDER)); + ASSERT_EQ(Success, + TamperOnce(tamperedResponse, + ByteString(tlv_id_kp_serverAuth, + sizeof(tlv_id_kp_serverAuth)), + ByteString(tlv_id_kp_OCSPSigning, + sizeof(tlv_id_kp_OCSPSigning)))); + Input tamperedResponseInput; + ASSERT_EQ(Success, tamperedResponseInput.Init(tamperedResponse.data(), + tamperedResponse.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + tamperedResponseInput, expired)); + ASSERT_FALSE(expired); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, good_unknown_issuer) +{ + static const char* subCAName = "good_indirect_unknown_issuer sub-CA"; + static const char* signerName = "good_indirect_unknown_issuer OCSP signer"; + + // unknown issuer + ScopedTestKeyPair unknownKeyPair(GenerateKeyPair()); + ASSERT_TRUE(unknownKeyPair.get()); + + // Delegated responder cert signed by unknown issuer + const ByteString extensions[] = { + CreateEncodedEKUExtension(OCSPSigningEKUDER, Critical::No), + ByteString() + }; + ScopedTestKeyPair signerKeyPair(GenerateKeyPair()); + ByteString signerDER(CreateEncodedCertificate( + 1, sha256WithRSAEncryption(), subCAName, + oneDayBeforeNow, oneDayAfterNow, signerName, + *signerKeyPair, extensions, *unknownKeyPair)); + ASSERT_FALSE(ENCODING_FAILED(signerDER)); + + // OCSP response signed by that delegated responder + ByteString certs[] = { signerDER, ByteString() }; + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, + signerName, *signerKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + sha256WithRSAEncryption(), certs)); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +// The CA that issued the OCSP responder cert is a sub-CA of the issuer of +// the certificate that the OCSP response is for. That sub-CA cert is included +// in the OCSP response before the OCSP responder cert. +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, + good_indirect_subca_1_first) +{ + static const char* subCAName = "good_indirect_subca_1_first sub-CA"; + static const char* signerName = "good_indirect_subca_1_first OCSP signer"; + static const long zero = 0; + + // sub-CA of root (root is the direct issuer of endEntity) + const ByteString subCAExtensions[] = { + CreateEncodedBasicConstraints(true, &zero, Critical::No), + ByteString() + }; + ScopedTestKeyPair subCAKeyPair(GenerateKeyPair()); + ByteString subCADER(CreateEncodedCertificate( + ++rootIssuedCount, sha256WithRSAEncryption(), rootName, + oneDayBeforeNow, oneDayAfterNow, subCAName, + *subCAKeyPair, subCAExtensions, *rootKeyPair)); + ASSERT_FALSE(ENCODING_FAILED(subCADER)); + + // Delegated responder cert signed by that sub-CA + const ByteString extensions[] = { + CreateEncodedEKUExtension(OCSPSigningEKUDER, Critical::No), + ByteString(), + }; + ScopedTestKeyPair signerKeyPair(GenerateKeyPair()); + ByteString signerDER(CreateEncodedCertificate( + 1, sha256WithRSAEncryption(), subCAName, + oneDayBeforeNow, oneDayAfterNow, signerName, + *signerKeyPair, extensions, *subCAKeyPair)); + ASSERT_FALSE(ENCODING_FAILED(signerDER)); + + // OCSP response signed by the delegated responder issued by the sub-CA + // that is trying to impersonate the root. + ByteString certs[] = { subCADER, signerDER, ByteString() }; + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, + signerName, *signerKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + sha256WithRSAEncryption(), certs)); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +// The CA that issued the OCSP responder cert is a sub-CA of the issuer of +// the certificate that the OCSP response is for. That sub-CA cert is included +// in the OCSP response after the OCSP responder cert. +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, + good_indirect_subca_1_second) +{ + static const char* subCAName = "good_indirect_subca_1_second sub-CA"; + static const char* signerName = "good_indirect_subca_1_second OCSP signer"; + static const long zero = 0; + + // sub-CA of root (root is the direct issuer of endEntity) + const ByteString subCAExtensions[] = { + CreateEncodedBasicConstraints(true, &zero, Critical::No), + ByteString() + }; + ScopedTestKeyPair subCAKeyPair(GenerateKeyPair()); + ByteString subCADER(CreateEncodedCertificate(++rootIssuedCount, + sha256WithRSAEncryption(), + rootName, + oneDayBeforeNow, oneDayAfterNow, + subCAName, *subCAKeyPair, + subCAExtensions, *rootKeyPair)); + ASSERT_FALSE(ENCODING_FAILED(subCADER)); + + // Delegated responder cert signed by that sub-CA + const ByteString extensions[] = { + CreateEncodedEKUExtension(OCSPSigningEKUDER, Critical::No), + ByteString() + }; + ScopedTestKeyPair signerKeyPair(GenerateKeyPair()); + ByteString signerDER(CreateEncodedCertificate( + 1, sha256WithRSAEncryption(), subCAName, + oneDayBeforeNow, oneDayAfterNow, signerName, + *signerKeyPair, extensions, *subCAKeyPair)); + ASSERT_FALSE(ENCODING_FAILED(signerDER)); + + // OCSP response signed by the delegated responder issued by the sub-CA + // that is trying to impersonate the root. + ByteString certs[] = { signerDER, subCADER, ByteString() }; + ByteString responseString( + CreateEncodedOCSPSuccessfulResponse( + OCSPResponseContext::good, *endEntityCertID, + signerName, *signerKeyPair, oneDayBeforeNow, + oneDayBeforeNow, &oneDayAfterNow, + sha256WithRSAEncryption(), certs)); + Input response; + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_DelegatedResponder, + good_unsupportedSignatureAlgorithmOnResponder) +{ + // Note that the algorithm ID (md5WithRSAEncryption) identifies the signature + // algorithm that will be used to sign the certificate that issues the OCSP + // responses, not the responses themselves. + PRUint32 policyMd5; + ASSERT_EQ(SECSuccess,NSS_GetAlgorithmPolicy(SEC_OID_MD5, &policyMd5)); + + /* our encode won't work if MD5 isn't allowed by policy */ + ASSERT_EQ(SECSuccess, + NSS_SetAlgorithmPolicy(SEC_OID_MD5, NSS_USE_ALG_IN_SIGNATURE, 0)); + ByteString responseString( + CreateEncodedIndirectOCSPSuccessfulResponse( + "good_indirect_unsupportedSignatureAlgorithm", + OCSPResponseContext::good, byKey, + md5WithRSAEncryption())); + Input response; + /* now restore the existing policy */ + ASSERT_EQ(Success, + response.Init(responseString.data(), responseString.length())); + ASSERT_EQ(SECSuccess, + NSS_SetAlgorithmPolicy(SEC_OID_MD5, policyMd5, NSS_USE_ALG_IN_SIGNATURE)); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); +} + +class pkixocsp_VerifyEncodedResponse_GetCertTrust + : public pkixocsp_VerifyEncodedResponse_DelegatedResponder { +public: + void SetUp() + { + pkixocsp_VerifyEncodedResponse_DelegatedResponder::SetUp(); + + responseString = + CreateEncodedIndirectOCSPSuccessfulResponse( + "OCSPGetCertTrustTest Signer", OCSPResponseContext::good, + byKey, sha256WithRSAEncryption(), &OCSPSigningEKUDER, + &signerCertDER); + if (ENCODING_FAILED(responseString)) { + abort(); + } + if (response.Init(responseString.data(), responseString.length()) + != Success) { + abort(); + } + if (signerCertDER.length() == 0) { + abort(); + } + } + + class TrustDomain final : public OCSPTestTrustDomain + { + public: + TrustDomain() + : certTrustLevel(TrustLevel::InheritsTrust) + { + } + + bool SetCertTrust(const ByteString& aCertDER, TrustLevel aCertTrustLevel) + { + this->certDER = aCertDER; + this->certTrustLevel = aCertTrustLevel; + return true; + } + private: + Result GetCertTrust(EndEntityOrCA endEntityOrCA, const CertPolicyId&, + Input candidateCert, /*out*/ TrustLevel& trustLevel) + override + { + EXPECT_EQ(endEntityOrCA, EndEntityOrCA::MustBeEndEntity); + EXPECT_FALSE(certDER.empty()); + Input certDERInput; + EXPECT_EQ(Success, certDERInput.Init(certDER.data(), certDER.length())); + EXPECT_TRUE(InputsAreEqual(certDERInput, candidateCert)); + trustLevel = certTrustLevel; + return Success; + } + + ByteString certDER; + TrustLevel certTrustLevel; + }; + +// trustDomain deliberately shadows the inherited field so that it isn't used +// by accident. See bug 1339921. +// Unfortunately GCC can't parse __has_warning("-Wshadow-field") even if it's +// the latter part of a conjunction that would evaluate to false, so we have to +// wrap it in a separate preprocessor conditional rather than using &&. +#if defined(__clang__) + #if __has_warning("-Wshadow-field") + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wshadow-field" + #endif +#endif + TrustDomain trustDomain; +#if defined(__clang__) + #if __has_warning("-Wshadow-field") + #pragma clang diagnostic pop + #endif +#endif + ByteString signerCertDER; + ByteString responseString; + Input response; // references data in responseString +}; + +TEST_F(pkixocsp_VerifyEncodedResponse_GetCertTrust, InheritTrust) +{ + ASSERT_TRUE(trustDomain.SetCertTrust(signerCertDER, + TrustLevel::InheritsTrust)); + bool expired; + ASSERT_EQ(Success, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_GetCertTrust, TrustAnchor) +{ + ASSERT_TRUE(trustDomain.SetCertTrust(signerCertDER, + TrustLevel::TrustAnchor)); + bool expired; + ASSERT_EQ(Success, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + response, expired)); + ASSERT_FALSE(expired); +} + +TEST_F(pkixocsp_VerifyEncodedResponse_GetCertTrust, ActivelyDistrusted) +{ + ASSERT_TRUE(trustDomain.SetCertTrust(signerCertDER, + TrustLevel::ActivelyDistrusted)); + Input responseInput; + ASSERT_EQ(Success, + responseInput.Init(responseString.data(), + responseString.length())); + bool expired; + ASSERT_EQ(Result::ERROR_OCSP_INVALID_SIGNING_CERT, + VerifyEncodedOCSPResponse(trustDomain, *endEntityCertID, Now(), + END_ENTITY_MAX_LIFETIME_IN_DAYS, + responseInput, expired)); + ASSERT_FALSE(expired); +} |