// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/cert/crl_set.h" #include "base/base64.h" #include "base/json/json_reader.h" #include "base/time/time.h" #include "base/trace_event/trace_event.h" #include "base/values.h" #include "crypto/sha2.h" #include "net/base/trace_constants.h" #include "third_party/boringssl/src/include/openssl/bytestring.h" #include "third_party/boringssl/src/include/openssl/mem.h" namespace net { namespace { // CRLSet format: // // uint16le header_len // byte[header_len] header_bytes // repeated { // byte[32] parent_spki_sha256 // uint32le num_serials // [num_serials] { // uint8_t serial_length; // byte[serial_length] serial; // } // // header_bytes consists of a JSON dictionary with the following keys: // Version (int): currently 0 // ContentType (string): "CRLSet" or "CRLSetDelta" (magic value) // DeltaFrom (int32_t): if this is a delta update (see below), then this // contains the sequence number of the base CRLSet. // Sequence (int32_t): the monotonic sequence number of this CRL set. // // ReadHeader reads the header (including length prefix) from |data| and // updates |data| to remove the header on return. Caller takes ownership of the // returned pointer. base::DictionaryValue* ReadHeader(base::StringPiece* data) { uint16_t header_len; if (data->size() < sizeof(header_len)) return nullptr; // Assumes little-endian. memcpy(&header_len, data->data(), sizeof(header_len)); data->remove_prefix(sizeof(header_len)); if (data->size() < header_len) return nullptr; const base::StringPiece header_bytes(data->data(), header_len); data->remove_prefix(header_len); std::unique_ptr header = base::JSONReader::Read(header_bytes, base::JSON_ALLOW_TRAILING_COMMAS); if (header.get() == nullptr) return nullptr; if (!header->is_dict()) return nullptr; return static_cast(header.release()); } // kCurrentFileVersion is the version of the CRLSet file format that we // currently implement. static const int kCurrentFileVersion = 0; bool ReadCRL(base::StringPiece* data, std::string* out_parent_spki_hash, std::vector* out_serials) { if (data->size() < crypto::kSHA256Length) return false; out_parent_spki_hash->assign(data->data(), crypto::kSHA256Length); data->remove_prefix(crypto::kSHA256Length); uint32_t num_serials; if (data->size() < sizeof(num_serials)) return false; // Assumes little endian. memcpy(&num_serials, data->data(), sizeof(num_serials)); data->remove_prefix(sizeof(num_serials)); if (num_serials > 32 * 1024 * 1024) // Sanity check. return false; out_serials->reserve(num_serials); for (uint32_t i = 0; i < num_serials; ++i) { if (data->size() < sizeof(uint8_t)) return false; uint8_t serial_length = data->data()[0]; data->remove_prefix(sizeof(uint8_t)); if (data->size() < serial_length) return false; out_serials->push_back(std::string()); out_serials->back().assign(data->data(), serial_length); data->remove_prefix(serial_length); } return true; } // CopyHashListFromHeader parses a list of base64-encoded, SHA-256 hashes from // the given |key| in |header_dict| and sets |*out| to the decoded values. It's // not an error if |key| is not found in |header_dict|. bool CopyHashListFromHeader(base::DictionaryValue* header_dict, const char* key, std::vector* out) { base::ListValue* list = nullptr; if (!header_dict->GetList(key, &list)) { // Hash lists are optional so it's not an error if not present. return true; } out->clear(); out->reserve(list->GetSize()); std::string sha256_base64; for (size_t i = 0; i < list->GetSize(); ++i) { sha256_base64.clear(); if (!list->GetString(i, &sha256_base64)) return false; out->push_back(std::string()); if (!base::Base64Decode(sha256_base64, &out->back())) { out->pop_back(); return false; } } return true; } // CopyHashToHashesMapFromHeader parse a map from base64-encoded, SHA-256 // hashes to lists of the same, from the given |key| in |header_dict|. It // copies the map data into |out| (after base64-decoding). bool CopyHashToHashesMapFromHeader( base::DictionaryValue* header_dict, const char* key, std::unordered_map>* out) { out->clear(); base::Value* const dict = header_dict->FindKeyOfType(key, base::Value::Type::DICTIONARY); if (dict == nullptr) { // Maps are optional so it's not an error if not present. return true; } for (const auto& i : dict->DictItems()) { if (!i.second.is_list()) { return false; } std::vector allowed_spkis; for (const auto& j : i.second.GetList()) { allowed_spkis.push_back(std::string()); if (!j.is_string() || !base::Base64Decode(j.GetString(), &allowed_spkis.back())) { return false; } } std::string subject_hash; if (!base::Base64Decode(i.first, &subject_hash)) { return false; } (*out)[subject_hash] = allowed_spkis; } return true; } } // namespace CRLSet::CRLSet() : sequence_(0), not_after_(0) { } CRLSet::~CRLSet() = default; // static bool CRLSet::Parse(base::StringPiece data, scoped_refptr* out_crl_set) { TRACE_EVENT0(kNetTracingCategory, "CRLSet::Parse"); // Other parts of Chrome assume that we're little endian, so we don't lose // anything by doing this. #if defined(__BYTE_ORDER) // Linux check static_assert(__BYTE_ORDER == __LITTLE_ENDIAN, "assumes little endian"); #elif defined(__BIG_ENDIAN__) // Mac check #error assumes little endian #endif std::unique_ptr header_dict(ReadHeader(&data)); if (!header_dict.get()) return false; std::string contents; if (!header_dict->GetString("ContentType", &contents)) return false; if (contents != "CRLSet") return false; int version; if (!header_dict->GetInteger("Version", &version) || version != kCurrentFileVersion) { return false; } int sequence; if (!header_dict->GetInteger("Sequence", &sequence)) return false; double not_after; if (!header_dict->GetDouble("NotAfter", ¬_after)) { // NotAfter is optional for now. not_after = 0; } if (not_after < 0) return false; scoped_refptr crl_set(new CRLSet()); crl_set->sequence_ = static_cast(sequence); crl_set->not_after_ = static_cast(not_after); crl_set->crls_.reserve(64); // Value observed experimentally. for (size_t crl_index = 0; !data.empty(); crl_index++) { std::string spki_hash; std::vector blocked_serials; if (!ReadCRL(&data, &spki_hash, &blocked_serials)) { return false; } crl_set->crls_[std::move(spki_hash)] = std::move(blocked_serials); } if (!CopyHashListFromHeader(header_dict.get(), "BlockedSPKIs", &crl_set->blocked_spkis_) || !CopyHashToHashesMapFromHeader(header_dict.get(), "LimitedSubjects", &crl_set->limited_subjects_)) { return false; } *out_crl_set = std::move(crl_set); return true; } CRLSet::Result CRLSet::CheckSPKI(const base::StringPiece& spki_hash) const { for (std::vector::const_iterator i = blocked_spkis_.begin(); i != blocked_spkis_.end(); ++i) { if (spki_hash.size() == i->size() && memcmp(spki_hash.data(), i->data(), i->size()) == 0) { return REVOKED; } } return GOOD; } CRLSet::Result CRLSet::CheckSubject(const base::StringPiece& encoded_subject, const base::StringPiece& spki_hash) const { const std::string digest(crypto::SHA256HashString(encoded_subject)); const auto i = limited_subjects_.find(digest); if (i == limited_subjects_.end()) { return GOOD; } for (const auto& j : i->second) { if (spki_hash == j) { return GOOD; } } return REVOKED; } CRLSet::Result CRLSet::CheckSerial( const base::StringPiece& serial_number, const base::StringPiece& issuer_spki_hash) const { base::StringPiece serial(serial_number); if (!serial.empty() && (serial[0] & 0x80) != 0) { // This serial number is negative but the process which generates CRL sets // will reject any certificates with negative serial numbers as invalid. return UNKNOWN; } // Remove any leading zero bytes. while (serial.size() > 1 && serial[0] == 0x00) serial.remove_prefix(1); auto it = crls_.find(issuer_spki_hash.as_string()); if (it == crls_.end()) return UNKNOWN; for (const auto& issuer_serial : it->second) { if (issuer_serial == serial) return REVOKED; } return GOOD; } bool CRLSet::IsExpired() const { if (not_after_ == 0) return false; uint64_t now = base::Time::Now().ToTimeT(); return now > not_after_; } uint32_t CRLSet::sequence() const { return sequence_; } const CRLSet::CRLList& CRLSet::CrlsForTesting() const { return crls_; } // static scoped_refptr CRLSet::EmptyCRLSetForTesting() { return ForTesting(false, nullptr, "", "", {}); } // static scoped_refptr CRLSet::ExpiredCRLSetForTesting() { return ForTesting(true, nullptr, "", "", {}); } // static scoped_refptr CRLSet::ForTesting( bool is_expired, const SHA256HashValue* issuer_spki, const std::string& serial_number, const std::string common_name, const std::vector acceptable_spki_hashes_for_cn) { std::string subject_hash; if (!common_name.empty()) { CBB cbb, top_level, set, inner_seq, oid, cn; uint8_t* x501_data; size_t x501_len; static const uint8_t kCommonNameOID[] = {0x55, 0x04, 0x03}; // 2.5.4.3 CBB_zero(&cbb); if (!CBB_init(&cbb, 32) || !CBB_add_asn1(&cbb, &top_level, CBS_ASN1_SEQUENCE) || !CBB_add_asn1(&top_level, &set, CBS_ASN1_SET) || !CBB_add_asn1(&set, &inner_seq, CBS_ASN1_SEQUENCE) || !CBB_add_asn1(&inner_seq, &oid, CBS_ASN1_OBJECT) || !CBB_add_bytes(&oid, kCommonNameOID, sizeof(kCommonNameOID)) || !CBB_add_asn1(&inner_seq, &cn, CBS_ASN1_PRINTABLESTRING) || !CBB_add_bytes(&cn, reinterpret_cast(common_name.data()), common_name.size()) || !CBB_finish(&cbb, &x501_data, &x501_len)) { CBB_cleanup(&cbb); return nullptr; } subject_hash.assign(crypto::SHA256HashString( base::StringPiece(reinterpret_cast(x501_data), x501_len))); OPENSSL_free(x501_data); } scoped_refptr crl_set(new CRLSet); crl_set->sequence_ = 0; if (is_expired) crl_set->not_after_ = 1; if (issuer_spki) { const std::string spki(reinterpret_cast(issuer_spki->data), sizeof(issuer_spki->data)); std::vector serials; if (!serial_number.empty()) serials.push_back(serial_number); crl_set->crls_.emplace(std::move(spki), std::move(serials)); } if (!subject_hash.empty()) crl_set->limited_subjects_[subject_hash] = acceptable_spki_hashes_for_cn; return crl_set; } } // namespace net