From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- security/pkix/lib/ScopedPtr.h | 83 ++ security/pkix/lib/pkixbuild.cpp | 382 +++++++ security/pkix/lib/pkixcert.cpp | 323 ++++++ security/pkix/lib/pkixcheck.cpp | 1088 ++++++++++++++++++++ security/pkix/lib/pkixcheck.h | 66 ++ security/pkix/lib/pkixder.cpp | 612 ++++++++++++ security/pkix/lib/pkixder.h | 565 +++++++++++ security/pkix/lib/pkixnames.cpp | 2050 ++++++++++++++++++++++++++++++++++++++ security/pkix/lib/pkixnss.cpp | 228 +++++ security/pkix/lib/pkixocsp.cpp | 1007 +++++++++++++++++++ security/pkix/lib/pkixresult.cpp | 46 + security/pkix/lib/pkixtime.cpp | 78 ++ security/pkix/lib/pkixutil.h | 289 ++++++ security/pkix/lib/pkixverify.cpp | 103 ++ 14 files changed, 6920 insertions(+) create mode 100644 security/pkix/lib/ScopedPtr.h create mode 100644 security/pkix/lib/pkixbuild.cpp create mode 100644 security/pkix/lib/pkixcert.cpp create mode 100644 security/pkix/lib/pkixcheck.cpp create mode 100644 security/pkix/lib/pkixcheck.h create mode 100644 security/pkix/lib/pkixder.cpp create mode 100644 security/pkix/lib/pkixder.h create mode 100644 security/pkix/lib/pkixnames.cpp create mode 100644 security/pkix/lib/pkixnss.cpp create mode 100644 security/pkix/lib/pkixocsp.cpp create mode 100644 security/pkix/lib/pkixresult.cpp create mode 100644 security/pkix/lib/pkixtime.cpp create mode 100644 security/pkix/lib/pkixutil.h create mode 100644 security/pkix/lib/pkixverify.cpp (limited to 'security/pkix/lib') diff --git a/security/pkix/lib/ScopedPtr.h b/security/pkix/lib/ScopedPtr.h new file mode 100644 index 000000000..a9e18adc1 --- /dev/null +++ b/security/pkix/lib/ScopedPtr.h @@ -0,0 +1,83 @@ +/* -*- 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. + */ + +#ifndef mozilla_pkix_ScopedPtr_h +#define mozilla_pkix_ScopedPtr_h + +namespace mozilla { namespace pkix { + +// A subset polyfill of std::unique_ptr that does not support move construction +// or move assignment. This is used instead of std::unique_ptr because some +// important toolchains still don't provide std::unique_ptr, including in +// particular Android NDK projects with APP_STL=stlport_static or +// ALL_STL=stlport_shared. +template +class ScopedPtr final +{ +public: + explicit ScopedPtr(T* value = nullptr) : mValue(value) { } + + ScopedPtr(const ScopedPtr&) = delete; + + ~ScopedPtr() + { + if (mValue) { + Destroyer(mValue); + } + } + + void operator=(const ScopedPtr&) = delete; + + T& operator*() const { return *mValue; } + T* operator->() const { return mValue; } + + explicit operator bool() const { return mValue; } + + T* get() const { return mValue; } + + T* release() + { + T* result = mValue; + mValue = nullptr; + return result; + } + + void reset(T* newValue = nullptr) + { + // The C++ standard requires std::unique_ptr to destroy the old value + // pointed to by mValue, if any, *after* assigning the new value to mValue. + T* oldValue = mValue; + mValue = newValue; + if (oldValue) { + Destroyer(oldValue); + } + } + +private: + T* mValue; +}; + +} } // namespace mozilla::pkix + +#endif // mozilla_pkix_ScopedPtr_h 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 diff --git a/security/pkix/lib/pkixcert.cpp b/security/pkix/lib/pkixcert.cpp new file mode 100644 index 000000000..ffa58fd3b --- /dev/null +++ b/security/pkix/lib/pkixcert.cpp @@ -0,0 +1,323 @@ +/* -*- 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 "pkixutil.h" + +namespace mozilla { namespace pkix { + +Result +BackCert::Init() +{ + Result rv; + + // Certificate ::= SEQUENCE { + // tbsCertificate TBSCertificate, + // signatureAlgorithm AlgorithmIdentifier, + // signatureValue BIT STRING } + + Reader tbsCertificate; + + // The scope of |input| and |certificate| are limited to this block so we + // don't accidentally confuse them for tbsCertificate later. + { + Reader certificate; + rv = der::ExpectTagAndGetValueAtEnd(der, der::SEQUENCE, certificate); + if (rv != Success) { + return rv; + } + rv = der::SignedData(certificate, tbsCertificate, signedData); + if (rv != Success) { + return rv; + } + rv = der::End(certificate); + if (rv != Success) { + return rv; + } + } + + // TBSCertificate ::= SEQUENCE { + // version [0] EXPLICIT Version DEFAULT v1, + // serialNumber CertificateSerialNumber, + // signature AlgorithmIdentifier, + // issuer Name, + // validity Validity, + // subject Name, + // subjectPublicKeyInfo SubjectPublicKeyInfo, + // issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, + // -- If present, version MUST be v2 or v3 + // subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, + // -- If present, version MUST be v2 or v3 + // extensions [3] EXPLICIT Extensions OPTIONAL + // -- If present, version MUST be v3 + // } + rv = der::OptionalVersion(tbsCertificate, version); + if (rv != Success) { + return rv; + } + rv = der::CertificateSerialNumber(tbsCertificate, serialNumber); + if (rv != Success) { + return rv; + } + rv = der::ExpectTagAndGetValue(tbsCertificate, der::SEQUENCE, signature); + if (rv != Success) { + return rv; + } + rv = der::ExpectTagAndGetTLV(tbsCertificate, der::SEQUENCE, issuer); + if (rv != Success) { + return rv; + } + rv = der::ExpectTagAndGetValue(tbsCertificate, der::SEQUENCE, validity); + if (rv != Success) { + return rv; + } + // TODO(bug XXXXXXX): We rely on the the caller of mozilla::pkix to validate + // that the name is syntactically valid, if they care. In Gecko we do this + // implicitly by parsing the certificate into a CERTCertificate object. + // Instead of relying on the caller to do this, we should do it ourselves. + rv = der::ExpectTagAndGetTLV(tbsCertificate, der::SEQUENCE, subject); + if (rv != Success) { + return rv; + } + rv = der::ExpectTagAndGetTLV(tbsCertificate, der::SEQUENCE, + subjectPublicKeyInfo); + if (rv != Success) { + return rv; + } + + static const uint8_t CSC = der::CONTEXT_SPECIFIC | der::CONSTRUCTED; + + // According to RFC 5280, all fields below this line are forbidden for + // certificate versions less than v3. However, for compatibility reasons, + // we parse v1/v2 certificates in the same way as v3 certificates. So if + // these fields appear in a v1 certificate, they will be used. + + // Ignore issuerUniqueID if present. + if (tbsCertificate.Peek(CSC | 1)) { + rv = der::ExpectTagAndSkipValue(tbsCertificate, CSC | 1); + if (rv != Success) { + return rv; + } + } + + // Ignore subjectUniqueID if present. + if (tbsCertificate.Peek(CSC | 2)) { + rv = der::ExpectTagAndSkipValue(tbsCertificate, CSC | 2); + if (rv != Success) { + return rv; + } + } + + rv = der::OptionalExtensions( + tbsCertificate, CSC | 3, + [this](Reader& extnID, const Input& extnValue, bool critical, + /*out*/ bool& understood) { + return RememberExtension(extnID, extnValue, critical, understood); + }); + if (rv != Success) { + return rv; + } + + // The Netscape Certificate Type extension is an obsolete + // Netscape-proprietary mechanism that we ignore in favor of the standard + // extensions. However, some CAs have issued certificates with the Netscape + // Cert Type extension marked critical. Thus, for compatibility reasons, we + // "understand" this extension by ignoring it when it is not critical, and + // by ensuring that the equivalent standardized extensions are present when + // it is marked critical, based on the assumption that the information in + // the Netscape Cert Type extension is consistent with the information in + // the standard extensions. + // + // Here is a mapping between the Netscape Cert Type extension and the + // standard extensions: + // + // Netscape Cert Type | BasicConstraints.cA | Extended Key Usage + // --------------------+-----------------------+---------------------- + // SSL Server | false | id_kp_serverAuth + // SSL Client | false | id_kp_clientAuth + // S/MIME Client | false | id_kp_emailProtection + // Object Signing | false | id_kp_codeSigning + // SSL Server CA | true | id_pk_serverAuth + // SSL Client CA | true | id_kp_clientAuth + // S/MIME CA | true | id_kp_emailProtection + // Object Signing CA | true | id_kp_codeSigning + if (criticalNetscapeCertificateType.GetLength() > 0 && + (basicConstraints.GetLength() == 0 || extKeyUsage.GetLength() == 0)) { + return Result::ERROR_UNKNOWN_CRITICAL_EXTENSION; + } + + return der::End(tbsCertificate); +} + +Result +BackCert::RememberExtension(Reader& extnID, Input extnValue, + bool critical, /*out*/ bool& understood) +{ + understood = false; + + // python DottedOIDToCode.py id-ce-keyUsage 2.5.29.15 + static const uint8_t id_ce_keyUsage[] = { + 0x55, 0x1d, 0x0f + }; + // python DottedOIDToCode.py id-ce-subjectAltName 2.5.29.17 + static const uint8_t id_ce_subjectAltName[] = { + 0x55, 0x1d, 0x11 + }; + // python DottedOIDToCode.py id-ce-basicConstraints 2.5.29.19 + static const uint8_t id_ce_basicConstraints[] = { + 0x55, 0x1d, 0x13 + }; + // python DottedOIDToCode.py id-ce-nameConstraints 2.5.29.30 + static const uint8_t id_ce_nameConstraints[] = { + 0x55, 0x1d, 0x1e + }; + // python DottedOIDToCode.py id-ce-certificatePolicies 2.5.29.32 + static const uint8_t id_ce_certificatePolicies[] = { + 0x55, 0x1d, 0x20 + }; + // python DottedOIDToCode.py id-ce-policyConstraints 2.5.29.36 + static const uint8_t id_ce_policyConstraints[] = { + 0x55, 0x1d, 0x24 + }; + // python DottedOIDToCode.py id-ce-extKeyUsage 2.5.29.37 + static const uint8_t id_ce_extKeyUsage[] = { + 0x55, 0x1d, 0x25 + }; + // python DottedOIDToCode.py id-ce-inhibitAnyPolicy 2.5.29.54 + static const uint8_t id_ce_inhibitAnyPolicy[] = { + 0x55, 0x1d, 0x36 + }; + // python DottedOIDToCode.py id-pe-authorityInfoAccess 1.3.6.1.5.5.7.1.1 + static const uint8_t id_pe_authorityInfoAccess[] = { + 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01 + }; + // python DottedOIDToCode.py id-pkix-ocsp-nocheck 1.3.6.1.5.5.7.48.1.5 + static const uint8_t id_pkix_ocsp_nocheck[] = { + 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x05 + }; + // python DottedOIDToCode.py Netscape-certificate-type 2.16.840.1.113730.1.1 + static const uint8_t Netscape_certificate_type[] = { + 0x60, 0x86, 0x48, 0x01, 0x86, 0xf8, 0x42, 0x01, 0x01 + }; + // python DottedOIDToCode.py id-pe-tlsfeature 1.3.6.1.5.5.7.1.24 + static const uint8_t id_pe_tlsfeature[] = { + 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x18 + }; + // python DottedOIDToCode.py id-embeddedSctList 1.3.6.1.4.1.11129.2.4.2 + // See Section 3.3 of RFC 6962. + static const uint8_t id_embeddedSctList[] = { + 0x2b, 0x06, 0x01, 0x04, 0x01, 0xd6, 0x79, 0x02, 0x04, 0x02 + }; + + Input* out = nullptr; + + // We already enforce the maximum possible constraints for policies so we + // can safely ignore even critical policy constraint extensions. + // + // XXX: Doing it this way won't allow us to detect duplicate + // policyConstraints extensions, but that's OK because (and only because) we + // ignore the extension. + Input dummyPolicyConstraints; + + // We don't need to save the contents of this extension if it is present. We + // just need to handle its presence (it is essentially ignored right now). + Input dummyOCSPNocheck; + + // For compatibility reasons, for some extensions we have to allow empty + // extension values. This would normally interfere with our duplicate + // extension checking code. However, as long as the extensions we allow to + // have empty values are also the ones we implicitly allow duplicates of, + // this will work fine. + bool emptyValueAllowed = false; + + // RFC says "Conforming CAs MUST mark this extension as non-critical" for + // both authorityKeyIdentifier and subjectKeyIdentifier, and we do not use + // them for anything, so we totally ignore them here. + + if (extnID.MatchRest(id_ce_keyUsage)) { + out = &keyUsage; + } else if (extnID.MatchRest(id_ce_subjectAltName)) { + out = &subjectAltName; + } else if (extnID.MatchRest(id_ce_basicConstraints)) { + out = &basicConstraints; + } else if (extnID.MatchRest(id_ce_nameConstraints)) { + out = &nameConstraints; + } else if (extnID.MatchRest(id_ce_certificatePolicies)) { + out = &certificatePolicies; + } else if (extnID.MatchRest(id_ce_policyConstraints)) { + out = &dummyPolicyConstraints; + } else if (extnID.MatchRest(id_ce_extKeyUsage)) { + out = &extKeyUsage; + } else if (extnID.MatchRest(id_ce_inhibitAnyPolicy)) { + out = &inhibitAnyPolicy; + } else if (extnID.MatchRest(id_pe_authorityInfoAccess)) { + out = &authorityInfoAccess; + } else if (extnID.MatchRest(id_pe_tlsfeature)) { + out = &requiredTLSFeatures; + } else if (extnID.MatchRest(id_embeddedSctList)) { + out = &signedCertificateTimestamps; + } else if (extnID.MatchRest(id_pkix_ocsp_nocheck) && critical) { + // We need to make sure we don't reject delegated OCSP response signing + // certificates that contain the id-pkix-ocsp-nocheck extension marked as + // critical when validating OCSP responses. Without this, an application + // that implements soft-fail OCSP might ignore a valid Revoked or Unknown + // response, and an application that implements hard-fail OCSP might fail + // to connect to a server given a valid Good response. + out = &dummyOCSPNocheck; + // We allow this extension to have an empty value. + // See http://comments.gmane.org/gmane.ietf.x509/30947 + emptyValueAllowed = true; + } else if (extnID.MatchRest(Netscape_certificate_type) && critical) { + out = &criticalNetscapeCertificateType; + } + + if (out) { + // Don't allow an empty value for any extension we understand. This way, we + // can test out->GetLength() != 0 or out->Init() to check for duplicates. + if (extnValue.GetLength() == 0 && !emptyValueAllowed) { + return Result::ERROR_EXTENSION_VALUE_INVALID; + } + if (out->Init(extnValue) != Success) { + // Duplicate extension + return Result::ERROR_EXTENSION_VALUE_INVALID; + } + understood = true; + } + + return Success; +} + +Result +ExtractSignedCertificateTimestampListFromExtension(Input extnValue, + Input& sctList) +{ + Reader decodedValue; + Result rv = der::ExpectTagAndGetValueAtEnd(extnValue, der::OCTET_STRING, + decodedValue); + if (rv != Success) { + return rv; + } + return decodedValue.SkipToEnd(sctList); +} + +} } // namespace mozilla::pkix diff --git a/security/pkix/lib/pkixcheck.cpp b/security/pkix/lib/pkixcheck.cpp new file mode 100644 index 000000000..be2d2837a --- /dev/null +++ b/security/pkix/lib/pkixcheck.cpp @@ -0,0 +1,1088 @@ +/* -*- 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 "pkixcheck.h" + +#include "pkixder.h" +#include "pkixutil.h" + +namespace mozilla { namespace pkix { + +// 4.1.1.2 signatureAlgorithm +// 4.1.2.3 signature + +Result +CheckSignatureAlgorithm(TrustDomain& trustDomain, + EndEntityOrCA endEntityOrCA, + Time notBefore, + const der::SignedDataWithSignature& signedData, + Input signatureValue) +{ + // 4.1.1.2. signatureAlgorithm + der::PublicKeyAlgorithm publicKeyAlg; + DigestAlgorithm digestAlg; + Reader signatureAlgorithmReader(signedData.algorithm); + Result rv = der::SignatureAlgorithmIdentifierValue(signatureAlgorithmReader, + publicKeyAlg, digestAlg); + if (rv != Success) { + return rv; + } + rv = der::End(signatureAlgorithmReader); + if (rv != Success) { + return rv; + } + + // 4.1.2.3. Signature + der::PublicKeyAlgorithm signedPublicKeyAlg; + DigestAlgorithm signedDigestAlg; + Reader signedSignatureAlgorithmReader(signatureValue); + rv = der::SignatureAlgorithmIdentifierValue(signedSignatureAlgorithmReader, + signedPublicKeyAlg, + signedDigestAlg); + if (rv != Success) { + return rv; + } + rv = der::End(signedSignatureAlgorithmReader); + if (rv != Success) { + return rv; + } + + // "This field MUST contain the same algorithm identifier as the + // signatureAlgorithm field in the sequence Certificate." However, it may + // be encoded differently. In particular, one of the fields may have a NULL + // parameter while the other one may omit the parameter field altogether, and + // these are considered equivalent. Some certificates generation software + // actually generates certificates like that, so we compare the parsed values + // instead of comparing the encoded values byte-for-byte. + // + // Along the same lines, we accept two different OIDs for RSA-with-SHA1, and + // we consider those OIDs to be equivalent here. + if (publicKeyAlg != signedPublicKeyAlg || digestAlg != signedDigestAlg) { + return Result::ERROR_SIGNATURE_ALGORITHM_MISMATCH; + } + + // During the time of the deprecation of SHA-1 and the deprecation of RSA + // keys of less than 2048 bits, we will encounter many certs signed using + // SHA-1 and/or too-small RSA keys. With this in mind, we ask the trust + // domain early on if it knows it will reject the signature purely based on + // the digest algorithm and/or the RSA key size (if an RSA signature). This + // is a good optimization because it completely avoids calling + // trustDomain.FindIssuers (which may be slow) for such rejected certs, and + // more generally it short-circuits any path building with them (which, of + // course, is even slower). + + rv = trustDomain.CheckSignatureDigestAlgorithm(digestAlg, endEntityOrCA, + notBefore); + if (rv != Success) { + return rv; + } + + switch (publicKeyAlg) { + case der::PublicKeyAlgorithm::RSA_PKCS1: + { + // The RSA computation may give a result that requires fewer bytes to + // encode than the public key (since it is modular arithmetic). However, + // the last step of generating a PKCS#1.5 signature is the I2OSP + // procedure, which pads any such shorter result with zeros so that it + // is exactly the same length as the public key. + unsigned int signatureSizeInBits = signedData.signature.GetLength() * 8u; + return trustDomain.CheckRSAPublicKeyModulusSizeInBits( + endEntityOrCA, signatureSizeInBits); + } + + case der::PublicKeyAlgorithm::ECDSA: + // In theory, we could implement a similar early-pruning optimization for + // ECDSA curves. However, since there has been no similar deprecation for + // for any curve that we support, the chances of us encountering a curve + // during path building is too low to be worth bothering with. + break; + + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } + + return Success; +} + +// 4.1.2.4 Issuer + +Result +CheckIssuer(Input encodedIssuer) +{ + // "The issuer field MUST contain a non-empty distinguished name (DN)." + Reader issuer(encodedIssuer); + Input encodedRDNs; + ExpectTagAndGetValue(issuer, der::SEQUENCE, encodedRDNs); + Reader rdns(encodedRDNs); + // Check that the issuer name contains at least one RDN + // (Note: this does not check related grammar rules, such as there being one + // or more AVAs in each RDN, or the values in AVAs not being empty strings) + if (rdns.AtEnd()) { + return Result::ERROR_EMPTY_ISSUER_NAME; + } + return Success; +} + +// 4.1.2.5 Validity + +Result +ParseValidity(Input encodedValidity, + /*optional out*/ Time* notBeforeOut, + /*optional out*/ Time* notAfterOut) +{ + Reader validity(encodedValidity); + Time notBefore(Time::uninitialized); + if (der::TimeChoice(validity, notBefore) != Success) { + return Result::ERROR_INVALID_DER_TIME; + } + + Time notAfter(Time::uninitialized); + if (der::TimeChoice(validity, notAfter) != Success) { + return Result::ERROR_INVALID_DER_TIME; + } + + if (der::End(validity) != Success) { + return Result::ERROR_INVALID_DER_TIME; + } + + if (notBefore > notAfter) { + return Result::ERROR_INVALID_DER_TIME; + } + + if (notBeforeOut) { + *notBeforeOut = notBefore; + } + if (notAfterOut) { + *notAfterOut = notAfter; + } + + return Success; +} + +Result +CheckValidity(Time time, Time notBefore, Time notAfter) +{ + if (time < notBefore) { + return Result::ERROR_NOT_YET_VALID_CERTIFICATE; + } + + if (time > notAfter) { + return Result::ERROR_EXPIRED_CERTIFICATE; + } + + return Success; +} + +// 4.1.2.7 Subject Public Key Info + +Result +CheckSubjectPublicKeyInfoContents(Reader& input, TrustDomain& trustDomain, + EndEntityOrCA endEntityOrCA) +{ + // Here, we validate the syntax and do very basic semantic validation of the + // public key of the certificate. The intention here is to filter out the + // types of bad inputs that are most likely to trigger non-mathematical + // security vulnerabilities in the TrustDomain, like buffer overflows or the + // use of unsafe elliptic curves. + // + // We don't check (all of) the mathematical properties of the public key here + // because it is more efficient for the TrustDomain to do it during signature + // verification and/or other use of the public key. In particular, we + // delegate the arithmetic validation of the public key, as specified in + // NIST SP800-56A section 5.6.2, to the TrustDomain, at least for now. + + Reader algorithm; + Input subjectPublicKey; + Result rv = der::ExpectTagAndGetValue(input, der::SEQUENCE, algorithm); + if (rv != Success) { + return rv; + } + rv = der::BitStringWithNoUnusedBits(input, subjectPublicKey); + if (rv != Success) { + return rv; + } + rv = der::End(input); + if (rv != Success) { + return rv; + } + + Reader subjectPublicKeyReader(subjectPublicKey); + + Reader algorithmOID; + rv = der::ExpectTagAndGetValue(algorithm, der::OIDTag, algorithmOID); + if (rv != Success) { + return rv; + } + + // RFC 3279 Section 2.3.1 + // python DottedOIDToCode.py rsaEncryption 1.2.840.113549.1.1.1 + static const uint8_t rsaEncryption[] = { + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01 + }; + + // RFC 3279 Section 2.3.5 and RFC 5480 Section 2.1.1 + // python DottedOIDToCode.py id-ecPublicKey 1.2.840.10045.2.1 + static const uint8_t id_ecPublicKey[] = { + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01 + }; + + if (algorithmOID.MatchRest(id_ecPublicKey)) { + // An id-ecPublicKey AlgorithmIdentifier has a parameter that identifes + // the curve being used. Although RFC 5480 specifies multiple forms, we + // only supported the NamedCurve form, where the curve is identified by an + // OID. + + Reader namedCurveOIDValue; + rv = der::ExpectTagAndGetValue(algorithm, der::OIDTag, + namedCurveOIDValue); + if (rv != Success) { + return rv; + } + + // RFC 5480 + // python DottedOIDToCode.py secp256r1 1.2.840.10045.3.1.7 + static const uint8_t secp256r1[] = { + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07 + }; + + // RFC 5480 + // python DottedOIDToCode.py secp384r1 1.3.132.0.34 + static const uint8_t secp384r1[] = { + 0x2b, 0x81, 0x04, 0x00, 0x22 + }; + + // RFC 5480 + // python DottedOIDToCode.py secp521r1 1.3.132.0.35 + static const uint8_t secp521r1[] = { + 0x2b, 0x81, 0x04, 0x00, 0x23 + }; + + // Matching is attempted based on a rough estimate of the commonality of the + // elliptic curve, to minimize the number of MatchRest calls. + NamedCurve curve; + unsigned int bits; + if (namedCurveOIDValue.MatchRest(secp256r1)) { + curve = NamedCurve::secp256r1; + bits = 256; + } else if (namedCurveOIDValue.MatchRest(secp384r1)) { + curve = NamedCurve::secp384r1; + bits = 384; + } else if (namedCurveOIDValue.MatchRest(secp521r1)) { + curve = NamedCurve::secp521r1; + bits = 521; + } else { + return Result::ERROR_UNSUPPORTED_ELLIPTIC_CURVE; + } + + rv = trustDomain.CheckECDSACurveIsAcceptable(endEntityOrCA, curve); + if (rv != Success) { + return rv; + } + + // RFC 5480 Section 2.2 says that the first octet will be 0x04 to indicate + // an uncompressed point, which is the only encoding we support. + uint8_t compressedOrUncompressed; + rv = subjectPublicKeyReader.Read(compressedOrUncompressed); + if (rv != Success) { + return rv; + } + if (compressedOrUncompressed != 0x04) { + return Result::ERROR_UNSUPPORTED_EC_POINT_FORM; + } + + // The point is encoded as two raw (not DER-encoded) integers, each padded + // to the bit length (rounded up to the nearest byte). + Input point; + rv = subjectPublicKeyReader.SkipToEnd(point); + if (rv != Success) { + return rv; + } + if (point.GetLength() != ((bits + 7) / 8u) * 2u) { + return Result::ERROR_BAD_DER; + } + + // XXX: We defer the mathematical verification of the validity of the point + // until signature verification. This means that if we never verify a + // signature, we'll never fully check whether the public key is valid. + } else if (algorithmOID.MatchRest(rsaEncryption)) { + // RFC 3279 Section 2.3.1 says "The parameters field MUST have ASN.1 type + // NULL for this algorithm identifier." + rv = der::ExpectTagAndEmptyValue(algorithm, der::NULLTag); + if (rv != Success) { + return rv; + } + + // RSAPublicKey :: = SEQUENCE{ + // modulus INTEGER, --n + // publicExponent INTEGER } --e + rv = der::Nested(subjectPublicKeyReader, der::SEQUENCE, + [&trustDomain, endEntityOrCA](Reader& r) { + Input modulus; + Input::size_type modulusSignificantBytes; + Result rv = der::PositiveInteger(r, modulus, &modulusSignificantBytes); + if (rv != Success) { + return rv; + } + // XXX: Should we do additional checks of the modulus? + rv = trustDomain.CheckRSAPublicKeyModulusSizeInBits( + endEntityOrCA, modulusSignificantBytes * 8u); + if (rv != Success) { + return rv; + } + + // XXX: We don't allow the TrustDomain to validate the exponent. + // XXX: We don't do our own sanity checking of the exponent. + Input exponent; + return der::PositiveInteger(r, exponent); + }); + if (rv != Success) { + return rv; + } + } else { + return Result::ERROR_UNSUPPORTED_KEYALG; + } + + rv = der::End(algorithm); + if (rv != Success) { + return rv; + } + rv = der::End(subjectPublicKeyReader); + if (rv != Success) { + return rv; + } + + return Success; +} + +Result +CheckSubjectPublicKeyInfo(Input subjectPublicKeyInfo, TrustDomain& trustDomain, + EndEntityOrCA endEntityOrCA) +{ + Reader spkiReader(subjectPublicKeyInfo); + Result rv = der::Nested(spkiReader, der::SEQUENCE, [&](Reader& r) { + return CheckSubjectPublicKeyInfoContents(r, trustDomain, endEntityOrCA); + }); + if (rv != Success) { + return rv; + } + return der::End(spkiReader); +} + +// 4.2.1.3. Key Usage (id-ce-keyUsage) + +// As explained in the comment in CheckKeyUsage, bit 0 is the most significant +// bit and bit 7 is the least significant bit. +inline uint8_t KeyUsageToBitMask(KeyUsage keyUsage) +{ + assert(keyUsage != KeyUsage::noParticularKeyUsageRequired); + return 0x80u >> static_cast(keyUsage); +} + +Result +CheckKeyUsage(EndEntityOrCA endEntityOrCA, const Input* encodedKeyUsage, + KeyUsage requiredKeyUsageIfPresent) +{ + if (!encodedKeyUsage) { + // TODO(bug 970196): Reject certificates that are being used to verify + // certificate signatures unless the certificate is a trust anchor, to + // reduce the chances of an end-entity certificate being abused as a CA + // certificate. + // if (endEntityOrCA == EndEntityOrCA::MustBeCA && !isTrustAnchor) { + // return Result::ERROR_INADEQUATE_KEY_USAGE; + // } + // + // TODO: Users may configure arbitrary certificates as trust anchors, not + // just roots. We should only allow a certificate without a key usage to be + // used as a CA when it is self-issued and self-signed. + return Success; + } + + Reader input(*encodedKeyUsage); + Reader value; + if (der::ExpectTagAndGetValue(input, der::BIT_STRING, value) != Success) { + return Result::ERROR_INADEQUATE_KEY_USAGE; + } + + uint8_t numberOfPaddingBits; + if (value.Read(numberOfPaddingBits) != Success) { + return Result::ERROR_INADEQUATE_KEY_USAGE; + } + if (numberOfPaddingBits > 7) { + return Result::ERROR_INADEQUATE_KEY_USAGE; + } + + uint8_t bits; + if (value.Read(bits) != Success) { + // Reject empty bit masks. + return Result::ERROR_INADEQUATE_KEY_USAGE; + } + + // The most significant bit is numbered 0 (digitalSignature) and the least + // significant bit is numbered 7 (encipherOnly), and the padding is in the + // least significant bits of the last byte. The numbering of bits in a byte + // is backwards from how we usually interpret them. + // + // For example, let's say bits is encoded in one byte with of value 0xB0 and + // numberOfPaddingBits == 4. Then, bits is 10110000 in binary: + // + // bit 0 bit 3 + // | | + // v v + // 10110000 + // ^^^^ + // | + // 4 padding bits + // + // Since bits is the last byte, we have to consider the padding by ensuring + // that the least significant 4 bits are all zero, since DER rules require + // all padding bits to be zero. Then we have to look at the bit N bits to the + // right of the most significant bit, where N is a value from the KeyUsage + // enumeration. + // + // Let's say we're interested in the keyCertSign (5) bit. We'd need to look + // at bit 5, which is zero, so keyCertSign is not asserted. (Since we check + // that the padding is all zeros, it is OK to read from the padding bits.) + // + // Let's say we're interested in the digitalSignature (0) bit. We'd need to + // look at the bit 0 (the most significant bit), which is set, so that means + // digitalSignature is asserted. Similarly, keyEncipherment (2) and + // dataEncipherment (3) are asserted. + // + // Note that since the KeyUsage enumeration is limited to values 0-7, we + // only ever need to examine the first byte test for + // requiredKeyUsageIfPresent. + + if (requiredKeyUsageIfPresent != KeyUsage::noParticularKeyUsageRequired) { + // Check that the required key usage bit is set. + if ((bits & KeyUsageToBitMask(requiredKeyUsageIfPresent)) == 0) { + return Result::ERROR_INADEQUATE_KEY_USAGE; + } + } + + // RFC 5280 says "The keyCertSign bit is asserted when the subject public + // key is used for verifying signatures on public key certificates. If the + // keyCertSign bit is asserted, then the cA bit in the basic constraints + // extension (Section 4.2.1.9) MUST also be asserted." + // However, we allow end-entity certificates (i.e. certificates without + // basicConstraints.cA set to TRUE) to claim keyCertSign for compatibility + // reasons. This does not compromise security because we only allow + // certificates with basicConstraints.cA set to TRUE to act as CAs. + if (requiredKeyUsageIfPresent == KeyUsage::keyCertSign && + endEntityOrCA != EndEntityOrCA::MustBeCA) { + return Result::ERROR_INADEQUATE_KEY_USAGE; + } + + // The padding applies to the last byte, so skip to the last byte. + while (!value.AtEnd()) { + if (value.Read(bits) != Success) { + return Result::ERROR_INADEQUATE_KEY_USAGE; + } + } + + // All of the padding bits must be zero, according to DER rules. + uint8_t paddingMask = static_cast((1 << numberOfPaddingBits) - 1); + if ((bits & paddingMask) != 0) { + return Result::ERROR_INADEQUATE_KEY_USAGE; + } + + return Success; +} + +// RFC5820 4.2.1.4. Certificate Policies + +// "The user-initial-policy-set contains the special value any-policy if the +// user is not concerned about certificate policy." +// +// python DottedOIDToCode.py anyPolicy 2.5.29.32.0 + +static const uint8_t anyPolicy[] = { + 0x55, 0x1d, 0x20, 0x00 +}; + +/*static*/ const CertPolicyId CertPolicyId::anyPolicy = { + 4, { 0x55, 0x1d, 0x20, 0x00 } +}; + +bool +CertPolicyId::IsAnyPolicy() const { + if (this == &CertPolicyId::anyPolicy) { + return true; + } + return numBytes == sizeof(::mozilla::pkix::anyPolicy) && + std::equal(bytes, bytes + numBytes, ::mozilla::pkix::anyPolicy); +} + +// certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation +Result +CheckCertificatePolicies(EndEntityOrCA endEntityOrCA, + const Input* encodedCertificatePolicies, + const Input* encodedInhibitAnyPolicy, + TrustLevel trustLevel, + const CertPolicyId& requiredPolicy) +{ + if (requiredPolicy.numBytes == 0 || + requiredPolicy.numBytes > sizeof requiredPolicy.bytes) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + + bool requiredPolicyFound = requiredPolicy.IsAnyPolicy(); + if (requiredPolicyFound) { + return Success; + } + + // Bug 989051. Until we handle inhibitAnyPolicy we will fail close when + // inhibitAnyPolicy extension is present and we are validating for a policy. + if (!requiredPolicyFound && encodedInhibitAnyPolicy) { + return Result::ERROR_POLICY_VALIDATION_FAILED; + } + + // The root CA certificate may omit the policies that it has been + // trusted for, so we cannot require the policies to be present in those + // certificates. Instead, the determination of which roots are trusted for + // which policies is made by the TrustDomain's GetCertTrust method. + if (trustLevel == TrustLevel::TrustAnchor && + endEntityOrCA == EndEntityOrCA::MustBeCA) { + requiredPolicyFound = true; + } + + Input requiredPolicyDER; + if (requiredPolicyDER.Init(requiredPolicy.bytes, requiredPolicy.numBytes) + != Success) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + + if (encodedCertificatePolicies) { + Reader extension(*encodedCertificatePolicies); + Reader certificatePolicies; + Result rv = der::ExpectTagAndGetValue(extension, der::SEQUENCE, + certificatePolicies); + if (rv != Success) { + return Result::ERROR_POLICY_VALIDATION_FAILED; + } + if (!extension.AtEnd()) { + return Result::ERROR_POLICY_VALIDATION_FAILED; + } + + do { + // PolicyInformation ::= SEQUENCE { + // policyIdentifier CertPolicyId, + // policyQualifiers SEQUENCE SIZE (1..MAX) OF + // PolicyQualifierInfo OPTIONAL } + Reader policyInformation; + rv = der::ExpectTagAndGetValue(certificatePolicies, der::SEQUENCE, + policyInformation); + if (rv != Success) { + return Result::ERROR_POLICY_VALIDATION_FAILED; + } + + Reader policyIdentifier; + rv = der::ExpectTagAndGetValue(policyInformation, der::OIDTag, + policyIdentifier); + if (rv != Success) { + return rv; + } + + if (policyIdentifier.MatchRest(requiredPolicyDER)) { + requiredPolicyFound = true; + } else if (endEntityOrCA == EndEntityOrCA::MustBeCA && + policyIdentifier.MatchRest(anyPolicy)) { + requiredPolicyFound = true; + } + + // RFC 5280 Section 4.2.1.4 says "Optional qualifiers, which MAY be + // present, are not expected to change the definition of the policy." Also, + // it seems that Section 6, which defines validation, does not require any + // matching of qualifiers. Thus, doing anything with the policy qualifiers + // would be a waste of time and a source of potential incompatibilities, so + // we just ignore them. + } while (!requiredPolicyFound && !certificatePolicies.AtEnd()); + } + + if (!requiredPolicyFound) { + return Result::ERROR_POLICY_VALIDATION_FAILED; + } + + return Success; +} + +static const long UNLIMITED_PATH_LEN = -1; // must be less than zero + +// BasicConstraints ::= SEQUENCE { +// cA BOOLEAN DEFAULT FALSE, +// pathLenConstraint INTEGER (0..MAX) OPTIONAL } + +// RFC5280 4.2.1.9. Basic Constraints (id-ce-basicConstraints) +Result +CheckBasicConstraints(EndEntityOrCA endEntityOrCA, + const Input* encodedBasicConstraints, + const der::Version version, TrustLevel trustLevel, + unsigned int subCACount) +{ + bool isCA = false; + long pathLenConstraint = UNLIMITED_PATH_LEN; + + if (encodedBasicConstraints) { + Reader input(*encodedBasicConstraints); + Result rv = der::Nested(input, der::SEQUENCE, + [&isCA, &pathLenConstraint](Reader& r) { + Result rv = der::OptionalBoolean(r, isCA); + if (rv != Success) { + return rv; + } + // TODO(bug 985025): If isCA is false, pathLenConstraint + // MUST NOT be included (as per RFC 5280 section + // 4.2.1.9), but for compatibility reasons, we don't + // check this. + return der::OptionalInteger(r, UNLIMITED_PATH_LEN, pathLenConstraint); + }); + if (rv != Success) { + return Result::ERROR_EXTENSION_VALUE_INVALID; + } + if (der::End(input) != Success) { + return Result::ERROR_EXTENSION_VALUE_INVALID; + } + } else { + // "If the basic constraints extension is not present in a version 3 + // certificate, or the extension is present but the cA boolean is not + // asserted, then the certified public key MUST NOT be used to verify + // certificate signatures." + // + // For compatibility, we must accept v1 trust anchors without basic + // constraints as CAs. + // + // There are devices with v1 certificates that are unlikely to be trust + // anchors. In order to allow applications to treat this case differently + // from other basic constraints violations (e.g. allowing certificate error + // overrides for only this case), we return a different error code. + // + // TODO: add check for self-signedness? + if (endEntityOrCA == EndEntityOrCA::MustBeCA && version == der::Version::v1) { + if (trustLevel == TrustLevel::TrustAnchor) { + isCA = true; + } else { + return Result::ERROR_V1_CERT_USED_AS_CA; + } + } + } + + if (endEntityOrCA == EndEntityOrCA::MustBeEndEntity) { + // CA certificates are not trusted as EE certs. + + if (isCA) { + // Note that this check prevents a delegated OCSP response signing + // certificate with the CA bit from successfully validating when we check + // it from pkixocsp.cpp, which is a good thing. + return Result::ERROR_CA_CERT_USED_AS_END_ENTITY; + } + + return Success; + } + + assert(endEntityOrCA == EndEntityOrCA::MustBeCA); + + // End-entity certificates are not allowed to act as CA certs. + if (!isCA) { + return Result::ERROR_CA_CERT_INVALID; + } + + if (pathLenConstraint >= 0 && + static_cast(subCACount) > pathLenConstraint) { + return Result::ERROR_PATH_LEN_CONSTRAINT_INVALID; + } + + return Success; +} + +// 4.2.1.12. Extended Key Usage (id-ce-extKeyUsage) + +static Result +MatchEKU(Reader& value, KeyPurposeId requiredEKU, + EndEntityOrCA endEntityOrCA, TrustDomain& trustDomain, + Time notBefore, /*in/out*/ bool& found, + /*in/out*/ bool& foundOCSPSigning) +{ + // See Section 5.9 of "A Layman's Guide to a Subset of ASN.1, BER, and DER" + // for a description of ASN.1 DER encoding of OIDs. + + // id-pkix OBJECT IDENTIFIER ::= + // { iso(1) identified-organization(3) dod(6) internet(1) + // security(5) mechanisms(5) pkix(7) } + // id-kp OBJECT IDENTIFIER ::= { id-pkix 3 } + // id-kp-serverAuth OBJECT IDENTIFIER ::= { id-kp 1 } + // id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 } + // id-kp-codeSigning OBJECT IDENTIFIER ::= { id-kp 3 } + // id-kp-emailProtection OBJECT IDENTIFIER ::= { id-kp 4 } + // id-kp-OCSPSigning OBJECT IDENTIFIER ::= { id-kp 9 } + static const uint8_t server[] = { (40*1)+3, 6, 1, 5, 5, 7, 3, 1 }; + static const uint8_t client[] = { (40*1)+3, 6, 1, 5, 5, 7, 3, 2 }; + static const uint8_t code [] = { (40*1)+3, 6, 1, 5, 5, 7, 3, 3 }; + static const uint8_t email [] = { (40*1)+3, 6, 1, 5, 5, 7, 3, 4 }; + static const uint8_t ocsp [] = { (40*1)+3, 6, 1, 5, 5, 7, 3, 9 }; + + // id-Netscape OBJECT IDENTIFIER ::= { 2 16 840 1 113730 } + // id-Netscape-policy OBJECT IDENTIFIER ::= { id-Netscape 4 } + // id-Netscape-stepUp OBJECT IDENTIFIER ::= { id-Netscape-policy 1 } + static const uint8_t serverStepUp[] = + { (40*2)+16, 128+6,72, 1, 128+6,128+120,66, 4, 1 }; + + bool match = false; + + if (!found) { + switch (requiredEKU) { + case KeyPurposeId::id_kp_serverAuth: { + if (value.MatchRest(server)) { + match = true; + break; + } + // Potentially treat CA certs with step-up OID as also having SSL server + // type. Comodo has issued certificates that require this behavior that + // don't expire until June 2020! + if (endEntityOrCA == EndEntityOrCA::MustBeCA && + value.MatchRest(serverStepUp)) { + Result rv = trustDomain.NetscapeStepUpMatchesServerAuth(notBefore, + match); + if (rv != Success) { + return rv; + } + } + break; + } + + case KeyPurposeId::id_kp_clientAuth: + match = value.MatchRest(client); + break; + + case KeyPurposeId::id_kp_codeSigning: + match = value.MatchRest(code); + break; + + case KeyPurposeId::id_kp_emailProtection: + match = value.MatchRest(email); + break; + + case KeyPurposeId::id_kp_OCSPSigning: + match = value.MatchRest(ocsp); + break; + + case KeyPurposeId::anyExtendedKeyUsage: + return NotReached("anyExtendedKeyUsage should start with found==true", + Result::FATAL_ERROR_LIBRARY_FAILURE); + } + } + + if (match) { + found = true; + if (requiredEKU == KeyPurposeId::id_kp_OCSPSigning) { + foundOCSPSigning = true; + } + } else if (value.MatchRest(ocsp)) { + foundOCSPSigning = true; + } + + value.SkipToEnd(); // ignore unmatched OIDs. + + return Success; +} + +Result +CheckExtendedKeyUsage(EndEntityOrCA endEntityOrCA, + const Input* encodedExtendedKeyUsage, + KeyPurposeId requiredEKU, TrustDomain& trustDomain, + Time notBefore) +{ + // XXX: We're using Result::ERROR_INADEQUATE_CERT_TYPE here so that callers + // can distinguish EKU mismatch from KU mismatch from basic constraints + // mismatch. We should probably add a new error code that is more clear for + // this type of problem. + + bool foundOCSPSigning = false; + + if (encodedExtendedKeyUsage) { + bool found = requiredEKU == KeyPurposeId::anyExtendedKeyUsage; + + Reader input(*encodedExtendedKeyUsage); + Result rv = der::NestedOf(input, der::SEQUENCE, der::OIDTag, + der::EmptyAllowed::No, [&](Reader& r) { + return MatchEKU(r, requiredEKU, endEntityOrCA, trustDomain, notBefore, + found, foundOCSPSigning); + }); + if (rv != Success) { + return Result::ERROR_INADEQUATE_CERT_TYPE; + } + if (der::End(input) != Success) { + return Result::ERROR_INADEQUATE_CERT_TYPE; + } + + // If the EKU extension was included, then the required EKU must be in the + // list. + if (!found) { + return Result::ERROR_INADEQUATE_CERT_TYPE; + } + } + + // pkixocsp.cpp depends on the following additional checks. + + if (endEntityOrCA == EndEntityOrCA::MustBeEndEntity) { + // When validating anything other than an delegated OCSP signing cert, + // reject any cert that also claims to be an OCSP responder, because such + // a cert does not make sense. For example, if an SSL certificate were to + // assert id-kp-OCSPSigning then it could sign OCSP responses for itself, + // if not for this check. + // That said, we accept CA certificates with id-kp-OCSPSigning because + // some CAs in Mozilla's CA program have issued such intermediate + // certificates, and because some CAs have reported some Microsoft server + // software wrongly requires CA certificates to have id-kp-OCSPSigning. + // Allowing this exception does not cause any security issues because we + // require delegated OCSP response signing certificates to be end-entity + // certificates. + if (foundOCSPSigning && requiredEKU != KeyPurposeId::id_kp_OCSPSigning) { + return Result::ERROR_INADEQUATE_CERT_TYPE; + } + // http://tools.ietf.org/html/rfc6960#section-4.2.2.2: + // "OCSP signing delegation SHALL be designated by the inclusion of + // id-kp-OCSPSigning in an extended key usage certificate extension + // included in the OCSP response signer's certificate." + // + // id-kp-OCSPSigning is the only EKU that isn't implicitly assumed when the + // EKU extension is missing from an end-entity certificate. However, any CA + // certificate can issue a delegated OCSP response signing certificate, so + // we can't require the EKU be explicitly included for CA certificates. + if (!foundOCSPSigning && requiredEKU == KeyPurposeId::id_kp_OCSPSigning) { + return Result::ERROR_INADEQUATE_CERT_TYPE; + } + } + + return Success; +} + +Result +CheckTLSFeatures(const BackCert& subject, BackCert& potentialIssuer) +{ + const Input* issuerTLSFeatures = potentialIssuer.GetRequiredTLSFeatures(); + if (!issuerTLSFeatures) { + return Success; + } + + const Input* subjectTLSFeatures = subject.GetRequiredTLSFeatures(); + if (issuerTLSFeatures->GetLength() == 0 || + !subjectTLSFeatures || + !InputsAreEqual(*issuerTLSFeatures, *subjectTLSFeatures)) { + return Result::ERROR_REQUIRED_TLS_FEATURE_MISSING; + } + + return Success; +} + +Result +TLSFeaturesSatisfiedInternal(const Input* requiredTLSFeatures, + const Input* stapledOCSPResponse) +{ + if (!requiredTLSFeatures) { + return Success; + } + + // RFC 6066 10.2: ExtensionType status_request + const static uint8_t status_request = 5; + const static uint8_t status_request_bytes[] = { status_request }; + + Reader input(*requiredTLSFeatures); + return der::NestedOf(input, der::SEQUENCE, der::INTEGER, + der::EmptyAllowed::No, [&](Reader& r) { + if (!r.MatchRest(status_request_bytes)) { + return Result::ERROR_REQUIRED_TLS_FEATURE_MISSING; + } + + if (!stapledOCSPResponse) { + return Result::ERROR_REQUIRED_TLS_FEATURE_MISSING; + } + + return Result::Success; + }); +} + +Result +CheckTLSFeaturesAreSatisfied(Input& cert, + const Input* stapledOCSPResponse) +{ + BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr); + Result rv = backCert.Init(); + if (rv != Success) { + return rv; + } + + return TLSFeaturesSatisfiedInternal(backCert.GetRequiredTLSFeatures(), + stapledOCSPResponse); +} + +Result +CheckIssuerIndependentProperties(TrustDomain& trustDomain, + const BackCert& cert, + Time time, + KeyUsage requiredKeyUsageIfPresent, + KeyPurposeId requiredEKUIfPresent, + const CertPolicyId& requiredPolicy, + unsigned int subCACount, + /*out*/ TrustLevel& trustLevel) +{ + Result rv; + + const EndEntityOrCA endEntityOrCA = cert.endEntityOrCA; + + // Check the cert's trust first, because we want to minimize the amount of + // processing we do on a distrusted cert, in case it is trying to exploit + // some bug in our processing. + rv = trustDomain.GetCertTrust(endEntityOrCA, requiredPolicy, cert.GetDER(), + trustLevel); + if (rv != Success) { + return rv; + } + + // IMPORTANT: We parse the validity interval here, so that we can use the + // notBefore and notAfter values in checks for things that might be deprecated + // over time. However, we must not fail for semantic errors until the end of + // this method, in order to preserve error ranking. + Time notBefore(Time::uninitialized); + Time notAfter(Time::uninitialized); + rv = ParseValidity(cert.GetValidity(), ¬Before, ¬After); + if (rv != Success) { + return rv; + } + + if (trustLevel == TrustLevel::TrustAnchor && + endEntityOrCA == EndEntityOrCA::MustBeEndEntity && + requiredEKUIfPresent == KeyPurposeId::id_kp_OCSPSigning) { + // OCSP signer certificates can never be trust anchors, especially + // since we don't support designated OCSP responders. All of the checks + // below that are dependent on trustLevel rely on this overriding of the + // trust level for OCSP signers. + trustLevel = TrustLevel::InheritsTrust; + } + + switch (trustLevel) { + case TrustLevel::InheritsTrust: + rv = CheckSignatureAlgorithm(trustDomain, endEntityOrCA, notBefore, + cert.GetSignedData(), cert.GetSignature()); + if (rv != Success) { + return rv; + } + break; + + case TrustLevel::TrustAnchor: + // We don't even bother checking signatureAlgorithm or signature for + // syntactic validity for trust anchors, because we don't use those + // fields for anything, and because the trust anchor might be signed + // with a signature algorithm we don't actually support. + break; + + case TrustLevel::ActivelyDistrusted: + return Result::ERROR_UNTRUSTED_CERT; + } + + // Check the SPKI early, because it is one of the most selective properties + // of the certificate due to SHA-1 deprecation and the deprecation of + // certificates with keys weaker than RSA 2048. + rv = CheckSubjectPublicKeyInfo(cert.GetSubjectPublicKeyInfo(), trustDomain, + endEntityOrCA); + if (rv != Success) { + return rv; + } + + // 4.1.2.4. Issuer + rv = CheckIssuer(cert.GetIssuer()); + if (rv != Success) { + return rv; + } + + // 4.2.1.1. Authority Key Identifier is ignored (see bug 965136). + + // 4.2.1.2. Subject Key Identifier is ignored (see bug 965136). + + // 4.2.1.3. Key Usage + rv = CheckKeyUsage(endEntityOrCA, cert.GetKeyUsage(), + requiredKeyUsageIfPresent); + if (rv != Success) { + return rv; + } + + // 4.2.1.4. Certificate Policies + rv = CheckCertificatePolicies(endEntityOrCA, cert.GetCertificatePolicies(), + cert.GetInhibitAnyPolicy(), trustLevel, + requiredPolicy); + if (rv != Success) { + return rv; + } + + // 4.2.1.5. Policy Mappings are not supported; see the documentation about + // policy enforcement in pkix.h. + + // 4.2.1.6. Subject Alternative Name dealt with during name constraint + // checking and during name verification (CERT_VerifyCertName). + + // 4.2.1.7. Issuer Alternative Name is not something that needs checking. + + // 4.2.1.8. Subject Directory Attributes is not something that needs + // checking. + + // 4.2.1.9. Basic Constraints. + rv = CheckBasicConstraints(endEntityOrCA, cert.GetBasicConstraints(), + cert.GetVersion(), trustLevel, subCACount); + if (rv != Success) { + return rv; + } + + // 4.2.1.10. Name Constraints is dealt with in during path building. + + // 4.2.1.11. Policy Constraints are implicitly supported; see the + // documentation about policy enforcement in pkix.h. + + // 4.2.1.12. Extended Key Usage + rv = CheckExtendedKeyUsage(endEntityOrCA, cert.GetExtKeyUsage(), + requiredEKUIfPresent, trustDomain, notBefore); + if (rv != Success) { + return rv; + } + + // 4.2.1.13. CRL Distribution Points is not supported, though the + // TrustDomain's CheckRevocation method may parse it and process it + // on its own. + + // 4.2.1.14. Inhibit anyPolicy is implicitly supported; see the documentation + // about policy enforcement in pkix.h. + + // IMPORTANT: Even though we parse validity above, we wait until this point to + // check it, so that error ranking works correctly. + rv = CheckValidity(time, notBefore, notAfter); + if (rv != Success) { + return rv; + } + + rv = trustDomain.CheckValidityIsAcceptable(notBefore, notAfter, endEntityOrCA, + requiredEKUIfPresent); + if (rv != Success) { + return rv; + } + + return Success; +} + +} } // namespace mozilla::pkix diff --git a/security/pkix/lib/pkixcheck.h b/security/pkix/lib/pkixcheck.h new file mode 100644 index 000000000..9ea205f3b --- /dev/null +++ b/security/pkix/lib/pkixcheck.h @@ -0,0 +1,66 @@ +/* -*- 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. + */ + +#ifndef mozilla_pkix_pkixcheck_h +#define mozilla_pkix_pkixcheck_h + +#include "pkix/pkixtypes.h" + +namespace mozilla { namespace pkix { + +class BackCert; + +Result CheckIssuerIndependentProperties( + TrustDomain& trustDomain, + const BackCert& cert, + Time time, + KeyUsage requiredKeyUsageIfPresent, + KeyPurposeId requiredEKUIfPresent, + const CertPolicyId& requiredPolicy, + unsigned int subCACount, + /*out*/ TrustLevel& trustLevel); + +Result CheckNameConstraints(Input encodedNameConstraints, + const BackCert& firstChild, + KeyPurposeId requiredEKUIfPresent); + +Result CheckIssuer(Input encodedIssuer); + +// ParseValidity and CheckValidity are usually used together. First you parse +// the dates from the DER Validity sequence, then you compare them to the time +// at which you are validating. They are separate so that the notBefore and +// notAfter times can be used for other things before they are checked against +// the time of validation. +Result ParseValidity(Input encodedValidity, + /*optional out*/ Time* notBeforeOut = nullptr, + /*optional out*/ Time* notAfterOut = nullptr); +Result CheckValidity(Time time, Time notBefore, Time notAfter); + +// Check that a subject has TLS Feature (rfc7633) requirements that match its +// potential issuer +Result CheckTLSFeatures(const BackCert& subject, BackCert& potentialIssuer); + +} } // namespace mozilla::pkix + +#endif // mozilla_pkix_pkixcheck_h diff --git a/security/pkix/lib/pkixder.cpp b/security/pkix/lib/pkixder.cpp new file mode 100644 index 000000000..0660b42b2 --- /dev/null +++ b/security/pkix/lib/pkixder.cpp @@ -0,0 +1,612 @@ +/* -*- 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 "pkixder.h" + +#include "pkixutil.h" + +namespace mozilla { namespace pkix { namespace der { + +// Too complicated to be inline +Result +ReadTagAndGetValue(Reader& input, /*out*/ uint8_t& tag, /*out*/ Input& value) +{ + Result rv; + + rv = input.Read(tag); + if (rv != Success) { + return rv; + } + if ((tag & 0x1F) == 0x1F) { + return Result::ERROR_BAD_DER; // high tag number form not allowed + } + + uint16_t length; + + // The short form of length is a single byte with the high order bit set + // to zero. The long form of length is one byte with the high order bit + // set, followed by N bytes, where N is encoded in the lowest 7 bits of + // the first byte. + uint8_t length1; + rv = input.Read(length1); + if (rv != Success) { + return rv; + } + if (!(length1 & 0x80)) { + length = length1; + } else if (length1 == 0x81) { + uint8_t length2; + rv = input.Read(length2); + if (rv != Success) { + return rv; + } + if (length2 < 128) { + // Not shortest possible encoding + return Result::ERROR_BAD_DER; + } + length = length2; + } else if (length1 == 0x82) { + rv = input.Read(length); + if (rv != Success) { + return rv; + } + if (length < 256) { + // Not shortest possible encoding + return Result::ERROR_BAD_DER; + } + } else { + // We don't support lengths larger than 2^16 - 1. + return Result::ERROR_BAD_DER; + } + + return input.Skip(length, value); +} + +static Result +OptionalNull(Reader& input) +{ + if (input.Peek(NULLTag)) { + return Null(input); + } + return Success; +} + +namespace { + +Result +AlgorithmIdentifierValue(Reader& input, /*out*/ Reader& algorithmOIDValue) +{ + Result rv = ExpectTagAndGetValue(input, der::OIDTag, algorithmOIDValue); + if (rv != Success) { + return rv; + } + return OptionalNull(input); +} + +} // namespace + +Result +SignatureAlgorithmIdentifierValue(Reader& input, + /*out*/ PublicKeyAlgorithm& publicKeyAlgorithm, + /*out*/ DigestAlgorithm& digestAlgorithm) +{ + // RFC 5758 Section 3.2 (ECDSA with SHA-2), and RFC 3279 Section 2.2.3 + // (ECDSA with SHA-1) say that parameters must be omitted. + // + // RFC 4055 Section 5 and RFC 3279 Section 2.2.1 both say that parameters for + // RSA must be encoded as NULL; we relax that requirement by allowing the + // NULL to be omitted, to match all the other signature algorithms we support + // and for compatibility. + Reader algorithmID; + Result rv = AlgorithmIdentifierValue(input, algorithmID); + if (rv != Success) { + return rv; + } + + // RFC 5758 Section 3.2 (ecdsa-with-SHA224 is intentionally excluded) + // python DottedOIDToCode.py ecdsa-with-SHA256 1.2.840.10045.4.3.2 + static const uint8_t ecdsa_with_SHA256[] = { + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02 + }; + // python DottedOIDToCode.py ecdsa-with-SHA384 1.2.840.10045.4.3.3 + static const uint8_t ecdsa_with_SHA384[] = { + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x03 + }; + // python DottedOIDToCode.py ecdsa-with-SHA512 1.2.840.10045.4.3.4 + static const uint8_t ecdsa_with_SHA512[] = { + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x04 + }; + + // RFC 4055 Section 5 (sha224WithRSAEncryption is intentionally excluded) + // python DottedOIDToCode.py sha256WithRSAEncryption 1.2.840.113549.1.1.11 + static const uint8_t sha256WithRSAEncryption[] = { + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b + }; + // python DottedOIDToCode.py sha384WithRSAEncryption 1.2.840.113549.1.1.12 + static const uint8_t sha384WithRSAEncryption[] = { + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0c + }; + // python DottedOIDToCode.py sha512WithRSAEncryption 1.2.840.113549.1.1.13 + static const uint8_t sha512WithRSAEncryption[] = { + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0d + }; + + // RFC 3279 Section 2.2.1 + // python DottedOIDToCode.py sha-1WithRSAEncryption 1.2.840.113549.1.1.5 + static const uint8_t sha_1WithRSAEncryption[] = { + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05 + }; + + // NIST Open Systems Environment (OSE) Implementor's Workshop (OIW) + // http://www.oiw.org/agreements/stable/12s-9412.txt (no longer works). + // http://www.imc.org/ietf-pkix/old-archive-97/msg01166.html + // We need to support this this non-PKIX OID for compatibility. + // python DottedOIDToCode.py sha1WithRSASignature 1.3.14.3.2.29 + static const uint8_t sha1WithRSASignature[] = { + 0x2b, 0x0e, 0x03, 0x02, 0x1d + }; + + // RFC 3279 Section 2.2.3 + // python DottedOIDToCode.py ecdsa-with-SHA1 1.2.840.10045.4.1 + static const uint8_t ecdsa_with_SHA1[] = { + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x01 + }; + + // Matching is attempted based on a rough estimate of the commonality of the + // algorithm, to minimize the number of MatchRest calls. + if (algorithmID.MatchRest(sha256WithRSAEncryption)) { + publicKeyAlgorithm = PublicKeyAlgorithm::RSA_PKCS1; + digestAlgorithm = DigestAlgorithm::sha256; + } else if (algorithmID.MatchRest(ecdsa_with_SHA256)) { + publicKeyAlgorithm = PublicKeyAlgorithm::ECDSA; + digestAlgorithm = DigestAlgorithm::sha256; + } else if (algorithmID.MatchRest(sha_1WithRSAEncryption)) { + publicKeyAlgorithm = PublicKeyAlgorithm::RSA_PKCS1; + digestAlgorithm = DigestAlgorithm::sha1; + } else if (algorithmID.MatchRest(ecdsa_with_SHA1)) { + publicKeyAlgorithm = PublicKeyAlgorithm::ECDSA; + digestAlgorithm = DigestAlgorithm::sha1; + } else if (algorithmID.MatchRest(ecdsa_with_SHA384)) { + publicKeyAlgorithm = PublicKeyAlgorithm::ECDSA; + digestAlgorithm = DigestAlgorithm::sha384; + } else if (algorithmID.MatchRest(ecdsa_with_SHA512)) { + publicKeyAlgorithm = PublicKeyAlgorithm::ECDSA; + digestAlgorithm = DigestAlgorithm::sha512; + } else if (algorithmID.MatchRest(sha384WithRSAEncryption)) { + publicKeyAlgorithm = PublicKeyAlgorithm::RSA_PKCS1; + digestAlgorithm = DigestAlgorithm::sha384; + } else if (algorithmID.MatchRest(sha512WithRSAEncryption)) { + publicKeyAlgorithm = PublicKeyAlgorithm::RSA_PKCS1; + digestAlgorithm = DigestAlgorithm::sha512; + } else if (algorithmID.MatchRest(sha1WithRSASignature)) { + // XXX(bug 1042479): recognize this old OID for compatibility. + publicKeyAlgorithm = PublicKeyAlgorithm::RSA_PKCS1; + digestAlgorithm = DigestAlgorithm::sha1; + } else { + return Result::ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED; + } + + return Success; +} + +Result +DigestAlgorithmIdentifier(Reader& input, /*out*/ DigestAlgorithm& algorithm) +{ + Reader r; + return der::Nested(input, SEQUENCE, [&algorithm](Reader& r) -> Result { + Reader algorithmID; + Result rv = AlgorithmIdentifierValue(r, algorithmID); + if (rv != Success) { + return rv; + } + + // RFC 4055 Section 2.1 + // python DottedOIDToCode.py id-sha1 1.3.14.3.2.26 + static const uint8_t id_sha1[] = { + 0x2b, 0x0e, 0x03, 0x02, 0x1a + }; + // python DottedOIDToCode.py id-sha256 2.16.840.1.101.3.4.2.1 + static const uint8_t id_sha256[] = { + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01 + }; + // python DottedOIDToCode.py id-sha384 2.16.840.1.101.3.4.2.2 + static const uint8_t id_sha384[] = { + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02 + }; + // python DottedOIDToCode.py id-sha512 2.16.840.1.101.3.4.2.3 + static const uint8_t id_sha512[] = { + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03 + }; + + // Matching is attempted based on a rough estimate of the commonality of the + // algorithm, to minimize the number of MatchRest calls. + if (algorithmID.MatchRest(id_sha1)) { + algorithm = DigestAlgorithm::sha1; + } else if (algorithmID.MatchRest(id_sha256)) { + algorithm = DigestAlgorithm::sha256; + } else if (algorithmID.MatchRest(id_sha384)) { + algorithm = DigestAlgorithm::sha384; + } else if (algorithmID.MatchRest(id_sha512)) { + algorithm = DigestAlgorithm::sha512; + } else { + return Result::ERROR_INVALID_ALGORITHM; + } + + return Success; + }); +} + +Result +SignedData(Reader& input, /*out*/ Reader& tbs, + /*out*/ SignedDataWithSignature& signedData) +{ + Reader::Mark mark(input.GetMark()); + + Result rv; + rv = ExpectTagAndGetValue(input, SEQUENCE, tbs); + if (rv != Success) { + return rv; + } + + rv = input.GetInput(mark, signedData.data); + if (rv != Success) { + return rv; + } + + rv = ExpectTagAndGetValue(input, der::SEQUENCE, signedData.algorithm); + if (rv != Success) { + return rv; + } + + rv = BitStringWithNoUnusedBits(input, signedData.signature); + if (rv == Result::ERROR_BAD_DER) { + rv = Result::ERROR_BAD_SIGNATURE; + } + return rv; +} + +Result +BitStringWithNoUnusedBits(Reader& input, /*out*/ Input& value) +{ + Reader valueWithUnusedBits; + Result rv = ExpectTagAndGetValue(input, BIT_STRING, valueWithUnusedBits); + if (rv != Success) { + return rv; + } + + uint8_t unusedBitsAtEnd; + if (valueWithUnusedBits.Read(unusedBitsAtEnd) != Success) { + return Result::ERROR_BAD_DER; + } + // XXX: Really the constraint should be that unusedBitsAtEnd must be less + // than 7. But, we suspect there are no real-world values in OCSP responses + // or certificates with non-zero unused bits. It seems like NSS assumes this + // in various places, so we enforce it too in order to simplify this code. If + // we find compatibility issues, we'll know we're wrong and we'll have to + // figure out how to shift the bits around. + if (unusedBitsAtEnd != 0) { + return Result::ERROR_BAD_DER; + } + return valueWithUnusedBits.SkipToEnd(value); +} + +static inline Result +ReadDigit(Reader& input, /*out*/ unsigned int& value) +{ + uint8_t b; + if (input.Read(b) != Success) { + return Result::ERROR_INVALID_DER_TIME; + } + if (b < '0' || b > '9') { + return Result::ERROR_INVALID_DER_TIME; + } + value = static_cast(b - static_cast('0')); + return Success; +} + +static inline Result +ReadTwoDigits(Reader& input, unsigned int minValue, unsigned int maxValue, + /*out*/ unsigned int& value) +{ + unsigned int hi; + Result rv = ReadDigit(input, hi); + if (rv != Success) { + return rv; + } + unsigned int lo; + rv = ReadDigit(input, lo); + if (rv != Success) { + return rv; + } + value = (hi * 10) + lo; + if (value < minValue || value > maxValue) { + return Result::ERROR_INVALID_DER_TIME; + } + return Success; +} + +namespace internal { + +// We parse GeneralizedTime and UTCTime according to RFC 5280 and we do not +// accept all time formats allowed in the ASN.1 spec. That is, +// GeneralizedTime must always be in the format YYYYMMDDHHMMSSZ and UTCTime +// must always be in the format YYMMDDHHMMSSZ. Timezone formats of the form +// +HH:MM or -HH:MM or NOT accepted. +Result +TimeChoice(Reader& tagged, uint8_t expectedTag, /*out*/ Time& time) +{ + unsigned int days; + + Reader input; + Result rv = ExpectTagAndGetValue(tagged, expectedTag, input); + if (rv != Success) { + return rv; + } + + unsigned int yearHi; + unsigned int yearLo; + if (expectedTag == GENERALIZED_TIME) { + rv = ReadTwoDigits(input, 0, 99, yearHi); + if (rv != Success) { + return rv; + } + rv = ReadTwoDigits(input, 0, 99, yearLo); + if (rv != Success) { + return rv; + } + } else if (expectedTag == UTCTime) { + rv = ReadTwoDigits(input, 0, 99, yearLo); + if (rv != Success) { + return rv; + } + yearHi = yearLo >= 50u ? 19u : 20u; + } else { + return NotReached("invalid tag given to TimeChoice", + Result::ERROR_INVALID_DER_TIME); + } + unsigned int year = (yearHi * 100u) + yearLo; + if (year < 1970u) { + // We don't support dates before January 1, 1970 because that is the epoch. + return Result::ERROR_INVALID_DER_TIME; + } + days = DaysBeforeYear(year); + + unsigned int month; + rv = ReadTwoDigits(input, 1u, 12u, month); + if (rv != Success) { + return rv; + } + unsigned int daysInMonth; + static const unsigned int jan = 31u; + const unsigned int feb = ((year % 4u == 0u) && + ((year % 100u != 0u) || (year % 400u == 0u))) + ? 29u + : 28u; + static const unsigned int mar = 31u; + static const unsigned int apr = 30u; + static const unsigned int may = 31u; + static const unsigned int jun = 30u; + static const unsigned int jul = 31u; + static const unsigned int aug = 31u; + static const unsigned int sep = 30u; + static const unsigned int oct = 31u; + static const unsigned int nov = 30u; + static const unsigned int dec = 31u; + switch (month) { + case 1: daysInMonth = jan; break; + case 2: daysInMonth = feb; days += jan; break; + case 3: daysInMonth = mar; days += jan + feb; break; + case 4: daysInMonth = apr; days += jan + feb + mar; break; + case 5: daysInMonth = may; days += jan + feb + mar + apr; break; + case 6: daysInMonth = jun; days += jan + feb + mar + apr + may; break; + case 7: daysInMonth = jul; days += jan + feb + mar + apr + may + jun; + break; + case 8: daysInMonth = aug; days += jan + feb + mar + apr + may + jun + + jul; + break; + case 9: daysInMonth = sep; days += jan + feb + mar + apr + may + jun + + jul + aug; + break; + case 10: daysInMonth = oct; days += jan + feb + mar + apr + may + jun + + jul + aug + sep; + break; + case 11: daysInMonth = nov; days += jan + feb + mar + apr + may + jun + + jul + aug + sep + oct; + break; + case 12: daysInMonth = dec; days += jan + feb + mar + apr + may + jun + + jul + aug + sep + oct + nov; + break; + default: + return NotReached("month already bounds-checked by ReadTwoDigits", + Result::FATAL_ERROR_INVALID_STATE); + } + + unsigned int dayOfMonth; + rv = ReadTwoDigits(input, 1u, daysInMonth, dayOfMonth); + if (rv != Success) { + return rv; + } + days += dayOfMonth - 1; + + unsigned int hours; + rv = ReadTwoDigits(input, 0u, 23u, hours); + if (rv != Success) { + return rv; + } + unsigned int minutes; + rv = ReadTwoDigits(input, 0u, 59u, minutes); + if (rv != Success) { + return rv; + } + unsigned int seconds; + rv = ReadTwoDigits(input, 0u, 59u, seconds); + if (rv != Success) { + return rv; + } + + uint8_t b; + if (input.Read(b) != Success) { + return Result::ERROR_INVALID_DER_TIME; + } + if (b != 'Z') { + return Result::ERROR_INVALID_DER_TIME; + } + if (End(input) != Success) { + return Result::ERROR_INVALID_DER_TIME; + } + + uint64_t totalSeconds = (static_cast(days) * 24u * 60u * 60u) + + (static_cast(hours) * 60u * 60u) + + (static_cast(minutes) * 60u) + + seconds; + + time = TimeFromElapsedSecondsAD(totalSeconds); + return Success; +} + +Result +IntegralBytes(Reader& input, uint8_t tag, + IntegralValueRestriction valueRestriction, + /*out*/ Input& value, + /*optional out*/ Input::size_type* significantBytes) +{ + Result rv = ExpectTagAndGetValue(input, tag, value); + if (rv != Success) { + return rv; + } + Reader reader(value); + + // There must be at least one byte in the value. (Zero is encoded with a + // single 0x00 value byte.) + uint8_t firstByte; + rv = reader.Read(firstByte); + if (rv != Success) { + if (rv == Result::ERROR_BAD_DER) { + return Result::ERROR_INVALID_INTEGER_ENCODING; + } + + return rv; + } + + // If there is a byte after an initial 0x00/0xFF, then the initial byte + // indicates a positive/negative integer value with its high bit set/unset. + bool prefixed = !reader.AtEnd() && (firstByte == 0 || firstByte == 0xff); + + if (prefixed) { + uint8_t nextByte; + if (reader.Read(nextByte) != Success) { + return NotReached("Read of one byte failed but not at end.", + Result::FATAL_ERROR_LIBRARY_FAILURE); + } + if ((firstByte & 0x80) == (nextByte & 0x80)) { + return Result::ERROR_INVALID_INTEGER_ENCODING; + } + } + + switch (valueRestriction) { + case IntegralValueRestriction::MustBe0To127: + if (value.GetLength() != 1 || (firstByte & 0x80) != 0) { + return Result::ERROR_INVALID_INTEGER_ENCODING; + } + break; + + case IntegralValueRestriction::MustBePositive: + if ((value.GetLength() == 1 && firstByte == 0) || + (firstByte & 0x80) != 0) { + return Result::ERROR_INVALID_INTEGER_ENCODING; + } + break; + + case IntegralValueRestriction::NoRestriction: + break; + } + + if (significantBytes) { + *significantBytes = value.GetLength(); + if (prefixed) { + assert(*significantBytes > 1); + --*significantBytes; + } + + assert(*significantBytes > 0); + } + + return Success; +} + +// This parser will only parse values between 0..127. If this range is +// increased then callers will need to be changed. +Result +IntegralValue(Reader& input, uint8_t tag, /*out*/ uint8_t& value) +{ + // Conveniently, all the Integers that we actually have to be able to parse + // are positive and very small. Consequently, this parser is *much* simpler + // than a general Integer parser would need to be. + Input valueBytes; + Result rv = IntegralBytes(input, tag, IntegralValueRestriction::MustBe0To127, + valueBytes, nullptr); + if (rv != Success) { + return rv; + } + Reader valueReader(valueBytes); + rv = valueReader.Read(value); + if (rv != Success) { + return NotReached("IntegralBytes already validated the value.", rv); + } + rv = End(valueReader); + assert(rv == Success); // guaranteed by IntegralBytes's range checks. + return rv; +} + +} // namespace internal + +Result +OptionalVersion(Reader& input, /*out*/ Version& version) +{ + static const uint8_t TAG = CONTEXT_SPECIFIC | CONSTRUCTED | 0; + if (!input.Peek(TAG)) { + version = Version::v1; + return Success; + } + return Nested(input, TAG, [&version](Reader& value) -> Result { + uint8_t integerValue; + Result rv = Integer(value, integerValue); + if (rv != Success) { + return rv; + } + // XXX(bug 1031093): We shouldn't accept an explicit encoding of v1, + // but we do here for compatibility reasons. + switch (integerValue) { + case static_cast(Version::v3): version = Version::v3; break; + case static_cast(Version::v2): version = Version::v2; break; + case static_cast(Version::v1): version = Version::v1; break; + case static_cast(Version::v4): version = Version::v4; break; + default: + return Result::ERROR_BAD_DER; + } + return Success; + }); +} + +} } } // namespace mozilla::pkix::der diff --git a/security/pkix/lib/pkixder.h b/security/pkix/lib/pkixder.h new file mode 100644 index 000000000..a17114bcb --- /dev/null +++ b/security/pkix/lib/pkixder.h @@ -0,0 +1,565 @@ +/* -*- 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. + */ + +#ifndef mozilla_pkix_pkixder_h +#define mozilla_pkix_pkixder_h + +// Expect* functions advance the input mark and return Success if the input +// matches the given criteria; they fail with the input mark in an undefined +// state if the input does not match the criteria. +// +// Match* functions advance the input mark and return true if the input matches +// the given criteria; they return false without changing the input mark if the +// input does not match the criteria. +// +// Skip* functions unconditionally advance the input mark and return Success if +// they are able to do so; otherwise they fail with the input mark in an +// undefined state. + +#include "pkix/Input.h" +#include "pkix/pkixtypes.h" + +namespace mozilla { namespace pkix { namespace der { + +enum Class : uint8_t +{ + UNIVERSAL = 0 << 6, +// APPLICATION = 1 << 6, // unused + CONTEXT_SPECIFIC = 2 << 6, +// PRIVATE = 3 << 6 // unused +}; + +enum Constructed +{ + CONSTRUCTED = 1 << 5 +}; + +enum Tag : uint8_t +{ + BOOLEAN = UNIVERSAL | 0x01, + INTEGER = UNIVERSAL | 0x02, + BIT_STRING = UNIVERSAL | 0x03, + OCTET_STRING = UNIVERSAL | 0x04, + NULLTag = UNIVERSAL | 0x05, + OIDTag = UNIVERSAL | 0x06, + ENUMERATED = UNIVERSAL | 0x0a, + UTF8String = UNIVERSAL | 0x0c, + SEQUENCE = UNIVERSAL | CONSTRUCTED | 0x10, // 0x30 + SET = UNIVERSAL | CONSTRUCTED | 0x11, // 0x31 + PrintableString = UNIVERSAL | 0x13, + TeletexString = UNIVERSAL | 0x14, + IA5String = UNIVERSAL | 0x16, + UTCTime = UNIVERSAL | 0x17, + GENERALIZED_TIME = UNIVERSAL | 0x18, +}; + +enum class EmptyAllowed { No = 0, Yes = 1 }; + +Result ReadTagAndGetValue(Reader& input, /*out*/ uint8_t& tag, + /*out*/ Input& value); +Result End(Reader& input); + +inline Result +ExpectTagAndGetValue(Reader& input, uint8_t tag, /*out*/ Input& value) +{ + uint8_t actualTag; + Result rv = ReadTagAndGetValue(input, actualTag, value); + if (rv != Success) { + return rv; + } + if (tag != actualTag) { + return Result::ERROR_BAD_DER; + } + return Success; +} + +inline Result +ExpectTagAndGetValue(Reader& input, uint8_t tag, /*out*/ Reader& value) +{ + Input valueInput; + Result rv = ExpectTagAndGetValue(input, tag, valueInput); + if (rv != Success) { + return rv; + } + return value.Init(valueInput); +} + +inline Result +ExpectTagAndEmptyValue(Reader& input, uint8_t tag) +{ + Reader value; + Result rv = ExpectTagAndGetValue(input, tag, value); + if (rv != Success) { + return rv; + } + return End(value); +} + +inline Result +ExpectTagAndSkipValue(Reader& input, uint8_t tag) +{ + Input ignoredValue; + return ExpectTagAndGetValue(input, tag, ignoredValue); +} + +// Like ExpectTagAndGetValue, except the output Input will contain the +// encoded tag and length along with the value. +inline Result +ExpectTagAndGetTLV(Reader& input, uint8_t tag, /*out*/ Input& tlv) +{ + Reader::Mark mark(input.GetMark()); + Result rv = ExpectTagAndSkipValue(input, tag); + if (rv != Success) { + return rv; + } + return input.GetInput(mark, tlv); +} + +inline Result +End(Reader& input) +{ + if (!input.AtEnd()) { + return Result::ERROR_BAD_DER; + } + + return Success; +} + +template +inline Result +Nested(Reader& input, uint8_t tag, Decoder decoder) +{ + Reader nested; + Result rv = ExpectTagAndGetValue(input, tag, nested); + if (rv != Success) { + return rv; + } + rv = decoder(nested); + if (rv != Success) { + return rv; + } + return End(nested); +} + +template +inline Result +Nested(Reader& input, uint8_t outerTag, uint8_t innerTag, Decoder decoder) +{ + Reader nestedInput; + Result rv = ExpectTagAndGetValue(input, outerTag, nestedInput); + if (rv != Success) { + return rv; + } + rv = Nested(nestedInput, innerTag, decoder); + if (rv != Success) { + return rv; + } + return End(nestedInput); +} + +// This can be used to decode constructs like this: +// +// ... +// foos SEQUENCE OF Foo, +// ... +// Foo ::= SEQUENCE { +// } +// +// using code like this: +// +// Result Foo(Reader& r) { /*...*/ } +// +// rv = der::NestedOf(input, der::SEQEUENCE, der::SEQUENCE, Foo); +// +// or: +// +// Result Bar(Reader& r, int value) { /*...*/ } +// +// int value = /*...*/; +// +// rv = der::NestedOf(input, der::SEQUENCE, [value](Reader& r) { +// return Bar(r, value); +// }); +// +// In these examples the function will get called once for each element of +// foos. +// +template +inline Result +NestedOf(Reader& input, uint8_t outerTag, uint8_t innerTag, + EmptyAllowed mayBeEmpty, Decoder decoder) +{ + Reader inner; + Result rv = ExpectTagAndGetValue(input, outerTag, inner); + if (rv != Success) { + return rv; + } + + if (inner.AtEnd()) { + if (mayBeEmpty != EmptyAllowed::Yes) { + return Result::ERROR_BAD_DER; + } + return Success; + } + + do { + rv = Nested(inner, innerTag, decoder); + if (rv != Success) { + return rv; + } + } while (!inner.AtEnd()); + + return Success; +} + +// Often, a function will need to decode an Input or Reader that contains +// DER-encoded data wrapped in a SEQUENCE (or similar) with nothing after it. +// This function reduces the boilerplate necessary for stripping the outermost +// SEQUENCE (or similar) and ensuring that nothing follows it. +inline Result +ExpectTagAndGetValueAtEnd(Reader& outer, uint8_t expectedTag, + /*out*/ Reader& inner) +{ + Result rv = der::ExpectTagAndGetValue(outer, expectedTag, inner); + if (rv != Success) { + return rv; + } + return der::End(outer); +} + +// Similar to the above, but takes an Input instead of a Reader&. +inline Result +ExpectTagAndGetValueAtEnd(Input outer, uint8_t expectedTag, + /*out*/ Reader& inner) +{ + Reader outerReader(outer); + return ExpectTagAndGetValueAtEnd(outerReader, expectedTag, inner); +} + +// Universal types + +namespace internal { + +enum class IntegralValueRestriction +{ + NoRestriction, + MustBePositive, + MustBe0To127, +}; + +Result IntegralBytes(Reader& input, uint8_t tag, + IntegralValueRestriction valueRestriction, + /*out*/ Input& value, + /*optional out*/ Input::size_type* significantBytes = nullptr); + +// This parser will only parse values between 0..127. If this range is +// increased then callers will need to be changed. +Result IntegralValue(Reader& input, uint8_t tag, /*out*/ uint8_t& value); + +} // namespace internal + +Result +BitStringWithNoUnusedBits(Reader& input, /*out*/ Input& value); + +inline Result +Boolean(Reader& input, /*out*/ bool& value) +{ + Reader valueReader; + Result rv = ExpectTagAndGetValue(input, BOOLEAN, valueReader); + if (rv != Success) { + return rv; + } + + uint8_t intValue; + rv = valueReader.Read(intValue); + if (rv != Success) { + return rv; + } + rv = End(valueReader); + if (rv != Success) { + return rv; + } + switch (intValue) { + case 0: value = false; return Success; + case 0xFF: value = true; return Success; + default: + return Result::ERROR_BAD_DER; + } +} + +// This is for BOOLEAN DEFAULT FALSE. +// The standard stipulates that "The encoding of a set value or sequence value +// shall not include an encoding for any component value which is equal to its +// default value." However, it appears to be common that other libraries +// incorrectly include the value of a BOOLEAN even when it's equal to the +// default value, so we allow invalid explicit encodings here. +inline Result +OptionalBoolean(Reader& input, /*out*/ bool& value) +{ + value = false; + if (input.Peek(BOOLEAN)) { + Result rv = Boolean(input, value); + if (rv != Success) { + return rv; + } + } + return Success; +} + +// This parser will only parse values between 0..127. If this range is +// increased then callers will need to be changed. +inline Result +Enumerated(Reader& input, uint8_t& value) +{ + return internal::IntegralValue(input, ENUMERATED | 0, value); +} + +namespace internal { + +// internal::TimeChoice implements the shared functionality of GeneralizedTime +// and TimeChoice. tag must be either UTCTime or GENERALIZED_TIME. +// +// Only times from 1970-01-01-00:00:00 onward are accepted, in order to +// eliminate the chance for complications in converting times to traditional +// time formats that start at 1970. +Result TimeChoice(Reader& input, uint8_t tag, /*out*/ Time& time); + +} // namespace internal + +// Only times from 1970-01-01-00:00:00 onward are accepted, in order to +// eliminate the chance for complications in converting times to traditional +// time formats that start at 1970. +inline Result +GeneralizedTime(Reader& input, /*out*/ Time& time) +{ + return internal::TimeChoice(input, GENERALIZED_TIME, time); +} + +// Only times from 1970-01-01-00:00:00 onward are accepted, in order to +// eliminate the chance for complications in converting times to traditional +// time formats that start at 1970. +inline Result +TimeChoice(Reader& input, /*out*/ Time& time) +{ + uint8_t expectedTag = input.Peek(UTCTime) ? UTCTime : GENERALIZED_TIME; + return internal::TimeChoice(input, expectedTag, time); +} + +// Parse a DER integer value into value. Empty values, negative values, and +// zero are rejected. If significantBytes is not null, then it will be set to +// the number of significant bytes in the value (the length of the value, less +// the length of any leading padding), which is useful for key size checks. +inline Result +PositiveInteger(Reader& input, /*out*/ Input& value, + /*optional out*/ Input::size_type* significantBytes = nullptr) +{ + return internal::IntegralBytes( + input, INTEGER, internal::IntegralValueRestriction::MustBePositive, + value, significantBytes); +} + +// This parser will only parse values between 0..127. If this range is +// increased then callers will need to be changed. +inline Result +Integer(Reader& input, /*out*/ uint8_t& value) +{ + return internal::IntegralValue(input, INTEGER, value); +} + +// This parser will only parse values between 0..127. If this range is +// increased then callers will need to be changed. The default value must be +// -1; defaultValue is only a parameter to make it clear in the calling code +// what the default value is. +inline Result +OptionalInteger(Reader& input, long defaultValue, /*out*/ long& value) +{ + // If we need to support a different default value in the future, we need to + // test that parsedValue != defaultValue. + if (defaultValue != -1) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + + if (!input.Peek(INTEGER)) { + value = defaultValue; + return Success; + } + + uint8_t parsedValue; + Result rv = Integer(input, parsedValue); + if (rv != Success) { + return rv; + } + value = parsedValue; + return Success; +} + +inline Result +Null(Reader& input) +{ + return ExpectTagAndEmptyValue(input, NULLTag); +} + +template +Result +OID(Reader& input, const uint8_t (&expectedOid)[Len]) +{ + Reader value; + Result rv = ExpectTagAndGetValue(input, OIDTag, value); + if (rv != Success) { + return rv; + } + if (!value.MatchRest(expectedOid)) { + return Result::ERROR_BAD_DER; + } + return Success; +} + +// PKI-specific types + +inline Result +CertificateSerialNumber(Reader& input, /*out*/ Input& value) +{ + // http://tools.ietf.org/html/rfc5280#section-4.1.2.2: + // + // * "The serial number MUST be a positive integer assigned by the CA to + // each certificate." + // * "Certificate users MUST be able to handle serialNumber values up to 20 + // octets. Conforming CAs MUST NOT use serialNumber values longer than 20 + // octets." + // * "Note: Non-conforming CAs may issue certificates with serial numbers + // that are negative or zero. Certificate users SHOULD be prepared to + // gracefully handle such certificates." + return internal::IntegralBytes( + input, INTEGER, internal::IntegralValueRestriction::NoRestriction, + value); +} + +// x.509 and OCSP both use this same version numbering scheme, though OCSP +// only supports v1. +enum class Version { v1 = 0, v2 = 1, v3 = 2, v4 = 3 }; + +// X.509 Certificate and OCSP ResponseData both use +// "[0] EXPLICIT Version DEFAULT v1". Although an explicit encoding of v1 is +// illegal, we support it because some real-world OCSP responses explicitly +// encode it. +Result OptionalVersion(Reader& input, /*out*/ Version& version); + +template +inline Result +OptionalExtensions(Reader& input, uint8_t tag, + ExtensionHandler extensionHandler) +{ + if (!input.Peek(tag)) { + return Success; + } + + return Nested(input, tag, [extensionHandler](Reader& tagged) { + // Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension + // + // TODO(bug 997994): According to the specification, there should never be + // an empty sequence of extensions but we've found OCSP responses that have + // that (see bug 991898). + return NestedOf(tagged, SEQUENCE, SEQUENCE, EmptyAllowed::Yes, + [extensionHandler](Reader& extension) -> Result { + // Extension ::= SEQUENCE { + // extnID OBJECT IDENTIFIER, + // critical BOOLEAN DEFAULT FALSE, + // extnValue OCTET STRING + // } + Reader extnID; + Result rv = ExpectTagAndGetValue(extension, OIDTag, extnID); + if (rv != Success) { + return rv; + } + bool critical; + rv = OptionalBoolean(extension, critical); + if (rv != Success) { + return rv; + } + Input extnValue; + rv = ExpectTagAndGetValue(extension, OCTET_STRING, extnValue); + if (rv != Success) { + return rv; + } + bool understood = false; + rv = extensionHandler(extnID, extnValue, critical, understood); + if (rv != Success) { + return rv; + } + if (critical && !understood) { + return Result::ERROR_UNKNOWN_CRITICAL_EXTENSION; + } + return Success; + }); + }); +} + +Result DigestAlgorithmIdentifier(Reader& input, + /*out*/ DigestAlgorithm& algorithm); + +enum class PublicKeyAlgorithm +{ + RSA_PKCS1, + ECDSA, +}; + +Result SignatureAlgorithmIdentifierValue( + Reader& input, + /*out*/ PublicKeyAlgorithm& publicKeyAlgorithm, + /*out*/ DigestAlgorithm& digestAlgorithm); + +struct SignedDataWithSignature final +{ +public: + Input data; + Input algorithm; + Input signature; + + void operator=(const SignedDataWithSignature&) = delete; +}; + +// Parses a SEQUENCE into tbs and then parses an AlgorithmIdentifier followed +// by a BIT STRING into signedData. This handles the commonality between +// parsing the signed/signature fields of certificates and OCSP responses. In +// the case of an OCSP response, the caller needs to parse the certs +// separately. +// +// Note that signatureAlgorithm is NOT parsed or validated. +// +// Certificate ::= SEQUENCE { +// tbsCertificate TBSCertificate, +// signatureAlgorithm AlgorithmIdentifier, +// signatureValue BIT STRING } +// +// BasicOCSPResponse ::= SEQUENCE { +// tbsResponseData ResponseData, +// signatureAlgorithm AlgorithmIdentifier, +// signature BIT STRING, +// certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL } +Result SignedData(Reader& input, /*out*/ Reader& tbs, + /*out*/ SignedDataWithSignature& signedDataWithSignature); + +} } } // namespace mozilla::pkix::der + +#endif // mozilla_pkix_pkixder_h diff --git a/security/pkix/lib/pkixnames.cpp b/security/pkix/lib/pkixnames.cpp new file mode 100644 index 000000000..ca9b8d7d3 --- /dev/null +++ b/security/pkix/lib/pkixnames.cpp @@ -0,0 +1,2050 @@ +/* -*- 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. + */ + +// This code implements RFC6125-ish name matching, RFC5280-ish name constraint +// checking, and related things. +// +// In this code, identifiers are classified as either "presented" or +// "reference" identifiers are defined in +// http://tools.ietf.org/html/rfc6125#section-1.8. A "presented identifier" is +// one in the subjectAltName of the certificate, or sometimes within a CN of +// the certificate's subject. The "reference identifier" is the one we are +// being asked to match the certificate against. When checking name +// constraints, the reference identifier is the entire encoded name constraint +// extension value. + +#include "pkixcheck.h" +#include "pkixutil.h" + +namespace mozilla { namespace pkix { + +namespace { + +// GeneralName ::= CHOICE { +// otherName [0] OtherName, +// rfc822Name [1] IA5String, +// dNSName [2] IA5String, +// x400Address [3] ORAddress, +// directoryName [4] Name, +// ediPartyName [5] EDIPartyName, +// uniformResourceIdentifier [6] IA5String, +// iPAddress [7] OCTET STRING, +// registeredID [8] OBJECT IDENTIFIER } +enum class GeneralNameType : uint8_t +{ + // Note that these values are NOT contiguous. Some values have the + // der::CONSTRUCTED bit set while others do not. + // (The der::CONSTRUCTED bit is for types where the value is a SEQUENCE.) + otherName = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 0, + rfc822Name = der::CONTEXT_SPECIFIC | 1, + dNSName = der::CONTEXT_SPECIFIC | 2, + x400Address = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 3, + directoryName = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 4, + ediPartyName = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 5, + uniformResourceIdentifier = der::CONTEXT_SPECIFIC | 6, + iPAddress = der::CONTEXT_SPECIFIC | 7, + registeredID = der::CONTEXT_SPECIFIC | 8, + // nameConstraints is a pseudo-GeneralName used to signify that a + // reference ID is actually the entire name constraint extension. + nameConstraints = 0xff +}; + +inline Result +ReadGeneralName(Reader& reader, + /*out*/ GeneralNameType& generalNameType, + /*out*/ Input& value) +{ + uint8_t tag; + Result rv = der::ReadTagAndGetValue(reader, tag, value); + if (rv != Success) { + return rv; + } + switch (tag) { + case static_cast(GeneralNameType::otherName): + generalNameType = GeneralNameType::otherName; + break; + case static_cast(GeneralNameType::rfc822Name): + generalNameType = GeneralNameType::rfc822Name; + break; + case static_cast(GeneralNameType::dNSName): + generalNameType = GeneralNameType::dNSName; + break; + case static_cast(GeneralNameType::x400Address): + generalNameType = GeneralNameType::x400Address; + break; + case static_cast(GeneralNameType::directoryName): + generalNameType = GeneralNameType::directoryName; + break; + case static_cast(GeneralNameType::ediPartyName): + generalNameType = GeneralNameType::ediPartyName; + break; + case static_cast(GeneralNameType::uniformResourceIdentifier): + generalNameType = GeneralNameType::uniformResourceIdentifier; + break; + case static_cast(GeneralNameType::iPAddress): + generalNameType = GeneralNameType::iPAddress; + break; + case static_cast(GeneralNameType::registeredID): + generalNameType = GeneralNameType::registeredID; + break; + default: + return Result::ERROR_BAD_DER; + } + return Success; +} + +enum class MatchResult +{ + NoNamesOfGivenType = 0, + Mismatch = 1, + Match = 2 +}; + +Result SearchNames(const Input* subjectAltName, Input subject, + GeneralNameType referenceIDType, + Input referenceID, + FallBackToSearchWithinSubject fallBackToCommonName, + /*out*/ MatchResult& match); +Result SearchWithinRDN(Reader& rdn, + GeneralNameType referenceIDType, + Input referenceID, + FallBackToSearchWithinSubject fallBackToEmailAddress, + FallBackToSearchWithinSubject fallBackToCommonName, + /*in/out*/ MatchResult& match); +Result MatchAVA(Input type, + uint8_t valueEncodingTag, + Input presentedID, + GeneralNameType referenceIDType, + Input referenceID, + FallBackToSearchWithinSubject fallBackToEmailAddress, + FallBackToSearchWithinSubject fallBackToCommonName, + /*in/out*/ MatchResult& match); +Result ReadAVA(Reader& rdn, + /*out*/ Input& type, + /*out*/ uint8_t& valueTag, + /*out*/ Input& value); +void MatchSubjectPresentedIDWithReferenceID(GeneralNameType presentedIDType, + Input presentedID, + GeneralNameType referenceIDType, + Input referenceID, + /*in/out*/ MatchResult& match); + +Result MatchPresentedIDWithReferenceID(GeneralNameType presentedIDType, + Input presentedID, + GeneralNameType referenceIDType, + Input referenceID, + /*in/out*/ MatchResult& matchResult); +Result CheckPresentedIDConformsToConstraints(GeneralNameType referenceIDType, + Input presentedID, + Input nameConstraints); + +uint8_t LocaleInsensitveToLower(uint8_t a); +bool StartsWithIDNALabel(Input id); + +enum class IDRole +{ + ReferenceID = 0, + PresentedID = 1, + NameConstraint = 2, +}; + +enum class AllowWildcards { No = 0, Yes = 1 }; + +// DNSName constraints implicitly allow subdomain matching when there is no +// leading dot ("foo.example.com" matches a constraint of "example.com"), but +// RFC822Name constraints only allow subdomain matching when there is a leading +// dot ("foo.example.com" does not match "example.com" but does match +// ".example.com"). +enum class AllowDotlessSubdomainMatches { No = 0, Yes = 1 }; + +bool IsValidDNSID(Input hostname, IDRole idRole, + AllowWildcards allowWildcards); + +Result MatchPresentedDNSIDWithReferenceDNSID( + Input presentedDNSID, + AllowWildcards allowWildcards, + AllowDotlessSubdomainMatches allowDotlessSubdomainMatches, + IDRole referenceDNSIDRole, + Input referenceDNSID, + /*out*/ bool& matches); + +Result MatchPresentedRFC822NameWithReferenceRFC822Name( + Input presentedRFC822Name, IDRole referenceRFC822NameRole, + Input referenceRFC822Name, /*out*/ bool& matches); + +} // namespace + +bool IsValidReferenceDNSID(Input hostname); +bool IsValidPresentedDNSID(Input hostname); +bool ParseIPv4Address(Input hostname, /*out*/ uint8_t (&out)[4]); +bool ParseIPv6Address(Input hostname, /*out*/ uint8_t (&out)[16]); + +// This is used by the pkixnames_tests.cpp tests. +Result +MatchPresentedDNSIDWithReferenceDNSID(Input presentedDNSID, + Input referenceDNSID, + /*out*/ bool& matches) +{ + return MatchPresentedDNSIDWithReferenceDNSID( + presentedDNSID, AllowWildcards::Yes, + AllowDotlessSubdomainMatches::Yes, IDRole::ReferenceID, + referenceDNSID, matches); +} + +// Verify that the given end-entity cert, which is assumed to have been already +// validated with BuildCertChain, is valid for the given hostname. hostname is +// assumed to be a string representation of an IPv4 address, an IPv6 addresss, +// or a normalized ASCII (possibly punycode) DNS name. +Result +CheckCertHostname(Input endEntityCertDER, Input hostname, + NameMatchingPolicy& nameMatchingPolicy) +{ + BackCert cert(endEntityCertDER, EndEntityOrCA::MustBeEndEntity, nullptr); + Result rv = cert.Init(); + if (rv != Success) { + return rv; + } + + Time notBefore(Time::uninitialized); + rv = ParseValidity(cert.GetValidity(), ¬Before); + if (rv != Success) { + return rv; + } + FallBackToSearchWithinSubject fallBackToSearchWithinSubject; + rv = nameMatchingPolicy.FallBackToCommonName(notBefore, + fallBackToSearchWithinSubject); + if (rv != Success) { + return rv; + } + + const Input* subjectAltName(cert.GetSubjectAltName()); + Input subject(cert.GetSubject()); + + // For backward compatibility with legacy certificates, we may fall back to + // searching for a name match in the subject common name for DNS names and + // IPv4 addresses. We don't do so for IPv6 addresses because we do not think + // there are many certificates that would need such fallback, and because + // comparisons of string representations of IPv6 addresses are particularly + // error prone due to the syntactic flexibility that IPv6 addresses have. + // + // IPv4 and IPv6 addresses are represented using the same type of GeneralName + // (iPAddress); they are differentiated by the lengths of the values. + MatchResult match; + uint8_t ipv6[16]; + uint8_t ipv4[4]; + if (IsValidReferenceDNSID(hostname)) { + rv = SearchNames(subjectAltName, subject, GeneralNameType::dNSName, + hostname, fallBackToSearchWithinSubject, match); + } else if (ParseIPv6Address(hostname, ipv6)) { + rv = SearchNames(subjectAltName, subject, GeneralNameType::iPAddress, + Input(ipv6), FallBackToSearchWithinSubject::No, match); + } else if (ParseIPv4Address(hostname, ipv4)) { + rv = SearchNames(subjectAltName, subject, GeneralNameType::iPAddress, + Input(ipv4), fallBackToSearchWithinSubject, match); + } else { + return Result::ERROR_BAD_CERT_DOMAIN; + } + if (rv != Success) { + return rv; + } + switch (match) { + case MatchResult::NoNamesOfGivenType: // fall through + case MatchResult::Mismatch: + return Result::ERROR_BAD_CERT_DOMAIN; + case MatchResult::Match: + return Success; + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } +} + +// 4.2.1.10. Name Constraints +Result +CheckNameConstraints(Input encodedNameConstraints, + const BackCert& firstChild, + KeyPurposeId requiredEKUIfPresent) +{ + for (const BackCert* child = &firstChild; child; child = child->childCert) { + FallBackToSearchWithinSubject fallBackToCommonName + = (child->endEntityOrCA == EndEntityOrCA::MustBeEndEntity && + requiredEKUIfPresent == KeyPurposeId::id_kp_serverAuth) + ? FallBackToSearchWithinSubject::Yes + : FallBackToSearchWithinSubject::No; + + MatchResult match; + Result rv = SearchNames(child->GetSubjectAltName(), child->GetSubject(), + GeneralNameType::nameConstraints, + encodedNameConstraints, fallBackToCommonName, + match); + if (rv != Success) { + return rv; + } + switch (match) { + case MatchResult::Match: // fall through + case MatchResult::NoNamesOfGivenType: + break; + case MatchResult::Mismatch: + return Result::ERROR_CERT_NOT_IN_NAME_SPACE; + } + } + + return Success; +} + +namespace { + +// SearchNames is used by CheckCertHostname and CheckNameConstraints. +// +// When called during name constraint checking, referenceIDType is +// GeneralNameType::nameConstraints and referenceID is the entire encoded name +// constraints extension value. +// +// The main benefit of using the exact same code paths for both is that we +// ensure consistency between name validation and name constraint enforcement +// regarding thing like "Which CN attributes should be considered as potential +// CN-IDs" and "Which character sets are acceptable for CN-IDs?" If the name +// matching and the name constraint enforcement logic were out of sync on these +// issues (e.g. if name matching were to consider all subject CN attributes, +// but name constraints were only enforced on the most specific subject CN), +// trivial name constraint bypasses could result. + +Result +SearchNames(/*optional*/ const Input* subjectAltName, + Input subject, + GeneralNameType referenceIDType, + Input referenceID, + FallBackToSearchWithinSubject fallBackToCommonName, + /*out*/ MatchResult& match) +{ + Result rv; + + match = MatchResult::NoNamesOfGivenType; + + // RFC 6125 says "A client MUST NOT seek a match for a reference identifier + // of CN-ID if the presented identifiers include a DNS-ID, SRV-ID, URI-ID, or + // any application-specific identifier types supported by the client." + // Accordingly, we only consider CN-IDs if there are no DNS-IDs in the + // subjectAltName. + // + // RFC 6125 says that IP addresses are out of scope, but for backward + // compatibility we accept them, by considering IP addresses to be an + // "application-specific identifier type supported by the client." + // + // TODO(bug XXXXXXX): Consider strengthening this check to "A client MUST NOT + // seek a match for a reference identifier of CN-ID if the certificate + // contains a subjectAltName extension." + // + // TODO(bug XXXXXXX): Consider dropping support for IP addresses as + // identifiers completely. + + if (subjectAltName) { + Reader altNames; + rv = der::ExpectTagAndGetValueAtEnd(*subjectAltName, der::SEQUENCE, + altNames); + if (rv != Success) { + return rv; + } + + // According to RFC 5280, "If the subjectAltName extension is present, the + // sequence MUST contain at least one entry." For compatibility reasons, we + // do not enforce this. See bug 1143085. + while (!altNames.AtEnd()) { + GeneralNameType presentedIDType; + Input presentedID; + rv = ReadGeneralName(altNames, presentedIDType, presentedID); + if (rv != Success) { + return rv; + } + + rv = MatchPresentedIDWithReferenceID(presentedIDType, presentedID, + referenceIDType, referenceID, + match); + if (rv != Success) { + return rv; + } + if (referenceIDType != GeneralNameType::nameConstraints && + match == MatchResult::Match) { + return Success; + } + if (presentedIDType == GeneralNameType::dNSName || + presentedIDType == GeneralNameType::iPAddress) { + fallBackToCommonName = FallBackToSearchWithinSubject::No; + } + } + } + + if (referenceIDType == GeneralNameType::nameConstraints) { + rv = CheckPresentedIDConformsToConstraints(GeneralNameType::directoryName, + subject, referenceID); + if (rv != Success) { + return rv; + } + } + + FallBackToSearchWithinSubject fallBackToEmailAddress; + if (!subjectAltName && + (referenceIDType == GeneralNameType::rfc822Name || + referenceIDType == GeneralNameType::nameConstraints)) { + fallBackToEmailAddress = FallBackToSearchWithinSubject::Yes; + } else { + fallBackToEmailAddress = FallBackToSearchWithinSubject::No; + } + + // Short-circuit the parsing of the subject name if we're not going to match + // any names in it + if (fallBackToEmailAddress == FallBackToSearchWithinSubject::No && + fallBackToCommonName == FallBackToSearchWithinSubject::No) { + return Success; + } + + // Attempt to match the reference ID against the CN-ID, which we consider to + // be the most-specific CN AVA in the subject field. + // + // https://tools.ietf.org/html/rfc6125#section-2.3.1 says: + // + // To reduce confusion, in this specification we avoid such terms and + // instead use the terms provided under Section 1.8; in particular, we + // do not use the term "(most specific) Common Name field in the subject + // field" from [HTTP-TLS] and instead state that a CN-ID is a Relative + // Distinguished Name (RDN) in the certificate subject containing one + // and only one attribute-type-and-value pair of type Common Name (thus + // removing the possibility that an RDN might contain multiple AVAs + // (Attribute Value Assertions) of type CN, one of which could be + // considered "most specific"). + // + // https://tools.ietf.org/html/rfc6125#section-7.4 says: + // + // [...] Although it would be preferable to + // forbid multiple CN-IDs entirely, there are several reasons at this + // time why this specification states that they SHOULD NOT (instead of + // MUST NOT) be included [...] + // + // Consequently, it is unclear what to do when there are multiple CNs in the + // subject, regardless of whether there "SHOULD NOT" be. + // + // NSS's CERT_VerifyCertName mostly follows RFC2818 in this instance, which + // says: + // + // If a subjectAltName extension of type dNSName is present, that MUST + // be used as the identity. Otherwise, the (most specific) Common Name + // field in the Subject field of the certificate MUST be used. + // + // [...] + // + // In some cases, the URI is specified as an IP address rather than a + // hostname. In this case, the iPAddress subjectAltName must be present + // in the certificate and must exactly match the IP in the URI. + // + // (The main difference from RFC2818 is that NSS's CERT_VerifyCertName also + // matches IP addresses in the most-specific CN.) + // + // NSS's CERT_VerifyCertName finds the most specific CN via + // CERT_GetCommoName, which uses CERT_GetLastNameElement. Note that many + // NSS-based applications, including Gecko, also use CERT_GetCommonName. It + // is likely that other, non-NSS-based, applications also expect only the + // most specific CN to be matched against the reference ID. + // + // "A Layman's Guide to a Subset of ASN.1, BER, and DER" and other sources + // agree that an RDNSequence is ordered from most significant (least + // specific) to least significant (most specific), as do other references. + // + // However, Chromium appears to use the least-specific (first) CN instead of + // the most-specific; see https://crbug.com/366957. Also, MSIE and some other + // popular implementations apparently attempt to match the reference ID + // against any/all CNs in the subject. Since we're trying to phase out the + // use of CN-IDs, we intentionally avoid trying to match MSIE's more liberal + // behavior. + + // Name ::= CHOICE { -- only one possibility for now -- + // rdnSequence RDNSequence } + // + // RDNSequence ::= SEQUENCE OF RelativeDistinguishedName + // + // RelativeDistinguishedName ::= + // SET SIZE (1..MAX) OF AttributeTypeAndValue + Reader subjectReader(subject); + return der::NestedOf(subjectReader, der::SEQUENCE, der::SET, + der::EmptyAllowed::Yes, [&](Reader& r) { + return SearchWithinRDN(r, referenceIDType, referenceID, + fallBackToEmailAddress, fallBackToCommonName, match); + }); +} + +// RelativeDistinguishedName ::= +// SET SIZE (1..MAX) OF AttributeTypeAndValue +// +// AttributeTypeAndValue ::= SEQUENCE { +// type AttributeType, +// value AttributeValue } +Result +SearchWithinRDN(Reader& rdn, + GeneralNameType referenceIDType, + Input referenceID, + FallBackToSearchWithinSubject fallBackToEmailAddress, + FallBackToSearchWithinSubject fallBackToCommonName, + /*in/out*/ MatchResult& match) +{ + do { + Input type; + uint8_t valueTag; + Input value; + Result rv = ReadAVA(rdn, type, valueTag, value); + if (rv != Success) { + return rv; + } + rv = MatchAVA(type, valueTag, value, referenceIDType, referenceID, + fallBackToEmailAddress, fallBackToCommonName, match); + if (rv != Success) { + return rv; + } + } while (!rdn.AtEnd()); + + return Success; +} + +// AttributeTypeAndValue ::= SEQUENCE { +// type AttributeType, +// value AttributeValue } +// +// AttributeType ::= OBJECT IDENTIFIER +// +// AttributeValue ::= ANY -- DEFINED BY AttributeType +// +// DirectoryString ::= CHOICE { +// teletexString TeletexString (SIZE (1..MAX)), +// printableString PrintableString (SIZE (1..MAX)), +// universalString UniversalString (SIZE (1..MAX)), +// utf8String UTF8String (SIZE (1..MAX)), +// bmpString BMPString (SIZE (1..MAX)) } +Result +MatchAVA(Input type, uint8_t valueEncodingTag, Input presentedID, + GeneralNameType referenceIDType, + Input referenceID, + FallBackToSearchWithinSubject fallBackToEmailAddress, + FallBackToSearchWithinSubject fallBackToCommonName, + /*in/out*/ MatchResult& match) +{ + // Try to match the CN as a DNSName or an IPAddress. + // + // id-at-commonName AttributeType ::= { id-at 3 } + // + // -- Naming attributes of type X520CommonName: + // -- X520CommonName ::= DirectoryName (SIZE (1..ub-common-name)) + // -- + // -- Expanded to avoid parameterized type: + // X520CommonName ::= CHOICE { + // teletexString TeletexString (SIZE (1..ub-common-name)), + // printableString PrintableString (SIZE (1..ub-common-name)), + // universalString UniversalString (SIZE (1..ub-common-name)), + // utf8String UTF8String (SIZE (1..ub-common-name)), + // bmpString BMPString (SIZE (1..ub-common-name)) } + // + // python DottedOIDToCode.py id-at-commonName 2.5.4.3 + static const uint8_t id_at_commonName[] = { + 0x55, 0x04, 0x03 + }; + if (fallBackToCommonName == FallBackToSearchWithinSubject::Yes && + InputsAreEqual(type, Input(id_at_commonName))) { + // We might have previously found a match. Now that we've found another CN, + // we no longer consider that previous match to be a match, so "forget" about + // it. + match = MatchResult::NoNamesOfGivenType; + + // PrintableString is a subset of ASCII that contains all the characters + // allowed in CN-IDs except '*'. Although '*' is illegal, there are many + // real-world certificates that are encoded this way, so we accept it. + // + // In the case of UTF8String, we rely on the fact that in UTF-8 the octets in + // a multi-byte encoding of a code point are always distinct from ASCII. Any + // non-ASCII byte in a UTF-8 string causes us to fail to match. We make no + // attempt to detect or report malformed UTF-8 (e.g. incomplete or overlong + // encodings of code points, or encodings of invalid code points). + // + // TeletexString is supported as long as it does not contain any escape + // sequences, which are not supported. We'll reject escape sequences as + // invalid characters in names, which means we only accept strings that are + // in the default character set, which is a superset of ASCII. Note that NSS + // actually treats TeletexString as ISO-8859-1. Many certificates that have + // wildcard CN-IDs (e.g. "*.example.com") use TeletexString because + // PrintableString is defined to not allow '*' and because, at one point in + // history, UTF8String was too new to use for compatibility reasons. + // + // UniversalString and BMPString are also deprecated, and they are a little + // harder to support because they are not single-byte ASCII superset + // encodings, so we don't bother. + if (valueEncodingTag != der::PrintableString && + valueEncodingTag != der::UTF8String && + valueEncodingTag != der::TeletexString) { + return Success; + } + + if (IsValidPresentedDNSID(presentedID)) { + MatchSubjectPresentedIDWithReferenceID(GeneralNameType::dNSName, + presentedID, referenceIDType, + referenceID, match); + } else { + // We don't match CN-IDs for IPv6 addresses. + // MatchSubjectPresentedIDWithReferenceID ensures that it won't match an + // IPv4 address with an IPv6 address, so we don't need to check that + // referenceID is an IPv4 address here. + uint8_t ipv4[4]; + if (ParseIPv4Address(presentedID, ipv4)) { + MatchSubjectPresentedIDWithReferenceID(GeneralNameType::iPAddress, + Input(ipv4), referenceIDType, + referenceID, match); + } + } + + // Regardless of whether there was a match, we keep going in case we find + // another CN later. If we do find another one, then this match/mismatch + // will be ignored, because we only care about the most specific CN. + + return Success; + } + + // Match an email address against an emailAddress attribute in the + // subject. + // + // id-emailAddress AttributeType ::= { pkcs-9 1 } + // + // EmailAddress ::= IA5String (SIZE (1..ub-emailaddress-length)) + // + // python DottedOIDToCode.py id-emailAddress 1.2.840.113549.1.9.1 + static const uint8_t id_emailAddress[] = { + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01 + }; + if (fallBackToEmailAddress == FallBackToSearchWithinSubject::Yes && + InputsAreEqual(type, Input(id_emailAddress))) { + if (referenceIDType == GeneralNameType::rfc822Name && + match == MatchResult::Match) { + // We already found a match; we don't need to match another one + return Success; + } + if (valueEncodingTag != der::IA5String) { + return Result::ERROR_BAD_DER; + } + return MatchPresentedIDWithReferenceID(GeneralNameType::rfc822Name, + presentedID, referenceIDType, + referenceID, match); + } + + return Success; +} + +void +MatchSubjectPresentedIDWithReferenceID(GeneralNameType presentedIDType, + Input presentedID, + GeneralNameType referenceIDType, + Input referenceID, + /*in/out*/ MatchResult& match) +{ + Result rv = MatchPresentedIDWithReferenceID(presentedIDType, presentedID, + referenceIDType, referenceID, + match); + if (rv != Success) { + match = MatchResult::Mismatch; + } +} + +Result +MatchPresentedIDWithReferenceID(GeneralNameType presentedIDType, + Input presentedID, + GeneralNameType referenceIDType, + Input referenceID, + /*out*/ MatchResult& matchResult) +{ + if (referenceIDType == GeneralNameType::nameConstraints) { + // matchResult is irrelevant when checking name constraints; only the + // pass/fail result of CheckPresentedIDConformsToConstraints matters. + return CheckPresentedIDConformsToConstraints(presentedIDType, presentedID, + referenceID); + } + + if (presentedIDType != referenceIDType) { + matchResult = MatchResult::Mismatch; + return Success; + } + + Result rv; + bool foundMatch; + + switch (referenceIDType) { + case GeneralNameType::dNSName: + rv = MatchPresentedDNSIDWithReferenceDNSID( + presentedID, AllowWildcards::Yes, + AllowDotlessSubdomainMatches::Yes, IDRole::ReferenceID, + referenceID, foundMatch); + break; + + case GeneralNameType::iPAddress: + foundMatch = InputsAreEqual(presentedID, referenceID); + rv = Success; + break; + + case GeneralNameType::rfc822Name: + rv = MatchPresentedRFC822NameWithReferenceRFC822Name( + presentedID, IDRole::ReferenceID, referenceID, foundMatch); + break; + + case GeneralNameType::directoryName: + // TODO: At some point, we may add APIs for matching DirectoryNames. + // fall through + + case GeneralNameType::otherName: // fall through + case GeneralNameType::x400Address: // fall through + case GeneralNameType::ediPartyName: // fall through + case GeneralNameType::uniformResourceIdentifier: // fall through + case GeneralNameType::registeredID: // fall through + case GeneralNameType::nameConstraints: + return NotReached("unexpected nameType for SearchType::Match", + Result::FATAL_ERROR_INVALID_ARGS); + + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } + + if (rv != Success) { + return rv; + } + matchResult = foundMatch ? MatchResult::Match : MatchResult::Mismatch; + return Success; +} + +enum class NameConstraintsSubtrees : uint8_t +{ + permittedSubtrees = der::CONSTRUCTED | der::CONTEXT_SPECIFIC | 0, + excludedSubtrees = der::CONSTRUCTED | der::CONTEXT_SPECIFIC | 1 +}; + +Result CheckPresentedIDConformsToNameConstraintsSubtrees( + GeneralNameType presentedIDType, + Input presentedID, + Reader& nameConstraints, + NameConstraintsSubtrees subtreesType); +Result MatchPresentedIPAddressWithConstraint(Input presentedID, + Input iPAddressConstraint, + /*out*/ bool& foundMatch); +Result MatchPresentedDirectoryNameWithConstraint( + NameConstraintsSubtrees subtreesType, Input presentedID, + Input directoryNameConstraint, /*out*/ bool& matches); + +Result +CheckPresentedIDConformsToConstraints( + GeneralNameType presentedIDType, + Input presentedID, + Input encodedNameConstraints) +{ + // NameConstraints ::= SEQUENCE { + // permittedSubtrees [0] GeneralSubtrees OPTIONAL, + // excludedSubtrees [1] GeneralSubtrees OPTIONAL } + Reader nameConstraints; + Result rv = der::ExpectTagAndGetValueAtEnd(encodedNameConstraints, + der::SEQUENCE, nameConstraints); + if (rv != Success) { + return rv; + } + + // RFC 5280 says "Conforming CAs MUST NOT issue certificates where name + // constraints is an empty sequence. That is, either the permittedSubtrees + // field or the excludedSubtrees MUST be present." + if (nameConstraints.AtEnd()) { + return Result::ERROR_BAD_DER; + } + + rv = CheckPresentedIDConformsToNameConstraintsSubtrees( + presentedIDType, presentedID, nameConstraints, + NameConstraintsSubtrees::permittedSubtrees); + if (rv != Success) { + return rv; + } + + rv = CheckPresentedIDConformsToNameConstraintsSubtrees( + presentedIDType, presentedID, nameConstraints, + NameConstraintsSubtrees::excludedSubtrees); + if (rv != Success) { + return rv; + } + + return der::End(nameConstraints); +} + +Result +CheckPresentedIDConformsToNameConstraintsSubtrees( + GeneralNameType presentedIDType, + Input presentedID, + Reader& nameConstraints, + NameConstraintsSubtrees subtreesType) +{ + if (!nameConstraints.Peek(static_cast(subtreesType))) { + return Success; + } + + Reader subtrees; + Result rv = der::ExpectTagAndGetValue(nameConstraints, + static_cast(subtreesType), + subtrees); + if (rv != Success) { + return rv; + } + + bool hasPermittedSubtreesMatch = false; + bool hasPermittedSubtreesMismatch = false; + + // GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree + // + // do { ... } while(...) because subtrees isn't allowed to be empty. + do { + // GeneralSubtree ::= SEQUENCE { + // base GeneralName, + // minimum [0] BaseDistance DEFAULT 0, + // maximum [1] BaseDistance OPTIONAL } + Reader subtree; + rv = ExpectTagAndGetValue(subtrees, der::SEQUENCE, subtree); + if (rv != Success) { + return rv; + } + GeneralNameType nameConstraintType; + Input base; + rv = ReadGeneralName(subtree, nameConstraintType, base); + if (rv != Success) { + return rv; + } + // http://tools.ietf.org/html/rfc5280#section-4.2.1.10: "Within this + // profile, the minimum and maximum fields are not used with any name + // forms, thus, the minimum MUST be zero, and maximum MUST be absent." + // + // Since the default value isn't allowed to be encoded according to the DER + // encoding rules for DEFAULT, this is equivalent to saying that neither + // minimum or maximum must be encoded. + rv = der::End(subtree); + if (rv != Success) { + return rv; + } + + if (presentedIDType == nameConstraintType) { + bool matches; + + switch (presentedIDType) { + case GeneralNameType::dNSName: + rv = MatchPresentedDNSIDWithReferenceDNSID( + presentedID, AllowWildcards::Yes, + AllowDotlessSubdomainMatches::Yes, IDRole::NameConstraint, + base, matches); + if (rv != Success) { + return rv; + } + break; + + case GeneralNameType::iPAddress: + rv = MatchPresentedIPAddressWithConstraint(presentedID, base, + matches); + if (rv != Success) { + return rv; + } + break; + + case GeneralNameType::directoryName: + rv = MatchPresentedDirectoryNameWithConstraint(subtreesType, + presentedID, base, + matches); + if (rv != Success) { + return rv; + } + break; + + case GeneralNameType::rfc822Name: + rv = MatchPresentedRFC822NameWithReferenceRFC822Name( + presentedID, IDRole::NameConstraint, base, matches); + if (rv != Success) { + return rv; + } + break; + + // RFC 5280 says "Conforming CAs [...] SHOULD NOT impose name + // constraints on the x400Address, ediPartyName, or registeredID + // name forms. It also says "Applications conforming to this profile + // [...] SHOULD be able to process name constraints that are imposed + // on [...] uniformResourceIdentifier [...]", but we don't bother. + // + // TODO: Ask to have spec updated to say ""Conforming CAs [...] SHOULD + // NOT impose name constraints on the otherName, x400Address, + // ediPartyName, uniformResourceIdentifier, or registeredID name + // forms." + case GeneralNameType::otherName: // fall through + case GeneralNameType::x400Address: // fall through + case GeneralNameType::ediPartyName: // fall through + case GeneralNameType::uniformResourceIdentifier: // fall through + case GeneralNameType::registeredID: // fall through + return Result::ERROR_CERT_NOT_IN_NAME_SPACE; + + case GeneralNameType::nameConstraints: + return NotReached("invalid presentedIDType", + Result::FATAL_ERROR_LIBRARY_FAILURE); + + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } + + switch (subtreesType) { + case NameConstraintsSubtrees::permittedSubtrees: + if (matches) { + hasPermittedSubtreesMatch = true; + } else { + hasPermittedSubtreesMismatch = true; + } + break; + case NameConstraintsSubtrees::excludedSubtrees: + if (matches) { + return Result::ERROR_CERT_NOT_IN_NAME_SPACE; + } + break; + } + } + } while (!subtrees.AtEnd()); + + if (hasPermittedSubtreesMismatch && !hasPermittedSubtreesMatch) { + // If there was any entry of the given type in permittedSubtrees, then it + // required that at least one of them must match. Since none of them did, + // we have a failure. + return Result::ERROR_CERT_NOT_IN_NAME_SPACE; + } + + return Success; +} + +// We do not distinguish between a syntactically-invalid presentedDNSID and one +// that is syntactically valid but does not match referenceDNSID; in both +// cases, the result is false. +// +// We assume that both presentedDNSID and referenceDNSID are encoded in such a +// way that US-ASCII (7-bit) characters are encoded in one byte and no encoding +// of a non-US-ASCII character contains a code point in the range 0-127. For +// example, UTF-8 is OK but UTF-16 is not. +// +// RFC6125 says that a wildcard label may be of the form *., where +// and/or may be empty. However, NSS requires to be empty, and we +// follow NSS's stricter policy by accepting wildcards only of the form +// *., where may be empty. +// +// An relative presented DNS ID matches both an absolute reference ID and a +// relative reference ID. Absolute presented DNS IDs are not supported: +// +// Presented ID Reference ID Result +// ------------------------------------- +// example.com example.com Match +// example.com. example.com Mismatch +// example.com example.com. Match +// example.com. example.com. Mismatch +// +// There are more subtleties documented inline in the code. +// +// Name constraints /////////////////////////////////////////////////////////// +// +// This is all RFC 5280 has to say about DNSName constraints: +// +// DNS name restrictions are expressed as host.example.com. Any DNS +// name that can be constructed by simply adding zero or more labels to +// the left-hand side of the name satisfies the name constraint. For +// example, www.host.example.com would satisfy the constraint but +// host1.example.com would not. +// +// This lack of specificity has lead to a lot of uncertainty regarding +// subdomain matching. In particular, the following questions have been +// raised and answered: +// +// Q: Does a presented identifier equal (case insensitive) to the name +// constraint match the constraint? For example, does the presented +// ID "host.example.com" match a "host.example.com" constraint? +// A: Yes. RFC5280 says "by simply adding zero or more labels" and this +// is the case of adding zero labels. +// +// Q: When the name constraint does not start with ".", do subdomain +// presented identifiers match it? For example, does the presented +// ID "www.host.example.com" match a "host.example.com" constraint? +// A: Yes. RFC5280 says "by simply adding zero or more labels" and this +// is the case of adding more than zero labels. The example is the +// one from RFC 5280. +// +// Q: When the name constraint does not start with ".", does a +// non-subdomain prefix match it? For example, does "bigfoo.bar.com" +// match "foo.bar.com"? [4] +// A: No. We interpret RFC 5280's language of "adding zero or more labels" +// to mean that whole labels must be prefixed. +// +// (Note that the above three scenarios are the same as the RFC 6265 +// domain matching rules [0].) +// +// Q: Is a name constraint that starts with "." valid, and if so, what +// semantics does it have? For example, does a presented ID of +// "www.example.com" match a constraint of ".example.com"? Does a +// presented ID of "example.com" match a constraint of ".example.com"? +// A: This implementation, NSS[1], and SChannel[2] all support a +// leading ".", but OpenSSL[3] does not yet. Amongst the +// implementations that support it, a leading "." is legal and means +// the same thing as when the "." is omitted, EXCEPT that a +// presented identifier equal (case insensitive) to the name +// constraint is not matched; i.e. presented DNSName identifiers +// must be subdomains. Some CAs in Mozilla's CA program (e.g. HARICA) +// have name constraints with the leading "." in their root +// certificates. The name constraints imposed on DCISS by Mozilla also +// have the it, so supporting this is a requirement for backward +// compatibility, even if it is not yet standardized. So, for example, a +// presented ID of "www.example.com" matches a constraint of +// ".example.com" but a presented ID of "example.com" does not. +// +// Q: Is there a way to prevent subdomain matches? +// A: Yes. +// +// Some people have proposed that dNSName constraints that do not +// start with a "." should be restricted to exact (case insensitive) +// matches. However, such a change of semantics from what RFC5280 +// specifies would be a non-backward-compatible change in the case of +// permittedSubtrees constraints, and it would be a security issue for +// excludedSubtrees constraints. +// +// However, it can be done with a combination of permittedSubtrees and +// excludedSubtrees, e.g. "example.com" in permittedSubtrees and +// ".example.com" in excudedSubtrees. +// +// Q: Are name constraints allowed to be specified as absolute names? +// For example, does a presented ID of "example.com" match a name +// constraint of "example.com." and vice versa. +// A: Absolute names are not supported as presented IDs or name +// constraints. Only reference IDs may be absolute. +// +// Q: Is "" a valid DNSName constraints? If so, what does it mean? +// A: Yes. Any valid presented DNSName can be formed "by simply adding zero +// or more labels to the left-hand side" of "". In particular, an +// excludedSubtrees DNSName constraint of "" forbids all DNSNames. +// +// Q: Is "." a valid DNSName constraints? If so, what does it mean? +// A: No, because absolute names are not allowed (see above). +// +// [0] RFC 6265 (Cookies) Domain Matching rules: +// http://tools.ietf.org/html/rfc6265#section-5.1.3 +// [1] NSS source code: +// https://mxr.mozilla.org/nss/source/lib/certdb/genname.c?rev=2a7348f013cb#1209 +// [2] Description of SChannel's behavior from Microsoft: +// http://www.imc.org/ietf-pkix/mail-archive/msg04668.html +// [3] Proposal to add such support to OpenSSL: +// http://www.mail-archive.com/openssl-dev%40openssl.org/msg36204.html +// https://rt.openssl.org/Ticket/Display.html?id=3562 +// [4] Feedback on the lack of clarify in the definition that never got +// incorporated into the spec: +// https://www.ietf.org/mail-archive/web/pkix/current/msg21192.html +Result +MatchPresentedDNSIDWithReferenceDNSID( + Input presentedDNSID, + AllowWildcards allowWildcards, + AllowDotlessSubdomainMatches allowDotlessSubdomainMatches, + IDRole referenceDNSIDRole, + Input referenceDNSID, + /*out*/ bool& matches) +{ + if (!IsValidDNSID(presentedDNSID, IDRole::PresentedID, allowWildcards)) { + return Result::ERROR_BAD_DER; + } + + if (!IsValidDNSID(referenceDNSID, referenceDNSIDRole, AllowWildcards::No)) { + return Result::ERROR_BAD_DER; + } + + Reader presented(presentedDNSID); + Reader reference(referenceDNSID); + + switch (referenceDNSIDRole) + { + case IDRole::ReferenceID: + break; + + case IDRole::NameConstraint: + { + if (presentedDNSID.GetLength() > referenceDNSID.GetLength()) { + if (referenceDNSID.GetLength() == 0) { + // An empty constraint matches everything. + matches = true; + return Success; + } + // If the reference ID starts with a dot then skip the prefix of + // of the presented ID and start the comparison at the position of that + // dot. Examples: + // + // Matches Doesn't Match + // ----------------------------------------------------------- + // original presented ID: www.example.com badexample.com + // skipped: www ba + // presented ID w/o prefix: .example.com dexample.com + // reference ID: .example.com .example.com + // + // If the reference ID does not start with a dot then we skip the + // prefix of the presented ID but also verify that the prefix ends with + // a dot. Examples: + // + // Matches Doesn't Match + // ----------------------------------------------------------- + // original presented ID: www.example.com badexample.com + // skipped: www ba + // must be '.': . d + // presented ID w/o prefix: example.com example.com + // reference ID: example.com example.com + // + if (reference.Peek('.')) { + if (presented.Skip(static_cast( + presentedDNSID.GetLength() - + referenceDNSID.GetLength())) != Success) { + return NotReached("skipping subdomain failed", + Result::FATAL_ERROR_LIBRARY_FAILURE); + } + } else if (allowDotlessSubdomainMatches == + AllowDotlessSubdomainMatches::Yes) { + if (presented.Skip(static_cast( + presentedDNSID.GetLength() - + referenceDNSID.GetLength() - 1)) != Success) { + return NotReached("skipping subdomains failed", + Result::FATAL_ERROR_LIBRARY_FAILURE); + } + uint8_t b; + if (presented.Read(b) != Success) { + return NotReached("reading from presentedDNSID failed", + Result::FATAL_ERROR_LIBRARY_FAILURE); + } + if (b != '.') { + matches = false; + return Success; + } + } + } + break; + } + + case IDRole::PresentedID: // fall through + return NotReached("IDRole::PresentedID is not a valid referenceDNSIDRole", + Result::FATAL_ERROR_INVALID_ARGS); + } + + // We only allow wildcard labels that consist only of '*'. + if (presented.Peek('*')) { + if (presented.Skip(1) != Success) { + return NotReached("Skipping '*' failed", + Result::FATAL_ERROR_LIBRARY_FAILURE); + } + do { + // This will happen if reference is a single, relative label + if (reference.AtEnd()) { + matches = false; + return Success; + } + uint8_t referenceByte; + if (reference.Read(referenceByte) != Success) { + return NotReached("invalid reference ID", + Result::FATAL_ERROR_INVALID_ARGS); + } + } while (!reference.Peek('.')); + } + + for (;;) { + uint8_t presentedByte; + if (presented.Read(presentedByte) != Success) { + matches = false; + return Success; + } + uint8_t referenceByte; + if (reference.Read(referenceByte) != Success) { + matches = false; + return Success; + } + if (LocaleInsensitveToLower(presentedByte) != + LocaleInsensitveToLower(referenceByte)) { + matches = false; + return Success; + } + if (presented.AtEnd()) { + // Don't allow presented IDs to be absolute. + if (presentedByte == '.') { + return Result::ERROR_BAD_DER; + } + break; + } + } + + // Allow a relative presented DNS ID to match an absolute reference DNS ID, + // unless we're matching a name constraint. + if (!reference.AtEnd()) { + if (referenceDNSIDRole != IDRole::NameConstraint) { + uint8_t referenceByte; + if (reference.Read(referenceByte) != Success) { + return NotReached("read failed but not at end", + Result::FATAL_ERROR_LIBRARY_FAILURE); + } + if (referenceByte != '.') { + matches = false; + return Success; + } + } + if (!reference.AtEnd()) { + matches = false; + return Success; + } + } + + matches = true; + return Success; +} + +// https://tools.ietf.org/html/rfc5280#section-4.2.1.10 says: +// +// For IPv4 addresses, the iPAddress field of GeneralName MUST contain +// eight (8) octets, encoded in the style of RFC 4632 (CIDR) to represent +// an address range [RFC4632]. For IPv6 addresses, the iPAddress field +// MUST contain 32 octets similarly encoded. For example, a name +// constraint for "class C" subnet 192.0.2.0 is represented as the +// octets C0 00 02 00 FF FF FF 00, representing the CIDR notation +// 192.0.2.0/24 (mask 255.255.255.0). +Result +MatchPresentedIPAddressWithConstraint(Input presentedID, + Input iPAddressConstraint, + /*out*/ bool& foundMatch) +{ + if (presentedID.GetLength() != 4 && presentedID.GetLength() != 16) { + return Result::ERROR_BAD_DER; + } + if (iPAddressConstraint.GetLength() != 8 && + iPAddressConstraint.GetLength() != 32) { + return Result::ERROR_BAD_DER; + } + + // an IPv4 address never matches an IPv6 constraint, and vice versa. + if (presentedID.GetLength() * 2 != iPAddressConstraint.GetLength()) { + foundMatch = false; + return Success; + } + + Reader constraint(iPAddressConstraint); + Reader constraintAddress; + Result rv = constraint.Skip(iPAddressConstraint.GetLength() / 2u, + constraintAddress); + if (rv != Success) { + return rv; + } + Reader constraintMask; + rv = constraint.Skip(iPAddressConstraint.GetLength() / 2u, constraintMask); + if (rv != Success) { + return rv; + } + rv = der::End(constraint); + if (rv != Success) { + return rv; + } + + Reader presented(presentedID); + do { + uint8_t presentedByte; + rv = presented.Read(presentedByte); + if (rv != Success) { + return rv; + } + uint8_t constraintAddressByte; + rv = constraintAddress.Read(constraintAddressByte); + if (rv != Success) { + return rv; + } + uint8_t constraintMaskByte; + rv = constraintMask.Read(constraintMaskByte); + if (rv != Success) { + return rv; + } + foundMatch = + ((presentedByte ^ constraintAddressByte) & constraintMaskByte) == 0; + } while (foundMatch && !presented.AtEnd()); + + return Success; +} + +// AttributeTypeAndValue ::= SEQUENCE { +// type AttributeType, +// value AttributeValue } +// +// AttributeType ::= OBJECT IDENTIFIER +// +// AttributeValue ::= ANY -- DEFINED BY AttributeType +Result +ReadAVA(Reader& rdn, + /*out*/ Input& type, + /*out*/ uint8_t& valueTag, + /*out*/ Input& value) +{ + return der::Nested(rdn, der::SEQUENCE, [&](Reader& ava) -> Result { + Result rv = der::ExpectTagAndGetValue(ava, der::OIDTag, type); + if (rv != Success) { + return rv; + } + rv = der::ReadTagAndGetValue(ava, valueTag, value); + if (rv != Success) { + return rv; + } + return Success; + }); +} + +// Names are sequences of RDNs. RDNS are sets of AVAs. That means that RDNs are +// unordered, so in theory we should match RDNs with equivalent AVAs that are +// in different orders. Within the AVAs are DirectoryNames that are supposed to +// be compared according to LDAP stringprep normalization rules (e.g. +// normalizing whitespace), consideration of different character encodings, +// etc. Indeed, RFC 5280 says we MUST deal with all of that. +// +// In practice, many implementations, including NSS, only match Names in a way +// that only meets a subset of the requirements of RFC 5280. Those +// normalization and character encoding conversion steps appear to be +// unnecessary for processing real-world certificates, based on experience from +// having used NSS in Firefox for many years. +// +// RFC 5280 also says "CAs issuing certificates with a restriction of the form +// directoryName SHOULD NOT rely on implementation of the full +// ISO DN name comparison algorithm. This implies name restrictions MUST +// be stated identically to the encoding used in the subject field or +// subjectAltName extension." It goes on to say, in the security +// considerations: +// +// In addition, name constraints for distinguished names MUST be stated +// identically to the encoding used in the subject field or +// subjectAltName extension. If not, then name constraints stated as +// excludedSubtrees will not match and invalid paths will be accepted +// and name constraints expressed as permittedSubtrees will not match +// and valid paths will be rejected. To avoid acceptance of invalid +// paths, CAs SHOULD state name constraints for distinguished names as +// permittedSubtrees wherever possible. +// +// For permittedSubtrees, the MUST-level requirement is relaxed for +// compatibility in the case of PrintableString and UTF8String. That is, if a +// name constraint has been encoded using UTF8String and the presented ID has +// been encoded with a PrintableString (or vice-versa), they are considered to +// match if they are equal everywhere except for the tag identifying the +// encoding. See bug 1150114. +// +// For excludedSubtrees, we simply prohibit any non-empty directoryName +// constraint to ensure we are not being too lenient. We support empty +// DirectoryName constraints in excludedSubtrees so that a CA can say "Do not +// allow any DirectoryNames in issued certificates." +Result +MatchPresentedDirectoryNameWithConstraint(NameConstraintsSubtrees subtreesType, + Input presentedID, + Input directoryNameConstraint, + /*out*/ bool& matches) +{ + Reader constraintRDNs; + Result rv = der::ExpectTagAndGetValueAtEnd(directoryNameConstraint, + der::SEQUENCE, constraintRDNs); + if (rv != Success) { + return rv; + } + Reader presentedRDNs; + rv = der::ExpectTagAndGetValueAtEnd(presentedID, der::SEQUENCE, + presentedRDNs); + if (rv != Success) { + return rv; + } + + switch (subtreesType) { + case NameConstraintsSubtrees::permittedSubtrees: + break; // dealt with below + case NameConstraintsSubtrees::excludedSubtrees: + if (!constraintRDNs.AtEnd() || !presentedRDNs.AtEnd()) { + return Result::ERROR_CERT_NOT_IN_NAME_SPACE; + } + matches = true; + return Success; + } + + for (;;) { + // The AVAs have to be fully equal, but the constraint RDNs just need to be + // a prefix of the presented RDNs. + if (constraintRDNs.AtEnd()) { + matches = true; + return Success; + } + if (presentedRDNs.AtEnd()) { + matches = false; + return Success; + } + Reader constraintRDN; + rv = der::ExpectTagAndGetValue(constraintRDNs, der::SET, constraintRDN); + if (rv != Success) { + return rv; + } + Reader presentedRDN; + rv = der::ExpectTagAndGetValue(presentedRDNs, der::SET, presentedRDN); + if (rv != Success) { + return rv; + } + while (!constraintRDN.AtEnd() && !presentedRDN.AtEnd()) { + Input constraintType; + uint8_t constraintValueTag; + Input constraintValue; + rv = ReadAVA(constraintRDN, constraintType, constraintValueTag, + constraintValue); + if (rv != Success) { + return rv; + } + Input presentedType; + uint8_t presentedValueTag; + Input presentedValue; + rv = ReadAVA(presentedRDN, presentedType, presentedValueTag, + presentedValue); + if (rv != Success) { + return rv; + } + // TODO (bug 1155767): verify that if an AVA is a PrintableString it + // consists only of characters valid for PrintableStrings. + bool avasMatch = + InputsAreEqual(constraintType, presentedType) && + InputsAreEqual(constraintValue, presentedValue) && + (constraintValueTag == presentedValueTag || + (constraintValueTag == der::Tag::UTF8String && + presentedValueTag == der::Tag::PrintableString) || + (constraintValueTag == der::Tag::PrintableString && + presentedValueTag == der::Tag::UTF8String)); + if (!avasMatch) { + matches = false; + return Success; + } + } + if (!constraintRDN.AtEnd() || !presentedRDN.AtEnd()) { + matches = false; + return Success; + } + } +} + +// RFC 5280 says: +// +// The format of an rfc822Name is a "Mailbox" as defined in Section 4.1.2 +// of [RFC2821]. A Mailbox has the form "Local-part@Domain". Note that a +// Mailbox has no phrase (such as a common name) before it, has no comment +// (text surrounded in parentheses) after it, and is not surrounded by "<" +// and ">". Rules for encoding Internet mail addresses that include +// internationalized domain names are specified in Section 7.5. +// +// and: +// +// A name constraint for Internet mail addresses MAY specify a +// particular mailbox, all addresses at a particular host, or all +// mailboxes in a domain. To indicate a particular mailbox, the +// constraint is the complete mail address. For example, +// "root@example.com" indicates the root mailbox on the host +// "example.com". To indicate all Internet mail addresses on a +// particular host, the constraint is specified as the host name. For +// example, the constraint "example.com" is satisfied by any mail +// address at the host "example.com". To specify any address within a +// domain, the constraint is specified with a leading period (as with +// URIs). For example, ".example.com" indicates all the Internet mail +// addresses in the domain "example.com", but not Internet mail +// addresses on the host "example.com". + +bool +IsValidRFC822Name(Input input) +{ + Reader reader(input); + + // Local-part@. + bool startOfAtom = true; + for (;;) { + uint8_t presentedByte; + if (reader.Read(presentedByte) != Success) { + return false; + } + switch (presentedByte) { + // atext is defined in https://tools.ietf.org/html/rfc2822#section-3.2.4 + case 'A': case 'a': case 'N': case 'n': case '0': case '!': case '#': + case 'B': case 'b': case 'O': case 'o': case '1': case '$': case '%': + case 'C': case 'c': case 'P': case 'p': case '2': case '&': case '\'': + case 'D': case 'd': case 'Q': case 'q': case '3': case '*': case '+': + case 'E': case 'e': case 'R': case 'r': case '4': case '-': case '/': + case 'F': case 'f': case 'S': case 's': case '5': case '=': case '?': + case 'G': case 'g': case 'T': case 't': case '6': case '^': case '_': + case 'H': case 'h': case 'U': case 'u': case '7': case '`': case '{': + case 'I': case 'i': case 'V': case 'v': case '8': case '|': case '}': + case 'J': case 'j': case 'W': case 'w': case '9': case '~': + case 'K': case 'k': case 'X': case 'x': + case 'L': case 'l': case 'Y': case 'y': + case 'M': case 'm': case 'Z': case 'z': + startOfAtom = false; + break; + + case '.': + if (startOfAtom) { + return false; + } + startOfAtom = true; + break; + + case '@': + { + if (startOfAtom) { + return false; + } + Input domain; + if (reader.SkipToEnd(domain) != Success) { + return false; + } + return IsValidDNSID(domain, IDRole::PresentedID, AllowWildcards::No); + } + + default: + return false; + } + } +} + +Result +MatchPresentedRFC822NameWithReferenceRFC822Name(Input presentedRFC822Name, + IDRole referenceRFC822NameRole, + Input referenceRFC822Name, + /*out*/ bool& matches) +{ + if (!IsValidRFC822Name(presentedRFC822Name)) { + return Result::ERROR_BAD_DER; + } + Reader presented(presentedRFC822Name); + + switch (referenceRFC822NameRole) + { + case IDRole::PresentedID: + return Result::FATAL_ERROR_INVALID_ARGS; + + case IDRole::ReferenceID: + break; + + case IDRole::NameConstraint: + { + if (InputContains(referenceRFC822Name, '@')) { + // The constraint is of the form "Local-part@Domain". + break; + } + + // The constraint is of the form "example.com" or ".example.com". + + // Skip past the '@' in the presented ID. + for (;;) { + uint8_t presentedByte; + if (presented.Read(presentedByte) != Success) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + if (presentedByte == '@') { + break; + } + } + + Input presentedDNSID; + if (presented.SkipToEnd(presentedDNSID) != Success) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + return MatchPresentedDNSIDWithReferenceDNSID( + presentedDNSID, AllowWildcards::No, + AllowDotlessSubdomainMatches::No, IDRole::NameConstraint, + referenceRFC822Name, matches); + } + } + + if (!IsValidRFC822Name(referenceRFC822Name)) { + return Result::ERROR_BAD_DER; + } + + Reader reference(referenceRFC822Name); + + for (;;) { + uint8_t presentedByte; + if (presented.Read(presentedByte) != Success) { + matches = reference.AtEnd(); + return Success; + } + uint8_t referenceByte; + if (reference.Read(referenceByte) != Success) { + matches = false; + return Success; + } + if (LocaleInsensitveToLower(presentedByte) != + LocaleInsensitveToLower(referenceByte)) { + matches = false; + return Success; + } + } +} + +// We avoid isdigit because it is locale-sensitive. See +// http://pubs.opengroup.org/onlinepubs/009695399/functions/tolower.html. +inline uint8_t +LocaleInsensitveToLower(uint8_t a) +{ + if (a >= 'A' && a <= 'Z') { // unlikely + return static_cast( + static_cast(a - static_cast('A')) + + static_cast('a')); + } + return a; +} + +bool +StartsWithIDNALabel(Input id) +{ + static const uint8_t IDN_ALABEL_PREFIX[4] = { 'x', 'n', '-', '-' }; + Reader input(id); + for (size_t i = 0; i < sizeof(IDN_ALABEL_PREFIX); ++i) { + uint8_t b; + if (input.Read(b) != Success) { + return false; + } + if (b != IDN_ALABEL_PREFIX[i]) { + return false; + } + } + return true; +} + +bool +ReadIPv4AddressComponent(Reader& input, bool lastComponent, + /*out*/ uint8_t& valueOut) +{ + size_t length = 0; + unsigned int value = 0; // Must be larger than uint8_t. + + for (;;) { + if (input.AtEnd() && lastComponent) { + break; + } + + uint8_t b; + if (input.Read(b) != Success) { + return false; + } + + if (b >= '0' && b <= '9') { + if (value == 0 && length > 0) { + return false; // Leading zeros are not allowed. + } + value = (value * 10) + (b - '0'); + if (value > 255) { + return false; // Component's value is too large. + } + ++length; + } else if (!lastComponent && b == '.') { + break; + } else { + return false; // Invalid character. + } + } + + if (length == 0) { + return false; // empty components not allowed + } + + valueOut = static_cast(value); + return true; +} + +} // namespace + +// On Windows and maybe other platforms, OS-provided IP address parsing +// functions might fail if the protocol (IPv4 or IPv6) has been disabled, so we +// can't rely on them. +bool +ParseIPv4Address(Input hostname, /*out*/ uint8_t (&out)[4]) +{ + Reader input(hostname); + return ReadIPv4AddressComponent(input, false, out[0]) && + ReadIPv4AddressComponent(input, false, out[1]) && + ReadIPv4AddressComponent(input, false, out[2]) && + ReadIPv4AddressComponent(input, true, out[3]); +} + +namespace { + +bool +FinishIPv6Address(/*in/out*/ uint8_t (&address)[16], int numComponents, + int contractionIndex) +{ + assert(numComponents >= 0); + assert(numComponents <= 8); + assert(contractionIndex >= -1); + assert(contractionIndex <= 8); + assert(contractionIndex <= numComponents); + if (!(numComponents >= 0 && + numComponents <= 8 && + contractionIndex >= -1 && + contractionIndex <= 8 && + contractionIndex <= numComponents)) { + return false; + } + + if (contractionIndex == -1) { + // no contraction + return numComponents == 8; + } + + if (numComponents >= 8) { + return false; // no room left to expand the contraction. + } + + // Shift components that occur after the contraction over. + size_t componentsToMove = static_cast(numComponents - + contractionIndex); + memmove(address + (2u * static_cast(8 - componentsToMove)), + address + (2u * static_cast(contractionIndex)), + componentsToMove * 2u); + // Fill in the contracted area with zeros. + std::fill_n(address + 2u * static_cast(contractionIndex), + (8u - static_cast(numComponents)) * 2u, static_cast(0u)); + + return true; +} + +} // namespace + +// On Windows and maybe other platforms, OS-provided IP address parsing +// functions might fail if the protocol (IPv4 or IPv6) has been disabled, so we +// can't rely on them. +bool +ParseIPv6Address(Input hostname, /*out*/ uint8_t (&out)[16]) +{ + Reader input(hostname); + + int currentComponentIndex = 0; + int contractionIndex = -1; + + if (input.Peek(':')) { + // A valid input can only start with ':' if there is a contraction at the + // beginning. + uint8_t b; + if (input.Read(b) != Success || b != ':') { + assert(false); + return false; + } + if (input.Read(b) != Success) { + return false; + } + if (b != ':') { + return false; + } + contractionIndex = 0; + } + + for (;;) { + // If we encounter a '.' then we'll have to backtrack to parse the input + // from startOfComponent to the end of the input as an IPv4 address. + Reader::Mark startOfComponent(input.GetMark()); + uint16_t componentValue = 0; + size_t componentLength = 0; + while (!input.AtEnd() && !input.Peek(':')) { + uint8_t value; + uint8_t b; + if (input.Read(b) != Success) { + assert(false); + return false; + } + switch (b) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + value = static_cast(b - static_cast('0')); + break; + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + value = static_cast(b - static_cast('a') + + UINT8_C(10)); + break; + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + value = static_cast(b - static_cast('A') + + UINT8_C(10)); + break; + case '.': + { + // A dot indicates we hit a IPv4-syntax component. Backtrack, parsing + // the input from startOfComponent to the end of the input as an IPv4 + // address, and then combine it with the other components. + + if (currentComponentIndex > 6) { + return false; // Too many components before the IPv4 component + } + + input.SkipToEnd(); + Input ipv4Component; + if (input.GetInput(startOfComponent, ipv4Component) != Success) { + return false; + } + uint8_t (*ipv4)[4] = + reinterpret_cast(&out[2 * currentComponentIndex]); + if (!ParseIPv4Address(ipv4Component, *ipv4)) { + return false; + } + assert(input.AtEnd()); + currentComponentIndex += 2; + + return FinishIPv6Address(out, currentComponentIndex, + contractionIndex); + } + default: + return false; + } + if (componentLength >= 4) { + // component too long + return false; + } + ++componentLength; + componentValue = (componentValue * 0x10u) + value; + } + + if (currentComponentIndex >= 8) { + return false; // too many components + } + + if (componentLength == 0) { + if (input.AtEnd() && currentComponentIndex == contractionIndex) { + if (contractionIndex == 0) { + // don't accept "::" + return false; + } + return FinishIPv6Address(out, currentComponentIndex, + contractionIndex); + } + return false; + } + + out[2 * currentComponentIndex] = + static_cast(componentValue / 0x100); + out[(2 * currentComponentIndex) + 1] = + static_cast(componentValue % 0x100); + + ++currentComponentIndex; + + if (input.AtEnd()) { + return FinishIPv6Address(out, currentComponentIndex, + contractionIndex); + } + + uint8_t b; + if (input.Read(b) != Success || b != ':') { + assert(false); + return false; + } + + if (input.Peek(':')) { + // Contraction + if (contractionIndex != -1) { + return false; // multiple contractions are not allowed. + } + if (input.Read(b) != Success || b != ':') { + assert(false); + return false; + } + contractionIndex = currentComponentIndex; + if (input.AtEnd()) { + // "::" at the end of the input. + return FinishIPv6Address(out, currentComponentIndex, + contractionIndex); + } + } + } +} + +bool +IsValidReferenceDNSID(Input hostname) +{ + return IsValidDNSID(hostname, IDRole::ReferenceID, AllowWildcards::No); +} + +bool +IsValidPresentedDNSID(Input hostname) +{ + return IsValidDNSID(hostname, IDRole::PresentedID, AllowWildcards::Yes); +} + +namespace { + +// RFC 5280 Section 4.2.1.6 says that a dNSName "MUST be in the 'preferred name +// syntax', as specified by Section 3.5 of [RFC1034] and as modified by Section +// 2.1 of [RFC1123]" except "a dNSName of ' ' MUST NOT be used." Additionally, +// we allow underscores for compatibility with existing practice. +bool +IsValidDNSID(Input hostname, IDRole idRole, AllowWildcards allowWildcards) +{ + if (hostname.GetLength() > 253) { + return false; + } + + Reader input(hostname); + + if (idRole == IDRole::NameConstraint && input.AtEnd()) { + return true; + } + + size_t dotCount = 0; + size_t labelLength = 0; + bool labelIsAllNumeric = false; + bool labelEndsWithHyphen = false; + + // Only presented IDs are allowed to have wildcard labels. And, like + // Chromium, be stricter than RFC 6125 requires by insisting that a + // wildcard label consist only of '*'. + bool isWildcard = allowWildcards == AllowWildcards::Yes && input.Peek('*'); + bool isFirstByte = !isWildcard; + if (isWildcard) { + Result rv = input.Skip(1); + if (rv != Success) { + assert(false); + return false; + } + + uint8_t b; + rv = input.Read(b); + if (rv != Success) { + return false; + } + if (b != '.') { + return false; + } + ++dotCount; + } + + do { + static const size_t MAX_LABEL_LENGTH = 63; + + uint8_t b; + if (input.Read(b) != Success) { + return false; + } + switch (b) { + case '-': + if (labelLength == 0) { + return false; // Labels must not start with a hyphen. + } + labelIsAllNumeric = false; + labelEndsWithHyphen = true; + ++labelLength; + if (labelLength > MAX_LABEL_LENGTH) { + return false; + } + break; + + // We avoid isdigit because it is locale-sensitive. See + // http://pubs.opengroup.org/onlinepubs/009695399/functions/isdigit.html + case '0': case '5': + case '1': case '6': + case '2': case '7': + case '3': case '8': + case '4': case '9': + if (labelLength == 0) { + labelIsAllNumeric = true; + } + labelEndsWithHyphen = false; + ++labelLength; + if (labelLength > MAX_LABEL_LENGTH) { + return false; + } + break; + + // We avoid using islower/isupper/tolower/toupper or similar things, to + // avoid any possibility of this code being locale-sensitive. See + // http://pubs.opengroup.org/onlinepubs/009695399/functions/isupper.html + case 'a': case 'A': case 'n': case 'N': + case 'b': case 'B': case 'o': case 'O': + case 'c': case 'C': case 'p': case 'P': + case 'd': case 'D': case 'q': case 'Q': + case 'e': case 'E': case 'r': case 'R': + case 'f': case 'F': case 's': case 'S': + case 'g': case 'G': case 't': case 'T': + case 'h': case 'H': case 'u': case 'U': + case 'i': case 'I': case 'v': case 'V': + case 'j': case 'J': case 'w': case 'W': + case 'k': case 'K': case 'x': case 'X': + case 'l': case 'L': case 'y': case 'Y': + case 'm': case 'M': case 'z': case 'Z': + // We allow underscores for compatibility with existing practices. + // See bug 1136616. + case '_': + labelIsAllNumeric = false; + labelEndsWithHyphen = false; + ++labelLength; + if (labelLength > MAX_LABEL_LENGTH) { + return false; + } + break; + + case '.': + ++dotCount; + if (labelLength == 0 && + (idRole != IDRole::NameConstraint || !isFirstByte)) { + return false; + } + if (labelEndsWithHyphen) { + return false; // Labels must not end with a hyphen. + } + labelLength = 0; + break; + + default: + return false; // Invalid character. + } + isFirstByte = false; + } while (!input.AtEnd()); + + // Only reference IDs, not presented IDs or name constraints, may be + // absolute. + if (labelLength == 0 && idRole != IDRole::ReferenceID) { + return false; + } + + if (labelEndsWithHyphen) { + return false; // Labels must not end with a hyphen. + } + + if (labelIsAllNumeric) { + return false; // Last label must not be all numeric. + } + + if (isWildcard) { + // If the DNS ID ends with a dot, the last dot signifies an absolute ID. + size_t labelCount = (labelLength == 0) ? dotCount : (dotCount + 1); + + // Like NSS, require at least two labels to follow the wildcard label. + // + // TODO(bug XXXXXXX): Allow the TrustDomain to control this on a + // per-eTLD+1 basis, similar to Chromium. Even then, it might be better to + // still enforce that there are at least two labels after the wildcard. + if (labelCount < 3) { + return false; + } + // XXX: RFC6125 says that we shouldn't accept wildcards within an IDN + // A-Label. The consequence of this is that we effectively discriminate + // against users of languages that cannot be encoded with ASCII. + if (StartsWithIDNALabel(hostname)) { + return false; + } + + // TODO(bug XXXXXXX): Wildcards are not allowed for EV certificates. + // Provide an option to indicate whether wildcards should be matched, for + // the purpose of helping the application enforce this. + } + + return true; +} + +} // namespace + +} } // namespace mozilla::pkix diff --git a/security/pkix/lib/pkixnss.cpp b/security/pkix/lib/pkixnss.cpp new file mode 100644 index 000000000..798a19265 --- /dev/null +++ b/security/pkix/lib/pkixnss.cpp @@ -0,0 +1,228 @@ +/*- *- 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/pkixnss.h" + +#include + +#include "cryptohi.h" +#include "keyhi.h" +#include "pk11pub.h" +#include "pkix/pkix.h" +#include "pkixutil.h" +#include "ScopedPtr.h" +#include "secerr.h" +#include "sslerr.h" + +namespace mozilla { namespace pkix { + +namespace { + +Result +VerifySignedDigest(const SignedDigest& sd, + Input subjectPublicKeyInfo, + SECOidTag pubKeyAlg, + void* pkcs11PinArg) +{ + SECOidTag digestAlg; + switch (sd.digestAlgorithm) { + case DigestAlgorithm::sha512: digestAlg = SEC_OID_SHA512; break; + case DigestAlgorithm::sha384: digestAlg = SEC_OID_SHA384; break; + case DigestAlgorithm::sha256: digestAlg = SEC_OID_SHA256; break; + case DigestAlgorithm::sha1: digestAlg = SEC_OID_SHA1; break; + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } + + SECItem subjectPublicKeyInfoSECItem = + UnsafeMapInputToSECItem(subjectPublicKeyInfo); + ScopedPtr + spki(SECKEY_DecodeDERSubjectPublicKeyInfo(&subjectPublicKeyInfoSECItem)); + if (!spki) { + return MapPRErrorCodeToResult(PR_GetError()); + } + ScopedPtr + pubKey(SECKEY_ExtractPublicKey(spki.get())); + if (!pubKey) { + return MapPRErrorCodeToResult(PR_GetError()); + } + + SECItem digestSECItem(UnsafeMapInputToSECItem(sd.digest)); + SECItem signatureSECItem(UnsafeMapInputToSECItem(sd.signature)); + SECStatus srv = VFY_VerifyDigestDirect(&digestSECItem, pubKey.get(), + &signatureSECItem, pubKeyAlg, + digestAlg, pkcs11PinArg); + if (srv != SECSuccess) { + return MapPRErrorCodeToResult(PR_GetError()); + } + + return Success; +} + +} // namespace + +Result +VerifyRSAPKCS1SignedDigestNSS(const SignedDigest& sd, + Input subjectPublicKeyInfo, + void* pkcs11PinArg) +{ + return VerifySignedDigest(sd, subjectPublicKeyInfo, + SEC_OID_PKCS1_RSA_ENCRYPTION, pkcs11PinArg); +} + +Result +VerifyECDSASignedDigestNSS(const SignedDigest& sd, + Input subjectPublicKeyInfo, + void* pkcs11PinArg) +{ + return VerifySignedDigest(sd, subjectPublicKeyInfo, + SEC_OID_ANSIX962_EC_PUBLIC_KEY, pkcs11PinArg); +} + +Result +DigestBufNSS(Input item, + DigestAlgorithm digestAlg, + /*out*/ uint8_t* digestBuf, + size_t digestBufLen) +{ + SECOidTag oid; + size_t bits; + switch (digestAlg) { + case DigestAlgorithm::sha512: oid = SEC_OID_SHA512; bits = 512; break; + case DigestAlgorithm::sha384: oid = SEC_OID_SHA384; bits = 384; break; + case DigestAlgorithm::sha256: oid = SEC_OID_SHA256; bits = 256; break; + case DigestAlgorithm::sha1: oid = SEC_OID_SHA1; bits = 160; break; + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } + if (digestBufLen != bits / 8) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + + SECItem itemSECItem = UnsafeMapInputToSECItem(item); + if (itemSECItem.len > + static_cast( + std::numeric_limits::max())) { + PR_NOT_REACHED("large items should not be possible here"); + return Result::FATAL_ERROR_INVALID_ARGS; + } + SECStatus srv = PK11_HashBuf(oid, digestBuf, itemSECItem.data, + static_cast(itemSECItem.len)); + if (srv != SECSuccess) { + return MapPRErrorCodeToResult(PR_GetError()); + } + return Success; +} + +Result +MapPRErrorCodeToResult(PRErrorCode error) +{ + switch (error) + { +#define MOZILLA_PKIX_MAP(mozilla_pkix_result, value, nss_result) \ + case nss_result: return Result::mozilla_pkix_result; + + MOZILLA_PKIX_MAP_LIST + +#undef MOZILLA_PKIX_MAP + + default: + return Result::ERROR_UNKNOWN_ERROR; + } +} + +PRErrorCode +MapResultToPRErrorCode(Result result) +{ + switch (result) + { +#define MOZILLA_PKIX_MAP(mozilla_pkix_result, value, nss_result) \ + case Result::mozilla_pkix_result: return nss_result; + + MOZILLA_PKIX_MAP_LIST + +#undef MOZILLA_PKIX_MAP + + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } +} + +void +RegisterErrorTable() +{ + // Note that these error strings are not localizable. + // When these strings change, update the localization information too. + static const PRErrorMessage ErrorTableText[] = { + { "MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE", + "The server uses key pinning (HPKP) but no trusted certificate chain " + "could be constructed that matches the pinset. Key pinning violations " + "cannot be overridden." }, + { "MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY", + "The server uses a certificate with a basic constraints extension " + "identifying it as a certificate authority. For a properly-issued " + "certificate, this should not be the case." }, + { "MOZILLA_PKIX_ERROR_INADEQUATE_KEY_SIZE", + "The server presented a certificate with a key size that is too small " + "to establish a secure connection." }, + { "MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA", + "An X.509 version 1 certificate that is not a trust anchor was used to " + "issue the server's certificate. X.509 version 1 certificates are " + "deprecated and should not be used to sign other certificates." }, + { "MOZILLA_PKIX_ERROR_NO_RFC822NAME_MATCH", + "The certificate is not valid for the given email address." }, + { "MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE", + "The server presented a certificate that is not yet valid." }, + { "MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE", + "A certificate that is not yet valid was used to issue the server's " + "certificate." }, + { "MOZILLA_PKIX_ERROR_SIGNATURE_ALGORITHM_MISMATCH", + "The signature algorithm in the signature field of the certificate does " + "not match the algorithm in its signatureAlgorithm field." }, + { "MOZILLA_PKIX_ERROR_OCSP_RESPONSE_FOR_CERT_MISSING", + "The OCSP response does not include a status for the certificate being " + "verified." }, + { "MOZILLA_PKIX_ERROR_VALIDITY_TOO_LONG", + "The server presented a certificate that is valid for too long." }, + { "MOZILLA_PKIX_ERROR_REQUIRED_TLS_FEATURE_MISSING", + "A required TLS feature is missing." }, + { "MOZILLA_PKIX_ERROR_INVALID_INTEGER_ENCODING", + "The server presented a certificate that contains an invalid encoding of " + "an integer. Common causes include negative serial numbers, negative RSA " + "moduli, and encodings that are longer than necessary." }, + { "MOZILLA_PKIX_ERROR_EMPTY_ISSUER_NAME", + "The server presented a certificate with an empty issuer distinguished " + "name." }, + }; + // Note that these error strings are not localizable. + // When these strings change, update the localization information too. + + static const PRErrorTable ErrorTable = { + ErrorTableText, + "pkixerrors", + ERROR_BASE, + PR_ARRAY_SIZE(ErrorTableText) + }; + + (void) PR_ErrorInstallTable(&ErrorTable); +} + +} } // namespace mozilla::pkix diff --git a/security/pkix/lib/pkixocsp.cpp b/security/pkix/lib/pkixocsp.cpp new file mode 100644 index 000000000..06cb53bc4 --- /dev/null +++ b/security/pkix/lib/pkixocsp.cpp @@ -0,0 +1,1007 @@ +/* -*- 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 + +#include "pkix/pkix.h" +#include "pkixcheck.h" +#include "pkixutil.h" + +namespace { + +const size_t SHA1_DIGEST_LENGTH = 160 / 8; + +} // namespace + +namespace mozilla { namespace pkix { + +// These values correspond to the tag values in the ASN.1 CertStatus +enum class CertStatus : uint8_t { + Good = der::CONTEXT_SPECIFIC | 0, + Revoked = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 1, + Unknown = der::CONTEXT_SPECIFIC | 2 +}; + +class Context final +{ +public: + Context(TrustDomain& trustDomain, const CertID& certID, Time time, + uint16_t maxLifetimeInDays, /*optional out*/ Time* thisUpdate, + /*optional out*/ Time* validThrough) + : trustDomain(trustDomain) + , certID(certID) + , time(time) + , maxLifetimeInDays(maxLifetimeInDays) + , certStatus(CertStatus::Unknown) + , thisUpdate(thisUpdate) + , validThrough(validThrough) + , expired(false) + , matchFound(false) + { + if (thisUpdate) { + *thisUpdate = TimeFromElapsedSecondsAD(0); + } + if (validThrough) { + *validThrough = TimeFromElapsedSecondsAD(0); + } + } + + TrustDomain& trustDomain; + const CertID& certID; + const Time time; + const uint16_t maxLifetimeInDays; + CertStatus certStatus; + Time* thisUpdate; + Time* validThrough; + bool expired; + + Input signedCertificateTimestamps; + + // Keep track of whether the OCSP response contains the status of the + // certificate we're interested in. Responders might reply without + // including the status of any of the requested certs, we should + // indicate a server failure in those cases. + bool matchFound; + + Context(const Context&) = delete; + void operator=(const Context&) = delete; +}; + +// Verify that potentialSigner is a valid delegated OCSP response signing cert +// according to RFC 6960 section 4.2.2.2. +static Result +CheckOCSPResponseSignerCert(TrustDomain& trustDomain, + BackCert& potentialSigner, + Input issuerSubject, + Input issuerSubjectPublicKeyInfo, + Time time) +{ + Result rv; + + // We don't need to do a complete verification of the signer (i.e. we don't + // have to call BuildCertChain to verify the entire chain) because we + // already know that the issuer is valid, since revocation checking is done + // from the root to the parent after we've built a complete chain that we + // know is otherwise valid. Rather, we just need to do a one-step validation + // from potentialSigner to the issuer. + // + // It seems reasonable to require the KU_DIGITAL_SIGNATURE key usage on the + // OCSP responder certificate if the OCSP responder certificate has a + // key usage extension. However, according to bug 240456, some OCSP responder + // certificates may have only the nonRepudiation bit set. Also, the OCSP + // specification (RFC 6960) does not mandate any particular key usage to be + // asserted for OCSP responde signers. Oddly, the CABForum Baseline + // Requirements v.1.1.5 do say "If the Root CA Private Key is used for + // signing OCSP responses, then the digitalSignature bit MUST be set." + // + // Note that CheckIssuerIndependentProperties processes + // SEC_OID_OCSP_RESPONDER in the way that the OCSP specification requires us + // to--in particular, it doesn't allow SEC_OID_OCSP_RESPONDER to be implied + // by a missing EKU extension, unlike other EKUs. + // + // TODO(bug 926261): If we're validating for a policy then the policy OID we + // are validating for should be passed to CheckIssuerIndependentProperties. + TrustLevel unusedTrustLevel; + rv = CheckIssuerIndependentProperties(trustDomain, potentialSigner, time, + KeyUsage::noParticularKeyUsageRequired, + KeyPurposeId::id_kp_OCSPSigning, + CertPolicyId::anyPolicy, 0, + unusedTrustLevel); + if (rv != Success) { + return rv; + } + + // It is possible that there exists a certificate with the same key as the + // issuer but with a different name, so we need to compare names + // XXX(bug 926270) XXX(bug 1008133) XXX(bug 980163): Improve name + // comparison. + // TODO: needs test + if (!InputsAreEqual(potentialSigner.GetIssuer(), issuerSubject)) { + return Result::ERROR_OCSP_RESPONDER_CERT_INVALID; + } + + // TODO(bug 926260): check name constraints + + rv = VerifySignedData(trustDomain, potentialSigner.GetSignedData(), + issuerSubjectPublicKeyInfo); + + // TODO: check for revocation of the OCSP responder certificate unless no-check + // or the caller forcing no-check. To properly support the no-check policy, we'd + // need to enforce policy constraints from the issuerChain. + + return rv; +} + +enum class ResponderIDType : uint8_t +{ + byName = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 1, + byKey = der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 2 +}; + +static inline Result OCSPResponse(Reader&, Context&); +static inline Result ResponseBytes(Reader&, Context&); +static inline Result BasicResponse(Reader&, Context&); +static inline Result ResponseData( + Reader& tbsResponseData, + Context& context, + const der::SignedDataWithSignature& signedResponseData, + const DERArray& certs); +static inline Result SingleResponse(Reader& input, Context& context); +static Result ExtensionNotUnderstood(Reader& extnID, Input extnValue, + bool critical, /*out*/ bool& understood); +static Result RememberSingleExtension(Context& context, Reader& extnID, + Input extnValue, bool critical, + /*out*/ bool& understood); +static inline Result CertID(Reader& input, + const Context& context, + /*out*/ bool& match); +static Result MatchKeyHash(TrustDomain& trustDomain, + Input issuerKeyHash, + Input issuerSubjectPublicKeyInfo, + /*out*/ bool& match); +static Result KeyHash(TrustDomain& trustDomain, + Input subjectPublicKeyInfo, + /*out*/ uint8_t* hashBuf, size_t hashBufSize); + +static Result +MatchResponderID(TrustDomain& trustDomain, + ResponderIDType responderIDType, + Input responderID, + Input potentialSignerSubject, + Input potentialSignerSubjectPublicKeyInfo, + /*out*/ bool& match) +{ + match = false; + + switch (responderIDType) { + case ResponderIDType::byName: + // XXX(bug 926270) XXX(bug 1008133) XXX(bug 980163): Improve name + // comparison. + match = InputsAreEqual(responderID, potentialSignerSubject); + return Success; + + case ResponderIDType::byKey: + { + Reader input(responderID); + Input keyHash; + Result rv = der::ExpectTagAndGetValue(input, der::OCTET_STRING, keyHash); + if (rv != Success) { + return rv; + } + return MatchKeyHash(trustDomain, keyHash, + potentialSignerSubjectPublicKeyInfo, match); + } + + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } +} + +static Result +VerifyOCSPSignedData(TrustDomain& trustDomain, + const der::SignedDataWithSignature& signedResponseData, + Input spki) +{ + Result rv = VerifySignedData(trustDomain, signedResponseData, spki); + if (rv == Result::ERROR_BAD_SIGNATURE) { + rv = Result::ERROR_OCSP_BAD_SIGNATURE; + } + return rv; +} + +// RFC 6960 section 4.2.2.2: The OCSP responder must either be the issuer of +// the cert or it must be a delegated OCSP response signing cert directly +// issued by the issuer. If the OCSP responder is a delegated OCSP response +// signer, then its certificate is (probably) embedded within the OCSP +// response and we'll need to verify that it is a valid certificate that chains +// *directly* to issuerCert. +static Result +VerifySignature(Context& context, ResponderIDType responderIDType, + Input responderID, const DERArray& certs, + const der::SignedDataWithSignature& signedResponseData) +{ + bool match; + Result rv = MatchResponderID(context.trustDomain, responderIDType, + responderID, context.certID.issuer, + context.certID.issuerSubjectPublicKeyInfo, + match); + if (rv != Success) { + return rv; + } + if (match) { + return VerifyOCSPSignedData(context.trustDomain, signedResponseData, + context.certID.issuerSubjectPublicKeyInfo); + } + + size_t numCerts = certs.GetLength(); + for (size_t i = 0; i < numCerts; ++i) { + BackCert cert(*certs.GetDER(i), EndEntityOrCA::MustBeEndEntity, nullptr); + rv = cert.Init(); + if (rv != Success) { + return rv; + } + rv = MatchResponderID(context.trustDomain, responderIDType, responderID, + cert.GetSubject(), cert.GetSubjectPublicKeyInfo(), + match); + if (rv != Success) { + if (IsFatalError(rv)) { + return rv; + } + continue; + } + + if (match) { + rv = CheckOCSPResponseSignerCert(context.trustDomain, cert, + context.certID.issuer, + context.certID.issuerSubjectPublicKeyInfo, + context.time); + if (rv != Success) { + if (IsFatalError(rv)) { + return rv; + } + continue; + } + + return VerifyOCSPSignedData(context.trustDomain, signedResponseData, + cert.GetSubjectPublicKeyInfo()); + } + } + + return Result::ERROR_OCSP_INVALID_SIGNING_CERT; +} + +static inline Result +MapBadDERToMalformedOCSPResponse(Result rv) +{ + if (rv == Result::ERROR_BAD_DER) { + return Result::ERROR_OCSP_MALFORMED_RESPONSE; + } + return rv; +} + +Result +VerifyEncodedOCSPResponse(TrustDomain& trustDomain, const struct CertID& certID, + Time time, uint16_t maxOCSPLifetimeInDays, + Input encodedResponse, + /*out*/ bool& expired, + /*optional out*/ Time* thisUpdate, + /*optional out*/ Time* validThrough) +{ + // Always initialize this to something reasonable. + expired = false; + + Context context(trustDomain, certID, time, maxOCSPLifetimeInDays, + thisUpdate, validThrough); + + Reader input(encodedResponse); + Result rv = der::Nested(input, der::SEQUENCE, [&context](Reader& r) { + return OCSPResponse(r, context); + }); + if (rv != Success) { + return MapBadDERToMalformedOCSPResponse(rv); + } + rv = der::End(input); + if (rv != Success) { + return MapBadDERToMalformedOCSPResponse(rv); + } + if (!context.matchFound) { + return Result::ERROR_OCSP_RESPONSE_FOR_CERT_MISSING; + } + + expired = context.expired; + + switch (context.certStatus) { + case CertStatus::Good: + if (expired) { + return Result::ERROR_OCSP_OLD_RESPONSE; + } + if (context.signedCertificateTimestamps.GetLength()) { + Input sctList; + rv = ExtractSignedCertificateTimestampListFromExtension( + context.signedCertificateTimestamps, sctList); + if (rv != Success) { + return MapBadDERToMalformedOCSPResponse(rv); + } + context.trustDomain.NoteAuxiliaryExtension( + AuxiliaryExtension::SCTListFromOCSPResponse, sctList); + } + return Success; + case CertStatus::Revoked: + return Result::ERROR_REVOKED_CERTIFICATE; + case CertStatus::Unknown: + return Result::ERROR_OCSP_UNKNOWN_CERT; + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } +} + +// OCSPResponse ::= SEQUENCE { +// responseStatus OCSPResponseStatus, +// responseBytes [0] EXPLICIT ResponseBytes OPTIONAL } +// +static inline Result +OCSPResponse(Reader& input, Context& context) +{ + // OCSPResponseStatus ::= ENUMERATED { + // successful (0), -- Response has valid confirmations + // malformedRequest (1), -- Illegal confirmation request + // internalError (2), -- Internal error in issuer + // tryLater (3), -- Try again later + // -- (4) is not used + // sigRequired (5), -- Must sign the request + // unauthorized (6) -- Request unauthorized + // } + uint8_t responseStatus; + + Result rv = der::Enumerated(input, responseStatus); + if (rv != Success) { + return rv; + } + switch (responseStatus) { + case 0: break; // successful + case 1: return Result::ERROR_OCSP_MALFORMED_REQUEST; + case 2: return Result::ERROR_OCSP_SERVER_ERROR; + case 3: return Result::ERROR_OCSP_TRY_SERVER_LATER; + case 5: return Result::ERROR_OCSP_REQUEST_NEEDS_SIG; + case 6: return Result::ERROR_OCSP_UNAUTHORIZED_REQUEST; + default: return Result::ERROR_OCSP_UNKNOWN_RESPONSE_STATUS; + } + + return der::Nested(input, der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 0, + der::SEQUENCE, [&context](Reader& r) { + return ResponseBytes(r, context); + }); +} + +// ResponseBytes ::= SEQUENCE { +// responseType OBJECT IDENTIFIER, +// response OCTET STRING } +static inline Result +ResponseBytes(Reader& input, Context& context) +{ + static const uint8_t id_pkix_ocsp_basic[] = { + 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x01 + }; + + Result rv = der::OID(input, id_pkix_ocsp_basic); + if (rv != Success) { + return rv; + } + + return der::Nested(input, der::OCTET_STRING, der::SEQUENCE, + [&context](Reader& r) { + return BasicResponse(r, context); + }); +} + +// BasicOCSPResponse ::= SEQUENCE { +// tbsResponseData ResponseData, +// signatureAlgorithm AlgorithmIdentifier, +// signature BIT STRING, +// certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL } +Result +BasicResponse(Reader& input, Context& context) +{ + Reader tbsResponseData; + der::SignedDataWithSignature signedData; + Result rv = der::SignedData(input, tbsResponseData, signedData); + if (rv != Success) { + if (rv == Result::ERROR_BAD_SIGNATURE) { + return Result::ERROR_OCSP_BAD_SIGNATURE; + } + return rv; + } + + // Parse certificates, if any + NonOwningDERArray certs; + if (!input.AtEnd()) { + rv = der::Nested(input, der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 0, + der::SEQUENCE, [&certs](Reader& certsDER) -> Result { + while (!certsDER.AtEnd()) { + Input cert; + Result rv = der::ExpectTagAndGetTLV(certsDER, der::SEQUENCE, cert); + if (rv != Success) { + return rv; + } + rv = certs.Append(cert); + if (rv != Success) { + return Result::ERROR_BAD_DER; // Too many certs + } + } + return Success; + }); + if (rv != Success) { + return rv; + } + } + + return ResponseData(tbsResponseData, context, signedData, certs); +} + +// ResponseData ::= SEQUENCE { +// version [0] EXPLICIT Version DEFAULT v1, +// responderID ResponderID, +// producedAt GeneralizedTime, +// responses SEQUENCE OF SingleResponse, +// responseExtensions [1] EXPLICIT Extensions OPTIONAL } +static inline Result +ResponseData(Reader& input, Context& context, + const der::SignedDataWithSignature& signedResponseData, + const DERArray& certs) +{ + der::Version version; + Result rv = der::OptionalVersion(input, version); + if (rv != Success) { + return rv; + } + if (version != der::Version::v1) { + // TODO: more specific error code for bad version? + return Result::ERROR_BAD_DER; + } + + // ResponderID ::= CHOICE { + // byName [1] Name, + // byKey [2] KeyHash } + Input responderID; + ResponderIDType responderIDType + = input.Peek(static_cast(ResponderIDType::byName)) + ? ResponderIDType::byName + : ResponderIDType::byKey; + rv = der::ExpectTagAndGetValue(input, static_cast(responderIDType), + responderID); + if (rv != Success) { + return rv; + } + + // This is the soonest we can verify the signature. We verify the signature + // right away to follow the principal of minimizing the processing of data + // before verifying its signature. + rv = VerifySignature(context, responderIDType, responderID, certs, + signedResponseData); + if (rv != Success) { + return rv; + } + + // TODO: Do we even need to parse this? Should we just skip it? + Time producedAt(Time::uninitialized); + rv = der::GeneralizedTime(input, producedAt); + if (rv != Success) { + return rv; + } + + // We don't accept an empty sequence of responses. In practice, a legit OCSP + // responder will never return an empty response, and handling the case of an + // empty response makes things unnecessarily complicated. + rv = der::NestedOf(input, der::SEQUENCE, der::SEQUENCE, + der::EmptyAllowed::No, [&context](Reader& r) { + return SingleResponse(r, context); + }); + if (rv != Success) { + return rv; + } + + return der::OptionalExtensions(input, + der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 1, + ExtensionNotUnderstood); +} + +// SingleResponse ::= SEQUENCE { +// certID CertID, +// certStatus CertStatus, +// thisUpdate GeneralizedTime, +// nextUpdate [0] EXPLICIT GeneralizedTime OPTIONAL, +// singleExtensions [1] EXPLICIT Extensions{{re-ocsp-crl | +// re-ocsp-archive-cutoff | +// CrlEntryExtensions, ...} +// } OPTIONAL } +static inline Result +SingleResponse(Reader& input, Context& context) +{ + bool match = false; + Result rv = der::Nested(input, der::SEQUENCE, [&context, &match](Reader& r) { + return CertID(r, context, match); + }); + if (rv != Success) { + return rv; + } + + if (!match) { + // This response does not reference the certificate we're interested in. + // By consuming the rest of our input and returning successfully, we can + // continue processing and examine another response that might have what + // we want. + input.SkipToEnd(); + return Success; + } + + // We found a response for the cert we're interested in. + context.matchFound = true; + + // CertStatus ::= CHOICE { + // good [0] IMPLICIT NULL, + // revoked [1] IMPLICIT RevokedInfo, + // unknown [2] IMPLICIT UnknownInfo } + // + // In the event of multiple SingleResponses for a cert that have conflicting + // statuses, we use the following precedence rules: + // + // * revoked overrides good and unknown + // * good overrides unknown + if (input.Peek(static_cast(CertStatus::Good))) { + rv = der::ExpectTagAndEmptyValue(input, + static_cast(CertStatus::Good)); + if (rv != Success) { + return rv; + } + if (context.certStatus != CertStatus::Revoked) { + context.certStatus = CertStatus::Good; + } + } else if (input.Peek(static_cast(CertStatus::Revoked))) { + // We don't need any info from the RevokedInfo structure, so we don't even + // parse it. TODO: We should mention issues like this in the explanation of + // why we treat invalid OCSP responses equivalently to revoked for OCSP + // stapling. + rv = der::ExpectTagAndSkipValue(input, + static_cast(CertStatus::Revoked)); + if (rv != Success) { + return rv; + } + context.certStatus = CertStatus::Revoked; + } else { + rv = der::ExpectTagAndEmptyValue(input, + static_cast(CertStatus::Unknown)); + if (rv != Success) { + return rv; + } + } + + // http://tools.ietf.org/html/rfc6960#section-3.2 + // 5. The time at which the status being indicated is known to be + // correct (thisUpdate) is sufficiently recent; + // 6. When available, the time at or before which newer information will + // be available about the status of the certificate (nextUpdate) is + // greater than the current time. + + Time thisUpdate(Time::uninitialized); + rv = der::GeneralizedTime(input, thisUpdate); + if (rv != Success) { + return rv; + } + + static const uint64_t SLOP_SECONDS = Time::ONE_DAY_IN_SECONDS; + + Time timePlusSlop(context.time); + rv = timePlusSlop.AddSeconds(SLOP_SECONDS); + if (rv != Success) { + return rv; + } + if (thisUpdate > timePlusSlop) { + return Result::ERROR_OCSP_FUTURE_RESPONSE; + } + + Time notAfter(Time::uninitialized); + static const uint8_t NEXT_UPDATE_TAG = + der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 0; + if (input.Peek(NEXT_UPDATE_TAG)) { + Time nextUpdate(Time::uninitialized); + rv = der::Nested(input, NEXT_UPDATE_TAG, [&nextUpdate](Reader& r) { + return der::GeneralizedTime(r, nextUpdate); + }); + if (rv != Success) { + return rv; + } + + if (nextUpdate < thisUpdate) { + return Result::ERROR_OCSP_MALFORMED_RESPONSE; + } + notAfter = thisUpdate; + if (notAfter.AddSeconds(context.maxLifetimeInDays * + Time::ONE_DAY_IN_SECONDS) != Success) { + // This could only happen if we're dealing with times beyond the year + // 10,000AD. + return Result::ERROR_OCSP_FUTURE_RESPONSE; + } + if (nextUpdate <= notAfter) { + notAfter = nextUpdate; + } + } else { + // NSS requires all OCSP responses without a nextUpdate to be recent. + // Match that stricter behavior. + notAfter = thisUpdate; + if (notAfter.AddSeconds(Time::ONE_DAY_IN_SECONDS) != Success) { + // This could only happen if we're dealing with times beyond the year + // 10,000AD. + return Result::ERROR_OCSP_FUTURE_RESPONSE; + } + } + + // Add some slop to hopefully handle clock-skew. + Time notAfterPlusSlop(notAfter); + rv = notAfterPlusSlop.AddSeconds(SLOP_SECONDS); + if (rv != Success) { + // This could only happen if we're dealing with times beyond the year + // 10,000AD. + return Result::ERROR_OCSP_FUTURE_RESPONSE; + } + if (context.time > notAfterPlusSlop) { + context.expired = true; + } + + rv = der::OptionalExtensions( + input, + der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 1, + [&context](Reader& extnID, const Input& extnValue, bool critical, + /*out*/ bool& understood) { + return RememberSingleExtension(context, extnID, extnValue, critical, + understood); + }); + + if (rv != Success) { + return rv; + } + + if (context.thisUpdate) { + *context.thisUpdate = thisUpdate; + } + if (context.validThrough) { + *context.validThrough = notAfterPlusSlop; + } + + return Success; +} + +// CertID ::= SEQUENCE { +// hashAlgorithm AlgorithmIdentifier, +// issuerNameHash OCTET STRING, -- Hash of issuer's DN +// issuerKeyHash OCTET STRING, -- Hash of issuer's public key +// serialNumber CertificateSerialNumber } +static inline Result +CertID(Reader& input, const Context& context, /*out*/ bool& match) +{ + match = false; + + DigestAlgorithm hashAlgorithm; + Result rv = der::DigestAlgorithmIdentifier(input, hashAlgorithm); + if (rv != Success) { + if (rv == Result::ERROR_INVALID_ALGORITHM) { + // Skip entries that are hashed with algorithms we don't support. + input.SkipToEnd(); + return Success; + } + return rv; + } + + Input issuerNameHash; + rv = der::ExpectTagAndGetValue(input, der::OCTET_STRING, issuerNameHash); + if (rv != Success) { + return rv; + } + + Input issuerKeyHash; + rv = der::ExpectTagAndGetValue(input, der::OCTET_STRING, issuerKeyHash); + if (rv != Success) { + return rv; + } + + Input serialNumber; + rv = der::CertificateSerialNumber(input, serialNumber); + if (rv != Success) { + return rv; + } + + if (!InputsAreEqual(serialNumber, context.certID.serialNumber)) { + // This does not reference the certificate we're interested in. + // Consume the rest of the input and return successfully to + // potentially continue processing other responses. + input.SkipToEnd(); + return Success; + } + + // TODO: support SHA-2 hashes. + + if (hashAlgorithm != DigestAlgorithm::sha1) { + // Again, not interested in this response. Consume input, return success. + input.SkipToEnd(); + return Success; + } + + if (issuerNameHash.GetLength() != SHA1_DIGEST_LENGTH) { + return Result::ERROR_OCSP_MALFORMED_RESPONSE; + } + + // From http://tools.ietf.org/html/rfc6960#section-4.1.1: + // "The hash shall be calculated over the DER encoding of the + // issuer's name field in the certificate being checked." + uint8_t hashBuf[SHA1_DIGEST_LENGTH]; + rv = context.trustDomain.DigestBuf(context.certID.issuer, + DigestAlgorithm::sha1, hashBuf, + sizeof(hashBuf)); + if (rv != Success) { + return rv; + } + Input computed(hashBuf); + if (!InputsAreEqual(computed, issuerNameHash)) { + // Again, not interested in this response. Consume input, return success. + input.SkipToEnd(); + return Success; + } + + return MatchKeyHash(context.trustDomain, issuerKeyHash, + context.certID.issuerSubjectPublicKeyInfo, match); +} + +// From http://tools.ietf.org/html/rfc6960#section-4.1.1: +// "The hash shall be calculated over the value (excluding tag and length) of +// the subject public key field in the issuer's certificate." +// +// From http://tools.ietf.org/html/rfc6960#appendix-B.1: +// KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key +// -- (i.e., the SHA-1 hash of the value of the +// -- BIT STRING subjectPublicKey [excluding +// -- the tag, length, and number of unused +// -- bits] in the responder's certificate) +static Result +MatchKeyHash(TrustDomain& trustDomain, Input keyHash, + const Input subjectPublicKeyInfo, /*out*/ bool& match) +{ + if (keyHash.GetLength() != SHA1_DIGEST_LENGTH) { + return Result::ERROR_OCSP_MALFORMED_RESPONSE; + } + uint8_t hashBuf[SHA1_DIGEST_LENGTH]; + Result rv = KeyHash(trustDomain, subjectPublicKeyInfo, hashBuf, + sizeof hashBuf); + if (rv != Success) { + return rv; + } + Input computed(hashBuf); + match = InputsAreEqual(computed, keyHash); + return Success; +} + +// TODO(bug 966856): support SHA-2 hashes +Result +KeyHash(TrustDomain& trustDomain, const Input subjectPublicKeyInfo, + /*out*/ uint8_t* hashBuf, size_t hashBufSize) +{ + if (!hashBuf || hashBufSize != SHA1_DIGEST_LENGTH) { + return Result::FATAL_ERROR_LIBRARY_FAILURE; + } + + // RFC 5280 Section 4.1 + // + // SubjectPublicKeyInfo ::= SEQUENCE { + // algorithm AlgorithmIdentifier, + // subjectPublicKey BIT STRING } + + Reader spki; + Result rv = der::ExpectTagAndGetValueAtEnd(subjectPublicKeyInfo, + der::SEQUENCE, spki); + if (rv != Success) { + return rv; + } + + // Skip AlgorithmIdentifier + rv = der::ExpectTagAndSkipValue(spki, der::SEQUENCE); + if (rv != Success) { + return rv; + } + + Input subjectPublicKey; + rv = der::BitStringWithNoUnusedBits(spki, subjectPublicKey); + if (rv != Success) { + return rv; + } + rv = der::End(spki); + if (rv != Success) { + return rv; + } + + return trustDomain.DigestBuf(subjectPublicKey, DigestAlgorithm::sha1, + hashBuf, hashBufSize); +} + +Result +ExtensionNotUnderstood(Reader& /*extnID*/, Input /*extnValue*/, + bool /*critical*/, /*out*/ bool& understood) +{ + understood = false; + return Success; +} + +Result +RememberSingleExtension(Context& context, Reader& extnID, Input extnValue, + bool /*critical*/, /*out*/ bool& understood) +{ + understood = false; + + // SingleExtension for Signed Certificate Timestamp List. + // See Section 3.3 of RFC 6962. + // python DottedOIDToCode.py + // id_ocsp_singleExtensionSctList 1.3.6.1.4.1.11129.2.4.5 + static const uint8_t id_ocsp_singleExtensionSctList[] = { + 0x2b, 0x06, 0x01, 0x04, 0x01, 0xd6, 0x79, 0x02, 0x04, 0x05 + }; + + if (extnID.MatchRest(id_ocsp_singleExtensionSctList)) { + // Empty values are not allowed for this extension. Note that + // we assume this later, when checking if the extension was present. + if (extnValue.GetLength() == 0) { + return Result::ERROR_EXTENSION_VALUE_INVALID; + } + if (context.signedCertificateTimestamps.Init(extnValue) != Success) { + // Duplicate extension. + return Result::ERROR_EXTENSION_VALUE_INVALID; + } + understood = true; + } + + return Success; +} + +// 1. The certificate identified in a received response corresponds to +// the certificate that was identified in the corresponding request; +// 2. The signature on the response is valid; +// 3. The identity of the signer matches the intended recipient of the +// request; +// 4. The signer is currently authorized to provide a response for the +// certificate in question; +// 5. The time at which the status being indicated is known to be +// correct (thisUpdate) is sufficiently recent; +// 6. When available, the time at or before which newer information will +// be available about the status of the certificate (nextUpdate) is +// greater than the current time. +// +// Responses whose nextUpdate value is earlier than +// the local system time value SHOULD be considered unreliable. +// Responses whose thisUpdate time is later than the local system time +// SHOULD be considered unreliable. +// +// If nextUpdate is not set, the responder is indicating that newer +// revocation information is available all the time. +// +// http://tools.ietf.org/html/rfc5019#section-4 + +Result +CreateEncodedOCSPRequest(TrustDomain& trustDomain, const struct CertID& certID, + /*out*/ uint8_t (&out)[OCSP_REQUEST_MAX_LENGTH], + /*out*/ size_t& outLen) +{ + // We do not add any extensions to the request. + + // RFC 6960 says "An OCSP client MAY wish to specify the kinds of response + // types it understands. To do so, it SHOULD use an extension with the OID + // id-pkix-ocsp-response." This use of MAY and SHOULD is unclear. MSIE11 + // on Windows 8.1 does not include any extensions, whereas NSS has always + // included the id-pkix-ocsp-response extension. Avoiding the sending the + // extension is better for OCSP GET because it makes the request smaller, + // and thus more likely to fit within the 255 byte limit for OCSP GET that + // is specified in RFC 5019 Section 5. + + // Bug 966856: Add the id-pkix-ocsp-pref-sig-algs extension. + + // Since we don't know whether the OCSP responder supports anything other + // than SHA-1, we have no choice but to use SHA-1 for issuerNameHash and + // issuerKeyHash. + static const uint8_t hashAlgorithm[11] = { + 0x30, 0x09, // SEQUENCE + 0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A, // OBJECT IDENTIFIER id-sha1 + 0x05, 0x00, // NULL + }; + static const uint8_t hashLen = 160 / 8; + + static const unsigned int totalLenWithoutSerialNumberData + = 2 // OCSPRequest + + 2 // tbsRequest + + 2 // requestList + + 2 // Request + + 2 // reqCert (CertID) + + sizeof(hashAlgorithm) // hashAlgorithm + + 2 + hashLen // issuerNameHash + + 2 + hashLen // issuerKeyHash + + 2; // serialNumber (header) + + // The only way we could have a request this large is if the serialNumber was + // ridiculously and unreasonably large. RFC 5280 says "Conforming CAs MUST + // NOT use serialNumber values longer than 20 octets." With this restriction, + // we allow for some amount of non-conformance with that requirement while + // still ensuring we can encode the length values in the ASN.1 TLV structures + // in a single byte. + static_assert(totalLenWithoutSerialNumberData < OCSP_REQUEST_MAX_LENGTH, + "totalLenWithoutSerialNumberData too big"); + if (certID.serialNumber.GetLength() > + OCSP_REQUEST_MAX_LENGTH - totalLenWithoutSerialNumberData) { + return Result::ERROR_BAD_DER; + } + + outLen = totalLenWithoutSerialNumberData + certID.serialNumber.GetLength(); + + uint8_t totalLen = static_cast(outLen); + + uint8_t* d = out; + *d++ = 0x30; *d++ = totalLen - 2u; // OCSPRequest (SEQUENCE) + *d++ = 0x30; *d++ = totalLen - 4u; // tbsRequest (SEQUENCE) + *d++ = 0x30; *d++ = totalLen - 6u; // requestList (SEQUENCE OF) + *d++ = 0x30; *d++ = totalLen - 8u; // Request (SEQUENCE) + *d++ = 0x30; *d++ = totalLen - 10u; // reqCert (CertID SEQUENCE) + + // reqCert.hashAlgorithm + for (size_t i = 0; i < sizeof(hashAlgorithm); ++i) { + *d++ = hashAlgorithm[i]; + } + + // reqCert.issuerNameHash (OCTET STRING) + *d++ = 0x04; + *d++ = hashLen; + Result rv = trustDomain.DigestBuf(certID.issuer, DigestAlgorithm::sha1, d, + hashLen); + if (rv != Success) { + return rv; + } + d += hashLen; + + // reqCert.issuerKeyHash (OCTET STRING) + *d++ = 0x04; + *d++ = hashLen; + rv = KeyHash(trustDomain, certID.issuerSubjectPublicKeyInfo, d, hashLen); + if (rv != Success) { + return rv; + } + d += hashLen; + + // reqCert.serialNumber (INTEGER) + *d++ = 0x02; // INTEGER + *d++ = static_cast(certID.serialNumber.GetLength()); + Reader serialNumber(certID.serialNumber); + do { + rv = serialNumber.Read(*d); + if (rv != Success) { + return rv; + } + ++d; + } while (!serialNumber.AtEnd()); + + assert(d == out + totalLen); + + return Success; +} + +} } // namespace mozilla::pkix diff --git a/security/pkix/lib/pkixresult.cpp b/security/pkix/lib/pkixresult.cpp new file mode 100644 index 000000000..670642de8 --- /dev/null +++ b/security/pkix/lib/pkixresult.cpp @@ -0,0 +1,46 @@ +/*- *- 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/Result.h" +#include "pkixutil.h" + +namespace mozilla { namespace pkix { + +const char* +MapResultToName(Result result) +{ + switch (result) + { +#define MOZILLA_PKIX_MAP(mozilla_pkix_result, value, nss_result) \ + case Result::mozilla_pkix_result: return "Result::" #mozilla_pkix_result; + + MOZILLA_PKIX_MAP_LIST + +#undef MOZILLA_PKIX_MAP + + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } +} + +} } // namespace mozilla::pkix diff --git a/security/pkix/lib/pkixtime.cpp b/security/pkix/lib/pkixtime.cpp new file mode 100644 index 000000000..ace23dd41 --- /dev/null +++ b/security/pkix/lib/pkixtime.cpp @@ -0,0 +1,78 @@ +/* -*- 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 "pkix/Time.h" +#include "pkixutil.h" + +#ifdef WIN32 +#ifdef _MSC_VER +#pragma warning(push, 3) +#endif +#include "windows.h" +#ifdef _MSC_VER +#pragma warning(pop) +#endif +#else +#include "sys/time.h" +#endif + +namespace mozilla { namespace pkix { + +Time +Now() +{ + uint64_t seconds; + +#ifdef WIN32 + // "Contains a 64-bit value representing the number of 100-nanosecond + // intervals since January 1, 1601 (UTC)." + // - http://msdn.microsoft.com/en-us/library/windows/desktop/ms724284(v=vs.85).aspx + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + uint64_t ft64 = (static_cast(ft.dwHighDateTime) << 32) | + ft.dwLowDateTime; + seconds = (DaysBeforeYear(1601) * Time::ONE_DAY_IN_SECONDS) + + ft64 / (1000u * 1000u * 1000u / 100u); +#else + // "The gettimeofday() function shall obtain the current time, expressed as + // seconds and microseconds since the Epoch." + // - http://pubs.opengroup.org/onlinepubs/009695399/functions/gettimeofday.html + timeval tv; + (void) gettimeofday(&tv, nullptr); + seconds = (DaysBeforeYear(1970) * Time::ONE_DAY_IN_SECONDS) + + static_cast(tv.tv_sec); +#endif + + return TimeFromElapsedSecondsAD(seconds); +} + +Time +TimeFromEpochInSeconds(uint64_t secondsSinceEpoch) +{ + uint64_t seconds = (DaysBeforeYear(1970) * Time::ONE_DAY_IN_SECONDS) + + secondsSinceEpoch; + return TimeFromElapsedSecondsAD(seconds); +} + +} } // namespace mozilla::pkix diff --git a/security/pkix/lib/pkixutil.h b/security/pkix/lib/pkixutil.h new file mode 100644 index 000000000..31557ea81 --- /dev/null +++ b/security/pkix/lib/pkixutil.h @@ -0,0 +1,289 @@ +/* -*- 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. + */ + +#ifndef mozilla_pkix_pkixutil_h +#define mozilla_pkix_pkixutil_h + +#include "pkixder.h" + +namespace mozilla { namespace pkix { + +// During path building and verification, we build a linked list of BackCerts +// from the current cert toward the end-entity certificate. The linked list +// is used to verify properties that aren't local to the current certificate +// and/or the direct link between the current certificate and its issuer, +// such as name constraints. +// +// Each BackCert contains pointers to all the given certificate's extensions +// so that we can parse the extension block once and then process the +// extensions in an order that may be different than they appear in the cert. +class BackCert final +{ +public: + // certDER and childCert must be valid for the lifetime of BackCert. + BackCert(Input certDER, EndEntityOrCA endEntityOrCA, + const BackCert* childCert) + : der(certDER) + , endEntityOrCA(endEntityOrCA) + , childCert(childCert) + { + } + + Result Init(); + + const Input GetDER() const { return der; } + const der::SignedDataWithSignature& GetSignedData() const { + return signedData; + } + + der::Version GetVersion() const { return version; } + const Input GetSerialNumber() const { return serialNumber; } + const Input GetSignature() const { return signature; } + const Input GetIssuer() const { return issuer; } + // XXX: "validity" is a horrible name for the structure that holds + // notBefore & notAfter, but that is the name used in RFC 5280 and we use the + // RFC 5280 names for everything. + const Input GetValidity() const { return validity; } + const Input GetSubject() const { return subject; } + const Input GetSubjectPublicKeyInfo() const + { + return subjectPublicKeyInfo; + } + const Input* GetAuthorityInfoAccess() const + { + return MaybeInput(authorityInfoAccess); + } + const Input* GetBasicConstraints() const + { + return MaybeInput(basicConstraints); + } + const Input* GetCertificatePolicies() const + { + return MaybeInput(certificatePolicies); + } + const Input* GetExtKeyUsage() const + { + return MaybeInput(extKeyUsage); + } + const Input* GetKeyUsage() const + { + return MaybeInput(keyUsage); + } + const Input* GetInhibitAnyPolicy() const + { + return MaybeInput(inhibitAnyPolicy); + } + const Input* GetNameConstraints() const + { + return MaybeInput(nameConstraints); + } + const Input* GetSubjectAltName() const + { + return MaybeInput(subjectAltName); + } + const Input* GetRequiredTLSFeatures() const + { + return MaybeInput(requiredTLSFeatures); + } + const Input* GetSignedCertificateTimestamps() const + { + return MaybeInput(signedCertificateTimestamps); + } + +private: + const Input der; + +public: + const EndEntityOrCA endEntityOrCA; + BackCert const* const childCert; + +private: + // When parsing certificates in BackCert::Init, we don't accept empty + // extensions. Consequently, we don't have to store a distinction between + // empty extensions and extensions that weren't included. However, when + // *processing* extensions, we distinguish between whether an extension was + // included or not based on whetehr the GetXXX function for the extension + // returns nullptr. + static inline const Input* MaybeInput(const Input& item) + { + return item.GetLength() > 0 ? &item : nullptr; + } + + der::SignedDataWithSignature signedData; + + der::Version version; + Input serialNumber; + Input signature; + Input issuer; + // XXX: "validity" is a horrible name for the structure that holds + // notBefore & notAfter, but that is the name used in RFC 5280 and we use the + // RFC 5280 names for everything. + Input validity; + Input subject; + Input subjectPublicKeyInfo; + + Input authorityInfoAccess; + Input basicConstraints; + Input certificatePolicies; + Input extKeyUsage; + Input inhibitAnyPolicy; + Input keyUsage; + Input nameConstraints; + Input subjectAltName; + Input criticalNetscapeCertificateType; + Input requiredTLSFeatures; + Input signedCertificateTimestamps; // RFC 6962 (Certificate Transparency) + + Result RememberExtension(Reader& extnID, Input extnValue, bool critical, + /*out*/ bool& understood); + + BackCert(const BackCert&) = delete; + void operator=(const BackCert&) = delete; +}; + +class NonOwningDERArray final : public DERArray +{ +public: + NonOwningDERArray() + : numItems(0) + { + // we don't need to initialize the items array because we always check + // numItems before accessing i. + } + + size_t GetLength() const override { return numItems; } + + const Input* GetDER(size_t i) const override + { + return i < numItems ? &items[i] : nullptr; + } + + Result Append(Input der) + { + if (numItems >= MAX_LENGTH) { + return Result::FATAL_ERROR_INVALID_ARGS; + } + Result rv = items[numItems].Init(der); // structure assignment + if (rv != Success) { + return rv; + } + ++numItems; + return Success; + } + + // Public so we can static_assert on this. Keep in sync with MAX_SUBCA_COUNT. + static const size_t MAX_LENGTH = 8; +private: + Input items[MAX_LENGTH]; // avoids any heap allocations + size_t numItems; + + NonOwningDERArray(const NonOwningDERArray&) = delete; + void operator=(const NonOwningDERArray&) = delete; +}; + +// Extracts the SignedCertificateTimestampList structure which is encoded as an +// OCTET STRING within the X.509v3 / OCSP extensions (see RFC 6962 section 3.3). +Result +ExtractSignedCertificateTimestampListFromExtension(Input extnValue, + Input& sctList); + +inline unsigned int +DaysBeforeYear(unsigned int year) +{ + assert(year <= 9999); + return ((year - 1u) * 365u) + + ((year - 1u) / 4u) // leap years are every 4 years, + - ((year - 1u) / 100u) // except years divisible by 100, + + ((year - 1u) / 400u); // except years divisible by 400. +} + +static const size_t MAX_DIGEST_SIZE_IN_BYTES = 512 / 8; // sha-512 + +Result DigestSignedData(TrustDomain& trustDomain, + const der::SignedDataWithSignature& signedData, + /*out*/ uint8_t(&digestBuf)[MAX_DIGEST_SIZE_IN_BYTES], + /*out*/ der::PublicKeyAlgorithm& publicKeyAlg, + /*out*/ SignedDigest& signedDigest); + +Result VerifySignedDigest(TrustDomain& trustDomain, + der::PublicKeyAlgorithm publicKeyAlg, + const SignedDigest& signedDigest, + Input signerSubjectPublicKeyInfo); + +// Combines DigestSignedData and VerifySignedDigest +Result VerifySignedData(TrustDomain& trustDomain, + const der::SignedDataWithSignature& signedData, + Input signerSubjectPublicKeyInfo); + +// Extracts the key parameters from |subjectPublicKeyInfo|, invoking +// the relevant methods of |trustDomain|. +Result +CheckSubjectPublicKeyInfo(Input subjectPublicKeyInfo, TrustDomain& trustDomain, + EndEntityOrCA endEntityOrCA); + +// In a switch over an enum, sometimes some compilers are not satisfied that +// all control flow paths have been considered unless there is a default case. +// However, in our code, such a default case is almost always unreachable dead +// code. That can be particularly problematic when the compiler wants the code +// to choose a value, such as a return value, for the default case, but there's +// no appropriate "impossible case" value to choose. +// +// MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM accounts for this. Example: +// +// // In xy.cpp +// #include "xt.h" +// +// enum class XY { X, Y }; +// +// int func(XY xy) { +// switch (xy) { +// case XY::X: return 1; +// case XY::Y; return 2; +// MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM +// } +// } +#if defined(__clang__) +// Clang will warn if not all cases are covered (-Wswitch-enum) AND it will +// warn if a switch statement that covers every enum label has a default case +// (-W-covered-switch-default). Versions prior to 3.5 warned about unreachable +// code in such default cases (-Wunreachable-code) even when +// -W-covered-switch-default was disabled, but that changed in Clang 3.5. +#define MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM // empty +#elif defined(__GNUC__) +// GCC will warn if not all cases are covered (-Wswitch-enum). It does not +// assume that the default case is unreachable. +#define MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM \ + default: assert(false); __builtin_unreachable(); +#elif defined(_MSC_VER) +// MSVC will warn if not all cases are covered (C4061, level 4). It does not +// assume that the default case is unreachable. +#define MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM \ + default: assert(false); __assume(0); +#else +#error Unsupported compiler for MOZILLA_PKIX_UNREACHABLE_DEFAULT. +#endif + +} } // namespace mozilla::pkix + +#endif // mozilla_pkix_pkixutil_h diff --git a/security/pkix/lib/pkixverify.cpp b/security/pkix/lib/pkixverify.cpp new file mode 100644 index 000000000..16737e49d --- /dev/null +++ b/security/pkix/lib/pkixverify.cpp @@ -0,0 +1,103 @@ +/* -*- 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 2015 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 "pkixutil.h" + +namespace mozilla { namespace pkix { + +Result +DigestSignedData(TrustDomain& trustDomain, + const der::SignedDataWithSignature& signedData, + /*out*/ uint8_t(&digestBuf)[MAX_DIGEST_SIZE_IN_BYTES], + /*out*/ der::PublicKeyAlgorithm& publicKeyAlg, + /*out*/ SignedDigest& signedDigest) +{ + Reader signatureAlg(signedData.algorithm); + Result rv = der::SignatureAlgorithmIdentifierValue( + signatureAlg, publicKeyAlg, signedDigest.digestAlgorithm); + if (rv != Success) { + return rv; + } + if (!signatureAlg.AtEnd()) { + return Result::ERROR_BAD_DER; + } + + size_t digestLen; + switch (signedDigest.digestAlgorithm) { + case DigestAlgorithm::sha512: digestLen = 512 / 8; break; + case DigestAlgorithm::sha384: digestLen = 384 / 8; break; + case DigestAlgorithm::sha256: digestLen = 256 / 8; break; + case DigestAlgorithm::sha1: digestLen = 160 / 8; break; + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } + assert(digestLen <= sizeof(digestBuf)); + + rv = trustDomain.DigestBuf(signedData.data, signedDigest.digestAlgorithm, + digestBuf, digestLen); + if (rv != Success) { + return rv; + } + rv = signedDigest.digest.Init(digestBuf, digestLen); + if (rv != Success) { + return rv; + } + + return signedDigest.signature.Init(signedData.signature); +} + +Result +VerifySignedDigest(TrustDomain& trustDomain, + der::PublicKeyAlgorithm publicKeyAlg, + const SignedDigest& signedDigest, + Input signerSubjectPublicKeyInfo) +{ + switch (publicKeyAlg) { + case der::PublicKeyAlgorithm::ECDSA: + return trustDomain.VerifyECDSASignedDigest(signedDigest, + signerSubjectPublicKeyInfo); + case der::PublicKeyAlgorithm::RSA_PKCS1: + return trustDomain.VerifyRSAPKCS1SignedDigest(signedDigest, + signerSubjectPublicKeyInfo); + MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM + } +} + +Result +VerifySignedData(TrustDomain& trustDomain, + const der::SignedDataWithSignature& signedData, + Input signerSubjectPublicKeyInfo) +{ + uint8_t digestBuf[MAX_DIGEST_SIZE_IN_BYTES]; + der::PublicKeyAlgorithm publicKeyAlg; + SignedDigest signedDigest; + Result rv = DigestSignedData(trustDomain, signedData, digestBuf, + publicKeyAlg, signedDigest); + if (rv != Success) { + return rv; + } + return VerifySignedDigest(trustDomain, publicKeyAlg, signedDigest, + signerSubjectPublicKeyInfo); +} + +} } // namespace mozilla::pkix -- cgit v1.2.3