| // Copyright 2017 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 "components/crx_file/crx_verifier.h" |
| |
| #include <cstring> |
| #include <iterator> |
| #include <memory> |
| #include <set> |
| #include <utility> |
| |
| #include "base/base64.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "components/crx_file/crx3.pb.h" |
| #include "components/crx_file/crx_file.h" |
| #include "components/crx_file/id_util.h" |
| #include "crypto/secure_hash.h" |
| #include "crypto/secure_util.h" |
| #include "crypto/sha2.h" |
| #include "crypto/signature_verifier.h" |
| |
| namespace crx_file { |
| |
| namespace { |
| |
| // The maximum size the Crx2 parser will tolerate for a public key. |
| constexpr uint32_t kMaxPublicKeySize = 1 << 16; |
| |
| // The maximum size the Crx2 parser will tolerate for a signature. |
| constexpr uint32_t kMaxSignatureSize = 1 << 16; |
| |
| // The maximum size the Crx3 parser will tolerate for a header. |
| constexpr uint32_t kMaxHeaderSize = 1 << 18; |
| |
| // The SHA256 hash of the DER SPKI "ecdsa_2017_public" Crx3 key. |
| constexpr uint8_t kPublisherKeyHash[] = { |
| 0x61, 0xf7, 0xf2, 0xa6, 0xbf, 0xcf, 0x74, 0xcd, 0x0b, 0xc1, 0xfe, |
| 0x24, 0x97, 0xcc, 0x9b, 0x04, 0x25, 0x4c, 0x65, 0x8f, 0x79, 0xf2, |
| 0x14, 0x53, 0x92, 0x86, 0x7e, 0xa8, 0x36, 0x63, 0x67, 0xcf}; |
| |
| // The SHA256 hash of the DER SPKI "ecdsa_2017_public" Crx3 test key. |
| constexpr uint8_t kPublisherTestKeyHash[] = { |
| 0x6c, 0x46, 0x41, 0x3b, 0x00, 0xd0, 0xfa, 0x0e, 0x72, 0xc8, 0xd2, |
| 0x5f, 0x64, 0xf3, 0xa6, 0x17, 0x03, 0x0d, 0xde, 0x21, 0x61, 0xbe, |
| 0xb7, 0x95, 0x91, 0x95, 0x83, 0x68, 0x12, 0xe9, 0x78, 0x1e}; |
| |
| using VerifierCollection = |
| std::vector<std::unique_ptr<crypto::SignatureVerifier>>; |
| using RepeatedProof = google::protobuf::RepeatedPtrField<AsymmetricKeyProof>; |
| |
| int ReadAndHashBuffer(uint8_t* buffer, |
| int length, |
| base::File* file, |
| crypto::SecureHash* hash) { |
| static_assert(sizeof(char) == sizeof(uint8_t), "Unsupported char size."); |
| int read = file->ReadAtCurrentPos(reinterpret_cast<char*>(buffer), length); |
| if (read > 0) |
| hash->Update(buffer, read); |
| return read; |
| } |
| |
| // Returns UINT32_MAX in the case of an unexpected EOF or read error, else |
| // returns the read uint32. |
| uint32_t ReadAndHashLittleEndianUInt32(base::File* file, |
| crypto::SecureHash* hash) { |
| uint8_t buffer[4] = {}; |
| if (ReadAndHashBuffer(buffer, 4, file, hash) != 4) |
| return UINT32_MAX; |
| return buffer[3] << 24 | buffer[2] << 16 | buffer[1] << 8 | buffer[0]; |
| } |
| |
| // Read to the end of the file, updating the hash and all verifiers. |
| bool ReadHashAndVerifyArchive(base::File* file, |
| crypto::SecureHash* hash, |
| const VerifierCollection& verifiers) { |
| uint8_t buffer[1 << 12] = {}; |
| size_t len = 0; |
| while ((len = ReadAndHashBuffer(buffer, base::size(buffer), file, hash)) > |
| 0) { |
| for (auto& verifier : verifiers) |
| verifier->VerifyUpdate(base::make_span(buffer, len)); |
| } |
| for (auto& verifier : verifiers) { |
| if (!verifier->VerifyFinal()) |
| return false; |
| } |
| return len == 0; |
| } |
| |
| // The remaining contents of a Crx3 file are [header-size][header][archive]. |
| // [header] is an encoded protocol buffer and contains both a signed and |
| // unsigned section. The unsigned section contains a set of key/signature pairs, |
| // and the signed section is the encoding of another protocol buffer. All |
| // signatures cover [prefix][signed-header-size][signed-header][archive]. |
| VerifierResult VerifyCrx3( |
| base::File* file, |
| crypto::SecureHash* hash, |
| const std::vector<std::vector<uint8_t>>& required_key_hashes, |
| std::string* public_key, |
| std::string* crx_id, |
| bool require_publisher_key, |
| bool accept_publisher_test_key) { |
| // Parse [header-size] and [header]. |
| const uint32_t header_size = ReadAndHashLittleEndianUInt32(file, hash); |
| if (header_size > kMaxHeaderSize) |
| return VerifierResult::ERROR_HEADER_INVALID; |
| std::vector<uint8_t> header_bytes(header_size); |
| // Assuming kMaxHeaderSize can fit in an int, the following cast is safe. |
| if (ReadAndHashBuffer(header_bytes.data(), header_size, file, hash) != |
| static_cast<int>(header_size)) |
| return VerifierResult::ERROR_HEADER_INVALID; |
| CrxFileHeader header; |
| if (!header.ParseFromArray(header_bytes.data(), header_size)) |
| return VerifierResult::ERROR_HEADER_INVALID; |
| |
| // Parse [signed-header]. |
| const std::string& signed_header_data_str = header.signed_header_data(); |
| SignedData signed_header_data; |
| if (!signed_header_data.ParseFromString(signed_header_data_str)) |
| return VerifierResult::ERROR_HEADER_INVALID; |
| const std::string& crx_id_encoded = signed_header_data.crx_id(); |
| const std::string declared_crx_id = id_util::GenerateIdFromHex( |
| base::HexEncode(crx_id_encoded.data(), crx_id_encoded.size())); |
| |
| // Create a little-endian representation of [signed-header-size]. |
| const int signed_header_size = signed_header_data_str.size(); |
| const uint8_t header_size_octets[] = { |
| static_cast<uint8_t>(signed_header_size), |
| static_cast<uint8_t>(signed_header_size >> 8), |
| static_cast<uint8_t>(signed_header_size >> 16), |
| static_cast<uint8_t>(signed_header_size >> 24)}; |
| |
| // Create a set of all required key hashes. |
| std::set<std::vector<uint8_t>> required_key_set(required_key_hashes.begin(), |
| required_key_hashes.end()); |
| |
| using ProofFetcher = const RepeatedProof& (CrxFileHeader::*)() const; |
| ProofFetcher rsa = &CrxFileHeader::sha256_with_rsa; |
| ProofFetcher ecdsa = &CrxFileHeader::sha256_with_ecdsa; |
| |
| std::string public_key_bytes; |
| VerifierCollection verifiers; |
| verifiers.reserve(header.sha256_with_rsa_size() + |
| header.sha256_with_ecdsa_size()); |
| const std::vector< |
| std::pair<ProofFetcher, crypto::SignatureVerifier::SignatureAlgorithm>> |
| proof_types = { |
| std::make_pair(rsa, crypto::SignatureVerifier::RSA_PKCS1_SHA256), |
| std::make_pair(ecdsa, crypto::SignatureVerifier::ECDSA_SHA256)}; |
| |
| std::vector<uint8_t> publisher_key(std::begin(kPublisherKeyHash), |
| std::end(kPublisherKeyHash)); |
| base::Optional<std::vector<uint8_t>> publisher_test_key; |
| if (accept_publisher_test_key) { |
| publisher_test_key.emplace(std::begin(kPublisherTestKeyHash), |
| std::end(kPublisherTestKeyHash)); |
| } |
| bool found_publisher_key = false; |
| |
| // Initialize all verifiers and update them with |
| // [prefix][signed-header-size][signed-header]. |
| // Clear any elements of required_key_set that are encountered, and watch for |
| // the developer key. |
| for (const auto& proof_type : proof_types) { |
| for (const auto& proof : (header.*proof_type.first)()) { |
| const std::string& key = proof.public_key(); |
| const std::string& sig = proof.signature(); |
| if (id_util::GenerateId(key) == declared_crx_id) |
| public_key_bytes = key; |
| std::vector<uint8_t> key_hash(crypto::kSHA256Length); |
| crypto::SHA256HashString(key, key_hash.data(), key_hash.size()); |
| required_key_set.erase(key_hash); |
| DCHECK_EQ(accept_publisher_test_key, publisher_test_key.has_value()); |
| found_publisher_key = |
| found_publisher_key || key_hash == publisher_key || |
| (accept_publisher_test_key && key_hash == *publisher_test_key); |
| auto v = std::make_unique<crypto::SignatureVerifier>(); |
| static_assert(sizeof(unsigned char) == sizeof(uint8_t), |
| "Unsupported char size."); |
| if (!v->VerifyInit(proof_type.second, |
| base::as_bytes(base::make_span(sig)), |
| base::as_bytes(base::make_span(key)))) |
| return VerifierResult::ERROR_SIGNATURE_INITIALIZATION_FAILED; |
| v->VerifyUpdate(kSignatureContext); |
| v->VerifyUpdate(header_size_octets); |
| v->VerifyUpdate(base::as_bytes(base::make_span(signed_header_data_str))); |
| verifiers.push_back(std::move(v)); |
| } |
| } |
| if (public_key_bytes.empty() || !required_key_set.empty()) |
| return VerifierResult::ERROR_REQUIRED_PROOF_MISSING; |
| |
| if (require_publisher_key && !found_publisher_key) |
| return VerifierResult::ERROR_REQUIRED_PROOF_MISSING; |
| |
| // Update and finalize the verifiers with [archive]. |
| if (!ReadHashAndVerifyArchive(file, hash, verifiers)) |
| return VerifierResult::ERROR_SIGNATURE_VERIFICATION_FAILED; |
| |
| base::Base64Encode(public_key_bytes, public_key); |
| *crx_id = declared_crx_id; |
| return VerifierResult::OK_FULL; |
| } |
| |
| VerifierResult VerifyCrx2( |
| base::File* file, |
| crypto::SecureHash* hash, |
| const std::vector<std::vector<uint8_t>>& required_key_hashes, |
| std::string* public_key, |
| std::string* crx_id) { |
| const uint32_t key_size = ReadAndHashLittleEndianUInt32(file, hash); |
| if (key_size > kMaxPublicKeySize) |
| return VerifierResult::ERROR_HEADER_INVALID; |
| const uint32_t sig_size = ReadAndHashLittleEndianUInt32(file, hash); |
| if (sig_size > kMaxSignatureSize) |
| return VerifierResult::ERROR_HEADER_INVALID; |
| std::vector<uint8_t> key(key_size); |
| if (ReadAndHashBuffer(key.data(), key_size, file, hash) != |
| static_cast<int>(key_size)) |
| return VerifierResult::ERROR_HEADER_INVALID; |
| for (const auto& expected_hash : required_key_hashes) { |
| // In practice we expect zero or one key_hashes_ for Crx2 files. |
| std::vector<uint8_t> hash(crypto::kSHA256Length); |
| std::unique_ptr<crypto::SecureHash> sha256 = |
| crypto::SecureHash::Create(crypto::SecureHash::SHA256); |
| sha256->Update(key.data(), key.size()); |
| sha256->Finish(hash.data(), hash.size()); |
| if (hash != expected_hash) |
| return VerifierResult::ERROR_REQUIRED_PROOF_MISSING; |
| } |
| |
| std::vector<uint8_t> sig(sig_size); |
| if (ReadAndHashBuffer(sig.data(), sig_size, file, hash) != |
| static_cast<int>(sig_size)) |
| return VerifierResult::ERROR_HEADER_INVALID; |
| std::vector<std::unique_ptr<crypto::SignatureVerifier>> verifiers; |
| verifiers.push_back(std::make_unique<crypto::SignatureVerifier>()); |
| if (!verifiers[0]->VerifyInit(crypto::SignatureVerifier::RSA_PKCS1_SHA1, sig, |
| key)) { |
| return VerifierResult::ERROR_SIGNATURE_INITIALIZATION_FAILED; |
| } |
| |
| if (!ReadHashAndVerifyArchive(file, hash, verifiers)) |
| return VerifierResult::ERROR_SIGNATURE_VERIFICATION_FAILED; |
| |
| const std::string public_key_bytes(key.begin(), key.end()); |
| base::Base64Encode(public_key_bytes, public_key); |
| *crx_id = id_util::GenerateId(public_key_bytes); |
| return VerifierResult::OK_FULL; |
| } |
| |
| } // namespace |
| |
| VerifierResult Verify( |
| const base::FilePath& crx_path, |
| const VerifierFormat& format, |
| const std::vector<std::vector<uint8_t>>& required_key_hashes, |
| const std::vector<uint8_t>& required_file_hash, |
| std::string* public_key, |
| std::string* crx_id) { |
| std::string public_key_local; |
| std::string crx_id_local; |
| base::File file(crx_path, base::File::FLAG_OPEN | base::File::FLAG_READ); |
| if (!file.IsValid()) |
| return VerifierResult::ERROR_FILE_NOT_READABLE; |
| |
| std::unique_ptr<crypto::SecureHash> file_hash = |
| crypto::SecureHash::Create(crypto::SecureHash::SHA256); |
| |
| // Magic number. |
| bool diff = false; |
| char buffer[kCrxFileHeaderMagicSize] = {}; |
| if (file.ReadAtCurrentPos(buffer, kCrxFileHeaderMagicSize) != |
| kCrxFileHeaderMagicSize) |
| return VerifierResult::ERROR_HEADER_INVALID; |
| if (!strncmp(buffer, kCrxDiffFileHeaderMagic, kCrxFileHeaderMagicSize)) |
| diff = true; |
| else if (strncmp(buffer, kCrxFileHeaderMagic, kCrxFileHeaderMagicSize)) |
| return VerifierResult::ERROR_HEADER_INVALID; |
| file_hash->Update(buffer, sizeof(buffer)); |
| |
| // Version number. |
| const uint32_t version = |
| ReadAndHashLittleEndianUInt32(&file, file_hash.get()); |
| VerifierResult result; |
| if (version == 2) |
| LOG(WARNING) << "File '" << crx_path |
| << "' is in CRX2 format, which is deprecated and will not be " |
| "supported in M78+"; |
| if (format == VerifierFormat::CRX2_OR_CRX3 && |
| (version == 2 || (diff && version == 0))) { |
| result = VerifyCrx2(&file, file_hash.get(), required_key_hashes, |
| &public_key_local, &crx_id_local); |
| } else if (version == 3) { |
| bool require_publisher_key = |
| format == VerifierFormat::CRX3_WITH_PUBLISHER_PROOF || |
| format == VerifierFormat::CRX3_WITH_TEST_PUBLISHER_PROOF; |
| result = |
| VerifyCrx3(&file, file_hash.get(), required_key_hashes, |
| &public_key_local, &crx_id_local, require_publisher_key, |
| format == VerifierFormat::CRX3_WITH_TEST_PUBLISHER_PROOF); |
| } else { |
| result = VerifierResult::ERROR_HEADER_INVALID; |
| } |
| if (result != VerifierResult::OK_FULL) |
| return result; |
| |
| // Finalize file hash. |
| uint8_t final_hash[crypto::kSHA256Length] = {}; |
| file_hash->Finish(final_hash, sizeof(final_hash)); |
| if (!required_file_hash.empty()) { |
| if (required_file_hash.size() != crypto::kSHA256Length) |
| return VerifierResult::ERROR_EXPECTED_HASH_INVALID; |
| if (!crypto::SecureMemEqual(final_hash, required_file_hash.data(), |
| crypto::kSHA256Length)) |
| return VerifierResult::ERROR_FILE_HASH_FAILED; |
| } |
| |
| // All is well. Set the out-params and return. |
| if (public_key) |
| *public_key = public_key_local; |
| if (crx_id) |
| *crx_id = crx_id_local; |
| return diff ? VerifierResult::OK_DELTA : VerifierResult::OK_FULL; |
| } |
| |
| } // namespace crx_file |