/* -*- 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 2013 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 "mozpkix/pkix.h" #include "mozpkix/pkixcheck.h" #include "mozpkix/pkixutil.h" namespace mozilla { namespace pkix { static Result BuildForward(TrustDomain& trustDomain, const BackCert& subject, Time time, KeyUsage requiredKeyUsageIfPresent, KeyPurposeId requiredEKUIfPresent, const CertPolicyId& requiredPolicy, /*optional*/ const Input* stapledOCSPResponse, unsigned int subCACount, unsigned int& buildForwardCallBudget); TrustDomain::IssuerChecker::IssuerChecker() { } TrustDomain::IssuerChecker::~IssuerChecker() { } // The implementation of TrustDomain::IssuerTracker is in a subclass only to // hide the implementation from external users. class PathBuildingStep final : public TrustDomain::IssuerChecker { public: PathBuildingStep(TrustDomain& aTrustDomain, const BackCert& aSubject, Time aTime, KeyPurposeId aRequiredEKUIfPresent, const CertPolicyId& aRequiredPolicy, /*optional*/ const Input* aStapledOCSPResponse, unsigned int aSubCACount, Result aDeferredSubjectError, unsigned int& aBuildForwardCallBudget) : trustDomain(aTrustDomain) , subject(aSubject) , time(aTime) , requiredEKUIfPresent(aRequiredEKUIfPresent) , requiredPolicy(aRequiredPolicy) , stapledOCSPResponse(aStapledOCSPResponse) , subCACount(aSubCACount) , deferredSubjectError(aDeferredSubjectError) , result(Result::FATAL_ERROR_LIBRARY_FAILURE) , resultWasSet(false) , buildForwardCallBudget(aBuildForwardCallBudget) { } Result Check(Input potentialIssuerDER, /*optional*/ const Input* additionalNameConstraints, /*out*/ bool& keepGoing) override; Result CheckResult() const; private: TrustDomain& trustDomain; const BackCert& subject; const Time time; const KeyPurposeId requiredEKUIfPresent; const CertPolicyId& requiredPolicy; /*optional*/ Input const* const stapledOCSPResponse; const unsigned int subCACount; const Result deferredSubjectError; Result RecordResult(Result currentResult, /*out*/ bool& keepGoing); Result result; bool resultWasSet; unsigned int& buildForwardCallBudget; PathBuildingStep(const PathBuildingStep&) = delete; void operator=(const PathBuildingStep&) = delete; }; Result PathBuildingStep::RecordResult(Result newResult, /*out*/ bool& keepGoing) { if (newResult == Result::ERROR_UNTRUSTED_CERT) { newResult = Result::ERROR_UNTRUSTED_ISSUER; } else if (newResult == Result::ERROR_EXPIRED_CERTIFICATE) { newResult = Result::ERROR_EXPIRED_ISSUER_CERTIFICATE; } else if (newResult == Result::ERROR_NOT_YET_VALID_CERTIFICATE) { newResult = Result::ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE; } if (resultWasSet) { if (result == Success) { return NotReached("RecordResult called after finding a chain", Result::FATAL_ERROR_INVALID_STATE); } // If every potential issuer has the same problem (e.g. expired) and/or if // there is only one bad potential issuer, then return a more specific // error. Otherwise, punt on trying to decide which error should be // returned by returning the generic Result::ERROR_UNKNOWN_ISSUER error. if (newResult != Success && newResult != result) { newResult = Result::ERROR_UNKNOWN_ISSUER; } } result = newResult; resultWasSet = true; keepGoing = result != Success; return Success; } Result PathBuildingStep::CheckResult() const { if (!resultWasSet) { return Result::ERROR_UNKNOWN_ISSUER; } return result; } // The code that executes in the inner loop of BuildForward Result PathBuildingStep::Check(Input potentialIssuerDER, /*optional*/ const Input* additionalNameConstraints, /*out*/ bool& keepGoing) { BackCert potentialIssuer(potentialIssuerDER, EndEntityOrCA::MustBeCA, &subject); Result rv = potentialIssuer.Init(); if (rv != Success) { return RecordResult(rv, keepGoing); } // Simple TrustDomain::FindIssuers implementations may pass in all possible // CA certificates without any filtering. Because of this, we don't consider // a mismatched name to be an error. Instead, we just pretend that any // certificate without a matching name was never passed to us. In particular, // we treat the case where the TrustDomain only asks us to check CA // certificates with mismatched names as equivalent to the case where the // TrustDomain never called Check() at all. if (!InputsAreEqual(potentialIssuer.GetSubject(), subject.GetIssuer())) { keepGoing = true; return Success; } // Loop prevention, done as recommended by RFC4158 Section 5.2 // TODO: this doesn't account for subjectAltNames! // TODO(perf): This probably can and should be optimized in some way. for (const BackCert* prev = potentialIssuer.childCert; prev; prev = prev->childCert) { if (InputsAreEqual(potentialIssuer.GetSubjectPublicKeyInfo(), prev->GetSubjectPublicKeyInfo()) && InputsAreEqual(potentialIssuer.GetSubject(), prev->GetSubject())) { // XXX: error code return RecordResult(Result::ERROR_UNKNOWN_ISSUER, keepGoing); } } if (potentialIssuer.GetNameConstraints()) { rv = CheckNameConstraints(*potentialIssuer.GetNameConstraints(), subject, requiredEKUIfPresent); if (rv != Success) { return RecordResult(rv, keepGoing); } } if (additionalNameConstraints) { rv = CheckNameConstraints(*additionalNameConstraints, subject, requiredEKUIfPresent); if (rv != Success) { return RecordResult(rv, keepGoing); } } rv = CheckTLSFeatures(subject, potentialIssuer); if (rv != Success) { return RecordResult(rv, keepGoing); } // If we've ran out of budget, stop searching. if (buildForwardCallBudget == 0) { Result savedRv = RecordResult(Result::ERROR_UNKNOWN_ISSUER, keepGoing); keepGoing = false; return savedRv; } buildForwardCallBudget--; // RFC 5280, Section 4.2.1.3: "If the keyUsage extension is present, then the // subject public key MUST NOT be used to verify signatures on certificates // or CRLs unless the corresponding keyCertSign or cRLSign bit is set." rv = BuildForward(trustDomain, potentialIssuer, time, KeyUsage::keyCertSign, requiredEKUIfPresent, requiredPolicy, nullptr, subCACount, buildForwardCallBudget); if (rv != Success) { return RecordResult(rv, keepGoing); } rv = VerifySignedData(trustDomain, subject.GetSignedData(), potentialIssuer.GetSubjectPublicKeyInfo()); if (rv != Success) { return RecordResult(rv, keepGoing); } // We avoid doing revocation checking for expired certificates because OCSP // responders are allowed to forget about expired certificates, and many OCSP // responders return an error when asked for the status of an expired // certificate. if (deferredSubjectError != Result::ERROR_EXPIRED_CERTIFICATE) { CertID certID(subject.GetIssuer(), potentialIssuer.GetSubjectPublicKeyInfo(), subject.GetSerialNumber()); Time notBefore(Time::uninitialized); Time notAfter(Time::uninitialized); // This should never fail. If we're here, we've already parsed the validity // and checked that the given time is in the certificate's validity period. rv = ParseValidity(subject.GetValidity(), ¬Before, ¬After); if (rv != Success) { return rv; } Duration validityDuration(notAfter, notBefore); rv = trustDomain.CheckRevocation(subject.endEntityOrCA, certID, time, validityDuration, stapledOCSPResponse, subject.GetAuthorityInfoAccess(), subject.GetSignedCertificateTimestamps()); if (rv != Success) { // Since this is actually a problem with the current subject certificate // (rather than the issuer), it doesn't make sense to keep going; all // paths through this certificate will fail. Result savedRv = RecordResult(rv, keepGoing); keepGoing = false; return savedRv; } if (subject.endEntityOrCA == EndEntityOrCA::MustBeEndEntity) { const Input* sctExtension = subject.GetSignedCertificateTimestamps(); if (sctExtension) { Input sctList; rv = ExtractSignedCertificateTimestampListFromExtension(*sctExtension, sctList); if (rv != Success) { // Again, the problem is with this certificate, and all paths through // it will fail. Result savedRv = RecordResult(rv, keepGoing); keepGoing = false; return savedRv; } 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 // because it affects which error we return when a certificate or certificate // chain has multiple problems. See the error ranking documentation in // pkix/pkix.h. static Result BuildForward(TrustDomain& trustDomain, const BackCert& subject, Time time, KeyUsage requiredKeyUsageIfPresent, KeyPurposeId requiredEKUIfPresent, const CertPolicyId& requiredPolicy, /*optional*/ const Input* stapledOCSPResponse, unsigned int subCACount, unsigned int& buildForwardCallBudget) { Result rv; TrustLevel trustLevel; // If this is an end-entity and not a trust anchor, we defer reporting // any error found here until after attempting to find a valid chain. // See the explanation of error prioritization in pkix.h. rv = CheckIssuerIndependentProperties(trustDomain, subject, time, requiredKeyUsageIfPresent, requiredEKUIfPresent, requiredPolicy, subCACount, trustLevel); Result deferredEndEntityError = Success; if (rv != Success) { if (subject.endEntityOrCA == EndEntityOrCA::MustBeEndEntity && trustLevel != TrustLevel::TrustAnchor) { deferredEndEntityError = rv; } else { return rv; } } if (trustLevel == TrustLevel::TrustAnchor) { // End of the recursion. NonOwningDERArray chain; for (const BackCert* cert = &subject; cert; cert = cert->childCert) { rv = chain.Append(cert->GetDER()); if (rv != Success) { return NotReached("NonOwningDERArray::SetItem failed.", rv); } } // This must be done here, after the chain is built but before any // revocation checks have been done. return trustDomain.IsChainValid(chain, time, requiredPolicy); } if (subject.endEntityOrCA == EndEntityOrCA::MustBeCA) { // Avoid stack overflows and poor performance by limiting cert chain // length. static const unsigned int MAX_SUBCA_COUNT = 6; static_assert(1/*end-entity*/ + MAX_SUBCA_COUNT + 1/*root*/ == NonOwningDERArray::MAX_LENGTH, "MAX_SUBCA_COUNT and NonOwningDERArray::MAX_LENGTH mismatch."); if (subCACount >= MAX_SUBCA_COUNT) { return Result::ERROR_UNKNOWN_ISSUER; } ++subCACount; } else { assert(subCACount == 0); } // Find a trusted issuer. PathBuildingStep pathBuilder(trustDomain, subject, time, requiredEKUIfPresent, requiredPolicy, stapledOCSPResponse, subCACount, deferredEndEntityError, buildForwardCallBudget); // TODO(bug 965136): Add SKI/AKI matching optimizations rv = trustDomain.FindIssuer(subject.GetIssuer(), pathBuilder, time); if (rv != Success) { return rv; } rv = pathBuilder.CheckResult(); if (rv != Success) { return rv; } // If we found a valid chain but deferred reporting an error with the // end-entity certificate, report it now. if (deferredEndEntityError != Success) { return deferredEndEntityError; } // We've built a valid chain from the subject cert up to a trusted root. return Success; } Result BuildCertChain(TrustDomain& trustDomain, Input certDER, Time time, EndEntityOrCA endEntityOrCA, KeyUsage requiredKeyUsageIfPresent, KeyPurposeId requiredEKUIfPresent, const CertPolicyId& requiredPolicy, /*optional*/ const Input* stapledOCSPResponse) { // XXX: Support the legacy use of the subject CN field for indicating the // domain name the certificate is valid for. BackCert cert(certDER, endEntityOrCA, nullptr); Result rv = cert.Init(); if (rv != Success) { return rv; } // See bug 1056341 for context. If mozilla::pkix is being used in an // environment where there are many certificates that all have the same // distinguished name as their subject and issuer (but different SPKIs - see // the loop prevention as per RFC4158 Section 5.2 in PathBuildingStep::Check), // the space to search becomes exponential. Because it would be prohibitively // expensive to explore the entire space, we introduce a budget here that, // when exhausted, terminates the search with the result // Result::ERROR_UNKNOWN_ISSUER. Essentially, we limit the total number of // times `BuildForward` can be called. The current value appears to be a good // balance between finding a path when one exists (when the space isn't too // large) and timing out quickly enough when the space is too large or there // is no valid path to a trust anchor. unsigned int buildForwardCallBudget = 200000; return BuildForward(trustDomain, cert, time, requiredKeyUsageIfPresent, requiredEKUIfPresent, requiredPolicy, stapledOCSPResponse, 0/*subCACount*/, buildForwardCallBudget); } } } // namespace mozilla::pkix