diff options
Diffstat (limited to 'security/pkix/lib/pkixbuild.cpp')
-rw-r--r-- | security/pkix/lib/pkixbuild.cpp | 382 |
1 files changed, 382 insertions, 0 deletions
diff --git a/security/pkix/lib/pkixbuild.cpp b/security/pkix/lib/pkixbuild.cpp new file mode 100644 index 000000000..fdbd9b59f --- /dev/null +++ b/security/pkix/lib/pkixbuild.cpp @@ -0,0 +1,382 @@ +/* -*- 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 "pkix/pkix.h" + +#include "pkixcheck.h" +#include "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); + +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& trustDomain, const BackCert& subject, + Time time, KeyPurposeId requiredEKUIfPresent, + const CertPolicyId& requiredPolicy, + /*optional*/ const Input* stapledOCSPResponse, + unsigned int subCACount, Result deferredSubjectError) + : trustDomain(trustDomain) + , subject(subject) + , time(time) + , requiredEKUIfPresent(requiredEKUIfPresent) + , requiredPolicy(requiredPolicy) + , stapledOCSPResponse(stapledOCSPResponse) + , subCACount(subCACount) + , deferredSubjectError(deferredSubjectError) + , result(Result::FATAL_ERROR_LIBRARY_FAILURE) + , resultWasSet(false) + { + } + + 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; + + // Initialized lazily. + uint8_t subjectSignatureDigestBuf[MAX_DIGEST_SIZE_IN_BYTES]; + der::PublicKeyAlgorithm subjectSignaturePublicKeyAlg; + SignedDigest subjectSignature; + + Result RecordResult(Result currentResult, /*out*/ bool& keepGoing); + Result result; + bool resultWasSet; + + 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. + bool loopDetected = false; + for (const BackCert* prev = potentialIssuer.childCert; + !loopDetected && prev != nullptr; 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); + } + + // 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); + if (rv != Success) { + return RecordResult(rv, keepGoing); + } + + // Calculate the digest of the subject's signed data if we haven't already + // done so. We do this lazily to avoid doing it at all if we backtrack before + // getting to this point. We cache the result to avoid recalculating it if we + // backtrack after getting to this point. + if (subjectSignature.digest.GetLength() == 0) { + rv = DigestSignedData(trustDomain, subject.GetSignedData(), + subjectSignatureDigestBuf, + subjectSignaturePublicKeyAlg, subjectSignature); + if (rv != Success) { + return rv; + } + } + + rv = VerifySignedDigest(trustDomain, subjectSignaturePublicKeyAlg, + subjectSignature, + 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()); + 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 +// 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) +{ + 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); + } + + 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); + + // 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; + } + + return BuildForward(trustDomain, cert, time, requiredKeyUsageIfPresent, + requiredEKUIfPresent, requiredPolicy, stapledOCSPResponse, + 0/*subCACount*/); +} + +} } // namespace mozilla::pkix |