Skip to content

Commit

Permalink
Add support to verify a Crx from a string (#780)
Browse files Browse the repository at this point in the history
With in-memory updates the Crx package will still be in memory when it
needs be verified.

This change duplicates a lot of Chromium code but this should only be
temporary - please see the comments in crx_verifier.cc for more on this.

b/158043520

(cherry picked from commit b62c600)
  • Loading branch information
hlwarriner authored and anonymous1-me committed Jul 14, 2023
1 parent a738b62 commit e9041ba
Show file tree
Hide file tree
Showing 3 changed files with 569 additions and 2 deletions.
306 changes: 305 additions & 1 deletion components/crx_file/crx_verifier.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2017 The Chromium Authors. All rights reserved.
// Copyright 2017 The Cobalt Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

Expand Down Expand Up @@ -64,6 +64,23 @@ int ReadAndHashBuffer(uint8_t* buffer,
hash->Update(buffer, read);
return read;
}
#if defined(IN_MEMORY_UPDATES)
int ReadAndHashBufferFromString(uint8_t* buffer,
int length,
std::string::const_iterator* it,
crypto::SecureHash* hash) {
static_assert(sizeof(char) == sizeof(uint8_t), "Unsupported char size.");
memcpy(buffer, &(**it), length);
hash->Update(buffer, length);

// TODO(b/158043520): consider wrapping the CRX string in a type that keeps
// track of how much of the string has already been copied so that the string
// can be "read" like a file, without direct management of the iterator.
std::advance(*it, length);

return length;
}
#endif

// Returns UINT32_MAX in the case of an unexpected EOF or read error, else
// returns the read uint32.
Expand All @@ -74,6 +91,16 @@ uint32_t ReadAndHashLittleEndianUInt32(base::File* file,
return UINT32_MAX;
return buffer[3] << 24 | buffer[2] << 16 | buffer[1] << 8 | buffer[0];
}
#if defined(IN_MEMORY_UPDATES)
// Returns the read uint32.
uint32_t ReadAndHashLittleEndianUInt32FromString(
std::string::const_iterator* it,
crypto::SecureHash* hash) {
uint8_t buffer[4] = {};
ReadAndHashBufferFromString(buffer, sizeof(buffer), it, hash);
return buffer[3] << 24 | buffer[2] << 16 | buffer[1] << 8 | buffer[0];
}
#endif

// Read to the end of the file, updating the hash and all verifiers.
bool ReadHashAndVerifyArchive(base::File* file,
Expand All @@ -92,6 +119,34 @@ bool ReadHashAndVerifyArchive(base::File* file,
}
return len == 0;
}
#if defined(IN_MEMORY_UPDATES)
// Reads to the end of the string, updating the hash and all verifiers.
bool ReadHashAndVerifyArchiveFromString(const std::string& crx_str,
std::string::const_iterator* it,
crypto::SecureHash* hash,
const VerifierCollection& verifiers) {
int remaining_bytes = crx_str.end() - *it;

uint8_t buffer[1 << 12] = {};
while (remaining_bytes > 0) {
size_t len = remaining_bytes >= base::size(buffer) ? base::size(buffer)
: remaining_bytes;

ReadAndHashBufferFromString(buffer, len, it, hash);
remaining_bytes -= len;

for (auto& verifier : verifiers) {
verifier->VerifyUpdate(base::make_span(buffer, len));
}
}

for (auto& verifier : verifiers) {
if (!verifier->VerifyFinal())
return false;
}
return true;
}
#endif

// The remaining contents of a Crx3 file are [header-size][header][archive].
// [header] is an encoded protocol buffer and contains both a signed and
Expand Down Expand Up @@ -208,6 +263,129 @@ VerifierResult VerifyCrx3(
return VerifierResult::OK_FULL;
}

#if defined(IN_MEMORY_UPDATES)
// TODO(b/158043520): because we want to leave the Chromium code relatively
// intact and also want to support both in-memory and legacy updates in the same
// build while we develop the feature, there is for now a lot of duplicated code
// in some of these functions. For these functions that implement longer
// algorithms that are mostly agnostic about the Crx's representation we could
// likely share code by making them generic, but it probably makes more sense to
// live with the duplication for now. Once the feature is complete and we only
// need to support in-memory updates in builds, we can have one copy of the
// function that uses preprocessor conditions to enlist the appropriate helpers.
VerifierResult VerifyCrx3FromString(
const std::string& crx_str,
std::string::const_iterator* it,
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 =
ReadAndHashLittleEndianUInt32FromString(it, 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 (ReadAndHashBufferFromString(header_bytes.data(), header_size, it, 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 (!ReadHashAndVerifyArchiveFromString(crx_str, it, hash, verifiers))
return VerifierResult::ERROR_SIGNATURE_VERIFICATION_FAILED;

base::Base64Encode(public_key_bytes, public_key);
*crx_id = declared_crx_id;
return VerifierResult::OK_FULL;
}
#endif

VerifierResult VerifyCrx2(
base::File* file,
crypto::SecureHash* hash,
Expand Down Expand Up @@ -255,6 +433,56 @@ VerifierResult VerifyCrx2(
return VerifierResult::OK_FULL;
}

#if defined(IN_MEMORY_UPDATES)
VerifierResult VerifyCrx2FromString(
const std::string& crx_str,
std::string::const_iterator* it,
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 = ReadAndHashLittleEndianUInt32FromString(it, hash);
if (key_size > kMaxPublicKeySize)
return VerifierResult::ERROR_HEADER_INVALID;
const uint32_t sig_size = ReadAndHashLittleEndianUInt32FromString(it, hash);
if (sig_size > kMaxSignatureSize)
return VerifierResult::ERROR_HEADER_INVALID;
std::vector<uint8_t> key(key_size);
if (ReadAndHashBufferFromString(key.data(), key_size, it, 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 (ReadAndHashBufferFromString(sig.data(), sig_size, it, 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 (!ReadHashAndVerifyArchiveFromString(crx_str, it, 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;
}
#endif

} // namespace

VerifierResult Verify(
Expand Down Expand Up @@ -330,4 +558,80 @@ VerifierResult Verify(
return diff ? VerifierResult::OK_DELTA : VerifierResult::OK_FULL;
}

#if defined(IN_MEMORY_UPDATES)
VerifierResult Verify(
const std::string& crx_str,
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;

std::unique_ptr<crypto::SecureHash> file_hash =
crypto::SecureHash::Create(crypto::SecureHash::SHA256);

// Magic number.
bool diff = false;
if (!strncmp(crx_str.c_str(), kCrxDiffFileHeaderMagic,
kCrxFileHeaderMagicSize)) {
diff = true;
} else if (strncmp(crx_str.c_str(), kCrxFileHeaderMagic,
kCrxFileHeaderMagicSize)) {
return VerifierResult::ERROR_HEADER_INVALID;
}
file_hash->Update(crx_str.c_str(), kCrxFileHeaderMagicSize);

std::string::const_iterator it = crx_str.begin();
// Advance the iterator past the magic string embedded in the header.
std::advance(it, kCrxFileHeaderMagicSize);

// Version number.
const uint32_t version =
ReadAndHashLittleEndianUInt32FromString(&it, file_hash.get());
VerifierResult result;
if (version == 2)
SB_LOG(WARNING) << "The string 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 =
VerifyCrx2FromString(crx_str, &it, 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 = VerifyCrx3FromString(
crx_str, &it, 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;
}
#endif

} // namespace crx_file
13 changes: 12 additions & 1 deletion components/crx_file/crx_verifier.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2017 The Chromium Authors. All rights reserved.
// Copyright 2017 The Cobalt Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

Expand Down Expand Up @@ -51,6 +51,17 @@ VerifierResult Verify(
std::string* public_key,
std::string* crx_id);

#if defined(IN_MEMORY_UPDATES)
// An overload that instead verifies |crx_str| as a valid Crx.
VerifierResult Verify(
const std::string& crx_str,
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);
#endif

} // namespace crx_file

#endif // COMPONENTS_CRX_FILE_CRX_VERIFIER_H_
Loading

0 comments on commit e9041ba

Please sign in to comment.