| /* -*- 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 <limits> |
| |
| #include "mozpkix/pkix.h" |
| #include "mozpkix/pkixcheck.h" |
| #include "mozpkix/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& aTrustDomain, const CertID& aCertID, Time aTime, |
| uint16_t aMaxLifetimeInDays, /*optional out*/ Time* aThisUpdate, |
| /*optional out*/ Time* aValidThrough) |
| : trustDomain(aTrustDomain) |
| , certID(aCertID) |
| , time(aTime) |
| , maxLifetimeInDays(aMaxLifetimeInDays) |
| , certStatus(CertStatus::Unknown) |
| , thisUpdate(aThisUpdate) |
| , validThrough(aValidThrough) |
| , 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); |
| // It is convention to name the function after the part of the data structure |
| // we're parsing from the RFC (e.g. OCSPResponse, ResponseBytes). |
| // But since we also have a C++ type called CertID, this function doesn't |
| // follow the convention to prevent shadowing. |
| static inline Result MatchCertID(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 nestedRv = |
| der::ExpectTagAndGetTLV(certsDER, der::SEQUENCE, cert); |
| if (nestedRv != Success) { |
| return nestedRv; |
| } |
| nestedRv = certs.Append(cert); |
| if (nestedRv != 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<uint8_t>(ResponderIDType::byName)) |
| ? ResponderIDType::byName |
| : ResponderIDType::byKey; |
| rv = der::ExpectTagAndGetValue(input, static_cast<uint8_t>(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 MatchCertID(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<uint8_t>(CertStatus::Good))) { |
| rv = der::ExpectTagAndEmptyValue(input, |
| static_cast<uint8_t>(CertStatus::Good)); |
| if (rv != Success) { |
| return rv; |
| } |
| if (context.certStatus != CertStatus::Revoked) { |
| context.certStatus = CertStatus::Good; |
| } |
| } else if (input.Peek(static_cast<uint8_t>(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<uint8_t>(CertStatus::Revoked)); |
| if (rv != Success) { |
| return rv; |
| } |
| context.certStatus = CertStatus::Revoked; |
| } else { |
| rv = der::ExpectTagAndEmptyValue(input, |
| static_cast<uint8_t>(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 |
| MatchCertID(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<uint8_t>(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 (const uint8_t hashAlgorithmByte : hashAlgorithm) { |
| *d++ = hashAlgorithmByte; |
| } |
| |
| // 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<uint8_t>(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 |