Skip to content

Commit

Permalink
Add initial OCSP signature checking.
Browse files Browse the repository at this point in the history
This is based on svaldez's work in https://codereview.chromium.org/1849773002/.

Bug: 649000,620005
Change-Id: I53eca60688eacb3e2b8472532e14af7c0af8e34e
Reviewed-on: https://chromium-review.googlesource.com/676324
Reviewed-by: Steven Valdez <svaldez@chromium.org>
Commit-Queue: Eric Roman <eroman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#503549}
  • Loading branch information
Eric Roman authored and Commit Bot committed Sep 21, 2017
1 parent fdbdfb6 commit dac9751
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 83 deletions.
6 changes: 3 additions & 3 deletions net/cert/cert_verify_proc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,9 @@ void BestEffortCheckOCSP(const std::string& raw_response,
return;
}

verify_result->revocation_status = CheckOCSPNoSignatureCheck(
raw_response, cert_der, issuer_der, base::Time::Now(),
/*skip_time_check=*/false, &verify_result->response_status);
verify_result->revocation_status =
CheckOCSP(raw_response, cert_der, issuer_der, base::Time::Now(),
/*skip_time_check=*/false, &verify_result->response_status);
}

// Records histograms indicating whether the certificate |cert|, which
Expand Down
303 changes: 235 additions & 68 deletions net/cert/internal/ocsp.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
#include <algorithm>

#include "base/time/time.h"
#include "net/cert/asn1_util.h"
#include "net/cert/internal/cert_errors.h"
#include "net/cert/internal/extended_key_usage.h"
#include "net/cert/internal/parsed_certificate.h"
#include "net/cert/internal/verify_name_match.h"
#include "net/cert/internal/verify_signed_data.h"
#include "net/cert/x509_util.h"
#include "net/der/encode_values.h"
#include "third_party/boringssl/src/include/openssl/digest.h"
Expand Down Expand Up @@ -440,10 +444,39 @@ bool VerifyHash(const EVP_MD* type,
return hash == der::Input(value_hash, value_hash_len);
}

// Extracts the bytes of the SubjectPublicKey bit string given an SPKI. That is
// to say, the value of subjectPublicKey without the leading unused bit
// count octet.
//
// Returns true on success and fills |*spk_tlv| with the result.
//
// SubjectPublicKeyInfo ::= SEQUENCE {
// algorithm AlgorithmIdentifier,
// subjectPublicKey BIT STRING
// }
bool GetSubjectPublicKeyBytes(const der::Input& spki_tlv, der::Input* spk_tlv) {
base::StringPiece spk_strpiece;
if (!asn1::ExtractSubjectPublicKeyFromSPKI(spki_tlv.AsStringPiece(),
&spk_strpiece)) {
return false;
}

// ExtractSubjectPublicKeyFromSPKI() includes the unused bit count. For this
// application, the unused bit count must be zero, and is not included in the
// result.
if (!spk_strpiece.starts_with("\0"))
return false;
spk_strpiece.remove_prefix(1);

*spk_tlv = der::Input(spk_strpiece);
return true;
}

// Checks the OCSPCertID |id| identifies |certificate|.
bool CheckCertIDMatchesCertificate(const OCSPCertID& id,
ParsedCertificate* certificate,
ParsedCertificate* issuer_certificate) {
bool CheckCertIDMatchesCertificate(
const OCSPCertID& id,
const ParsedCertificate* certificate,
const ParsedCertificate* issuer_certificate) {
const EVP_MD* type = nullptr;
switch (id.hash_algorithm) {
case DigestAlgorithm::Md2:
Expand All @@ -468,25 +501,10 @@ bool CheckCertIDMatchesCertificate(const OCSPCertID& id,
if (!VerifyHash(type, id.issuer_name_hash, certificate->tbs().issuer_tlv))
return false;

// TODO(eroman): This is largely duplicated with
// asn1::ExtractSubjectPublicKeyFromSPKI().
//
// SubjectPublicKeyInfo ::= SEQUENCE {
// algorithm AlgorithmIdentifier,
// subjectPublicKey BIT STRING
// }
der::Parser outer_parser(issuer_certificate->tbs().spki_tlv);
der::Parser spki_parser;
der::BitString key_bits;
if (!outer_parser.ReadSequence(&spki_parser))
return false;
if (outer_parser.HasMore())
der::Input key_tlv;
if (!GetSubjectPublicKeyBytes(issuer_certificate->tbs().spki_tlv, &key_tlv))
return false;
if (!spki_parser.SkipTag(der::kSequence))
return false;
if (!spki_parser.ReadBitString(&key_bits))
return false;
der::Input key_tlv = key_bits.bytes();

if (!VerifyHash(type, id.issuer_key_hash, key_tlv))
return false;

Expand All @@ -509,19 +527,197 @@ scoped_refptr<ParsedCertificate> ParseCertificate(base::StringPiece der) {
{}, &errors);
}

} // namespace
// Checks that the ResponderID |id| matches the certificate |cert| either
// by verifying the name matches that of the certificate or that the hash
// matches the certificate's public key hash (RFC 6960, 4.2.2.3).
WARN_UNUSED_RESULT bool CheckResponderIDMatchesCertificate(
const OCSPResponseData::ResponderID& id,
const ParsedCertificate* cert) {
switch (id.type) {
case OCSPResponseData::ResponderType::NAME: {
der::Input name_rdn;
der::Input cert_rdn;
if (!der::Parser(id.name).ReadTag(der::kSequence, &name_rdn) ||
!der::Parser(cert->tbs().subject_tlv)
.ReadTag(der::kSequence, &cert_rdn))
return false;
return VerifyNameMatch(name_rdn, cert_rdn);
}
case OCSPResponseData::ResponderType::KEY_HASH: {
der::Input key;
if (!GetSubjectPublicKeyBytes(cert->tbs().spki_tlv, &key))
return false;
return VerifyHash(EVP_sha1(), id.key_hash, key);
}
}

OCSPRevocationStatus CheckOCSPNoSignatureCheck(
base::StringPiece raw_response,
base::StringPiece certificate_der,
base::StringPiece issuer_certificate_der,
return false;
}

// Verifies that |responder_certificate| has been authority for OCSP signing,
// delegated to it by |issuer_certificate|.
//
// TODO(eroman): No revocation checks are done (see id-pkix-ocsp-nocheck in the
// spec). extension).
//
// TODO(eroman): Not all properties of the certificate are verified, only the
// signature and EKU. Can full RFC 5280 validation be used, or are there
// compatibility concerns?
WARN_UNUSED_RESULT bool VerifyAuthorizedResponderCert(
const ParsedCertificate* responder_certificate,
const ParsedCertificate* issuer_certificate) {
// The Authorized Responder must be directly signed by the issuer of the
// certificate being checked.
// TODO(eroman): Must check the signature algorithm against policy.
if (!VerifySignedData(responder_certificate->signature_algorithm(),
responder_certificate->tbs_certificate_tlv(),
responder_certificate->signature_value(),
issuer_certificate->tbs().spki_tlv)) {
return false;
}

// The Authorized Responder must include the value id-kp-OCSPSigning as
// part of the extended key usage extension.
if (!responder_certificate->has_extended_key_usage())
return false;
const std::vector<der::Input>& ekus =
responder_certificate->extended_key_usage();
if (std::find(ekus.begin(), ekus.end(), OCSPSigning()) == ekus.end())
return false;

return true;
}

WARN_UNUSED_RESULT bool VerifyOCSPResponseSignatureGivenCert(
const OCSPResponse& response,
const ParsedCertificate* cert) {
// TODO(eroman): Must check the signature algorithm against policy.
return VerifySignedData(*(response.signature_algorithm), response.data,
response.signature, cert->tbs().spki_tlv);
}

// Verifies that the OCSP response has a valid signature using
// |issuer_certificate|, or an authorized responder issued by
// |issuer_certificate| for OCSP signing.
WARN_UNUSED_RESULT bool VerifyOCSPResponseSignature(
const OCSPResponse& response,
const OCSPResponseData& response_data,
const ParsedCertificate* issuer_certificate) {
// In order to verify the OCSP signature, a valid responder matching the OCSP
// Responder ID must be located (RFC 6960, 4.2.2.2). The responder is allowed
// to be either the certificate issuer or a delegated authority directly
// signed by the issuer.
if (CheckResponderIDMatchesCertificate(response_data.responder_id,
issuer_certificate) &&
VerifyOCSPResponseSignatureGivenCert(response, issuer_certificate)) {
return true;
}

// Otherwise search through the provided certificates for the Authorized
// Responder. Want a certificate that:
// (1) Matches the OCSP Responder ID.
// (2) Has been given authority for OCSP signing by |issuer_certificate|.
// (3) Has signed the OCSP response using its public key.
for (const auto& responder_cert_tlv : response.certs) {
scoped_refptr<ParsedCertificate> cur_responder_certificate =
ParseCertificate(responder_cert_tlv.AsStringPiece());

// If failed parsing the certificate, keep looking.
if (!cur_responder_certificate)
continue;

// If the certificate doesn't match the OCSP's responder ID, keep looking.
if (!CheckResponderIDMatchesCertificate(response_data.responder_id,
cur_responder_certificate.get())) {
continue;
}

// If the certificate isn't a valid Authorized Responder certificate, keep
// looking.
if (!VerifyAuthorizedResponderCert(cur_responder_certificate.get(),
issuer_certificate)) {
continue;
}

// If the certificate signed this OCSP response, have found a match.
// Otherwise keep looking.
if (VerifyOCSPResponseSignatureGivenCert(response,
cur_responder_certificate.get())) {
return true;
}
}

// Failed to confirm the validity of the OCSP signature using any of the
// candidate certificates.
return false;
}

// Loops through the OCSPSingleResponses to find the best match for |cert|.
OCSPRevocationStatus GetRevocationStatusForCert(
const OCSPResponseData& response_data,
const ParsedCertificate* cert,
const ParsedCertificate* issuer_certificate,
const base::Time& verify_time,
bool skip_time_check,
OCSPVerifyResult::ResponseStatus* response_details) {
// The maximum age for an OCSP response, implemented as time since the
// |this_update| field in OCSPSingleResponse. Responses older than |max_age|
// will be considered invalid.
base::TimeDelta max_age = base::TimeDelta::FromDays(7);
OCSPRevocationStatus result = OCSPRevocationStatus::UNKNOWN;
*response_details = OCSPVerifyResult::NO_MATCHING_RESPONSE;

for (const auto& single_response_der : response_data.responses) {
// In the common case, there should only be one SingleResponse in the
// ResponseData (matching the certificate requested and used on this
// connection). However, it is possible for the OCSP responder to provide
// multiple responses for multiple certificates. Look through all the
// provided SingleResponses, and check to see if any match the
// certificate. A SingleResponse matches a certificate if it has the same
// serial number, issuer name (hash), and issuer public key (hash).
OCSPSingleResponse single_response;
if (!ParseOCSPSingleResponse(single_response_der, &single_response))
return OCSPRevocationStatus::UNKNOWN;
OCSPCertID cert_id;
if (!ParseOCSPCertID(single_response.cert_id_tlv, &cert_id))
return OCSPRevocationStatus::UNKNOWN;
if (!CheckCertIDMatchesCertificate(cert_id, cert, issuer_certificate))
continue;

// The SingleResponse matches the certificate, but may be out of date. Out
// of date responses are noted seperate from responses with mismatched
// serial numbers. If an OCSP responder provides both an up to date
// response and an expired response, the up to date response takes
// precedence (PROVIDED > INVALID_DATE).
if (!skip_time_check &&
!CheckOCSPDateValid(single_response, verify_time, max_age)) {
if (*response_details != OCSPVerifyResult::PROVIDED)
*response_details = OCSPVerifyResult::INVALID_DATE;
continue;
}

// In the case with multiple matching and up to date responses, keep only
// the strictest status (REVOKED > UNKNOWN > GOOD).
if (*response_details != OCSPVerifyResult::PROVIDED ||
result == OCSPRevocationStatus::GOOD ||
single_response.cert_status.status == OCSPRevocationStatus::REVOKED) {
result = single_response.cert_status.status;
}
*response_details = OCSPVerifyResult::PROVIDED;
}

return result;
}

} // namespace

OCSPRevocationStatus CheckOCSP(
base::StringPiece raw_response,
base::StringPiece certificate_der,
base::StringPiece issuer_certificate_der,
const base::Time& verify_time,
bool skip_time_check,
OCSPVerifyResult::ResponseStatus* response_details) {
*response_details = OCSPVerifyResult::NOT_CHECKED;

if (raw_response.empty()) {
Expand Down Expand Up @@ -586,50 +782,21 @@ OCSPRevocationStatus CheckOCSPNoSignatureCheck(
}
}

OCSPRevocationStatus result = OCSPRevocationStatus::UNKNOWN;
*response_details = OCSPVerifyResult::NO_MATCHING_RESPONSE;

for (const auto& single_response_der : response_data.responses) {
// In the common case, there should only be one SingleResponse in the
// ResponseData (matching the certificate requested and used on this
// connection). However, it is possible for the OCSP responder to provide
// multiple responses for multiple certificates. Look through all the
// provided SingleResponses, and check to see if any match the certificate.
// A SingleResponse matches a certificate if it has the same serial number,
// issuer name (hash), and issuer public key (hash).
OCSPSingleResponse single_response;
if (!ParseOCSPSingleResponse(single_response_der, &single_response))
return OCSPRevocationStatus::UNKNOWN;
OCSPCertID cert_id;
if (!ParseOCSPCertID(single_response.cert_id_tlv, &cert_id))
return OCSPRevocationStatus::UNKNOWN;
if (!CheckCertIDMatchesCertificate(cert_id, certificate.get(),
issuer_certificate.get()))
continue;

// The SingleResponse matches the certificate, but may be out of date. Out
// of date responses are noted seperate from responses with mismatched
// serial numbers. If an OCSP responder provides both an up to date response
// and an expired response, the up to date response takes precedence
// (PROVIDED > INVALID_DATE).
if (!skip_time_check &&
!CheckOCSPDateValid(single_response, verify_time, max_age)) {
if (*response_details != OCSPVerifyResult::PROVIDED)
*response_details = OCSPVerifyResult::INVALID_DATE;
continue;
}

// In the case with multiple matching and up to date responses, keep only
// the strictest status (REVOKED > UNKNOWN > GOOD).
if (*response_details != OCSPVerifyResult::PROVIDED ||
result == OCSPRevocationStatus::GOOD ||
single_response.cert_status.status == OCSPRevocationStatus::REVOKED) {
result = single_response.cert_status.status;
}
*response_details = OCSPVerifyResult::PROVIDED;
// Look through all of the OCSPSingleResponses for a match (based on CertID
// and time).
OCSPRevocationStatus status = GetRevocationStatusForCert(
response_data, certificate.get(), issuer_certificate.get(), verify_time,
skip_time_check, response_details);

// Check that the OCSP response has a valid signature. It must either be
// signed directly by the issuing certificate, or a valid authorized
// responder.
if (!VerifyOCSPResponseSignature(response, response_data,
issuer_certificate.get())) {
return OCSPRevocationStatus::UNKNOWN;
}

return result;
return status;
}

bool CheckOCSPDateValid(const OCSPSingleResponse& response,
Expand Down
5 changes: 1 addition & 4 deletions net/cert/internal/ocsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,6 @@ NET_EXPORT_PRIVATE bool ParseOCSPResponse(const der::Input& raw_tlv,
// Checks the revocation status of the certificate |certificate_der| by using
// the der-encoded |raw_response|.
//
// TODO(eroman): As the name implies this does not check the authenticity of the
// OCSP response.
//
// Returns GOOD if the OCSP response indicates the certificate is not revoked,
// REVOKED if it indicates it is revoked, or UNKNOWN for all other cases.
//
Expand All @@ -286,7 +283,7 @@ NET_EXPORT_PRIVATE bool ParseOCSPResponse(const der::Input& raw_tlv,
// * |response_details|: Additional details about failures.
// TODO(eroman): This is only being used for logging of Expect-Staple, can
// remove if that gets pulled out.
NET_EXPORT OCSPRevocationStatus CheckOCSPNoSignatureCheck(
NET_EXPORT OCSPRevocationStatus CheckOCSP(
base::StringPiece raw_response,
base::StringPiece certificate_der,
base::StringPiece issuer_certificate_der,
Expand Down
Loading

0 comments on commit dac9751

Please sign in to comment.