// Copyright 2014 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 "extensions/browser/verified_contents.h" #include #include #include "base/base64url.h" #include "base/files/file_util.h" #include "base/json/json_reader.h" #include "base/memory/ptr_util.h" #include "base/metrics/histogram_macros.h" #include "base/strings/string_util.h" #include "base/timer/elapsed_timer.h" #include "base/values.h" #include "build/build_config.h" #include "components/crx_file/id_util.h" #include "crypto/signature_verifier.h" #include "extensions/common/extension.h" using base::DictionaryValue; using base::ListValue; using base::Value; namespace { const char kBlockSizeKey[] = "block_size"; const char kContentHashesKey[] = "content_hashes"; const char kDescriptionKey[] = "description"; const char kFilesKey[] = "files"; const char kFormatKey[] = "format"; const char kHashBlockSizeKey[] = "hash_block_size"; const char kHeaderKidKey[] = "header.kid"; const char kItemIdKey[] = "item_id"; const char kItemVersionKey[] = "item_version"; const char kPathKey[] = "path"; const char kPayloadKey[] = "payload"; const char kProtectedKey[] = "protected"; const char kRootHashKey[] = "root_hash"; const char kSignatureKey[] = "signature"; const char kSignaturesKey[] = "signatures"; const char kSignedContentKey[] = "signed_content"; const char kTreeHashPerFile[] = "treehash per file"; const char kTreeHash[] = "treehash"; const char kWebstoreKId[] = "webstore"; // Helper function to iterate over a list of dictionaries, returning the // dictionary that has |key| -> |value| in it, if any, or NULL. const DictionaryValue* FindDictionaryWithValue(const ListValue* list, const std::string& key, const std::string& value) { for (const auto& i : *list) { const DictionaryValue* dictionary; if (!i.GetAsDictionary(&dictionary)) continue; std::string found_value; if (dictionary->GetString(key, &found_value) && found_value == value) return dictionary; } return NULL; } // Helper to record UMA for results of initializing verified_contents.json file. // TODO(lazyboy): Merge this with ScopedUMARecorder in computed_hashes.cc. class ScopedUMARecorder { public: ScopedUMARecorder() = default; ~ScopedUMARecorder() { if (recorded_) return; RecordImpl(false); } void RecordSuccess() { recorded_ = true; RecordImpl(true); } private: void RecordImpl(bool success) { if (success) { UMA_HISTOGRAM_TIMES( "Extensions.ContentVerification.VerifiedContentsInitTime", timer_.Elapsed()); } UMA_HISTOGRAM_BOOLEAN( "Extensions.ContentVerification.VerifiedContentsInitResult", success); } private: base::ElapsedTimer timer_; bool recorded_ = false; DISALLOW_COPY_AND_ASSIGN(ScopedUMARecorder); }; #if defined(OS_WIN) // Returns true if |path| ends with (.| )+. // |out_path| will contain "." and/or " " suffix removed from |path|. bool TrimDotSpaceSuffix(const base::FilePath::StringType& path, base::FilePath::StringType* out_path) { base::FilePath::StringType::size_type trim_pos = path.find_last_not_of(FILE_PATH_LITERAL(". ")); if (trim_pos == base::FilePath::StringType::npos) return false; *out_path = path.substr(0, trim_pos + 1); return true; } #endif // defined(OS_WIN) } // namespace namespace extensions { VerifiedContents::VerifiedContents(base::span public_key) : public_key_(public_key), valid_signature_(false), // Guilty until proven innocent. block_size_(0) {} VerifiedContents::~VerifiedContents() { } // The format of the payload json is: // { // "item_id": "", // "item_version": "", // "content_hashes": [ // { // "block_size": 4096, // "hash_block_size": 4096, // "format": "treehash", // "files": [ // { // "path": "foo/bar", // "root_hash": "" // }, // ... // ] // } // ] // } // static. std::unique_ptr VerifiedContents::Create( base::span public_key, const base::FilePath& path) { ScopedUMARecorder uma_recorder; // Note: VerifiedContents constructor is private. auto verified_contents = base::WrapUnique(new VerifiedContents(public_key)); std::string payload; if (!verified_contents->GetPayload(path, &payload)) return nullptr; std::unique_ptr value(base::JSONReader::Read(payload)); if (!value.get() || !value->is_dict()) return nullptr; DictionaryValue* dictionary = static_cast(value.get()); std::string item_id; if (!dictionary->GetString(kItemIdKey, &item_id) || !crx_file::id_util::IdIsValid(item_id)) { return nullptr; } verified_contents->extension_id_ = item_id; std::string version_string; if (!dictionary->GetString(kItemVersionKey, &version_string)) return nullptr; verified_contents->version_ = base::Version(version_string); if (!verified_contents->version_.IsValid()) return nullptr; ListValue* hashes_list = nullptr; if (!dictionary->GetList(kContentHashesKey, &hashes_list)) return nullptr; for (size_t i = 0; i < hashes_list->GetSize(); i++) { DictionaryValue* hashes = nullptr; if (!hashes_list->GetDictionary(i, &hashes)) return nullptr; std::string format; if (!hashes->GetString(kFormatKey, &format) || format != kTreeHash) continue; int block_size = 0; int hash_block_size = 0; if (!hashes->GetInteger(kBlockSizeKey, &block_size) || !hashes->GetInteger(kHashBlockSizeKey, &hash_block_size)) { return nullptr; } verified_contents->block_size_ = block_size; // We don't support using a different block_size and hash_block_size at // the moment. if (verified_contents->block_size_ != hash_block_size) return nullptr; ListValue* files = nullptr; if (!hashes->GetList(kFilesKey, &files)) return nullptr; for (size_t j = 0; j < files->GetSize(); ++j) { DictionaryValue* data = nullptr; if (!files->GetDictionary(j, &data)) return nullptr; std::string file_path_string; std::string encoded_root_hash; std::string root_hash; if (!data->GetString(kPathKey, &file_path_string) || !base::IsStringUTF8(file_path_string) || !data->GetString(kRootHashKey, &encoded_root_hash) || !base::Base64UrlDecode(encoded_root_hash, base::Base64UrlDecodePolicy::IGNORE_PADDING, &root_hash)) { return nullptr; } base::FilePath file_path = base::FilePath::FromUTF8Unsafe(file_path_string); base::FilePath::StringType lowercase_file_path = base::ToLowerASCII(file_path.value()); auto i = verified_contents->root_hashes_.insert( std::make_pair(lowercase_file_path, std::string())); i->second.swap(root_hash); #if defined(OS_WIN) // Additionally store a canonicalized filename without (.| )+ suffix, so // that any filename with (.| )+ suffix can be matched later, see // HasTreeHashRoot() and TreeHashRootEquals(). base::FilePath::StringType trimmed_path; if (TrimDotSpaceSuffix(lowercase_file_path, &trimmed_path)) verified_contents->root_hashes_.insert( std::make_pair(trimmed_path, i->second)); #endif // defined(OS_WIN) } break; } uma_recorder.RecordSuccess(); return verified_contents; } bool VerifiedContents::HasTreeHashRoot( const base::FilePath& relative_path) const { base::FilePath::StringType path = base::ToLowerASCII( relative_path.NormalizePathSeparatorsTo('/').value()); if (base::ContainsKey(root_hashes_, path)) return true; #if defined(OS_WIN) base::FilePath::StringType trimmed_path; if (TrimDotSpaceSuffix(path, &trimmed_path)) return base::ContainsKey(root_hashes_, trimmed_path); #endif // defined(OS_WIN) return false; } bool VerifiedContents::TreeHashRootEquals(const base::FilePath& relative_path, const std::string& expected) const { base::FilePath::StringType normalized_relative_path = base::ToLowerASCII(relative_path.NormalizePathSeparatorsTo('/').value()); if (TreeHashRootEqualsImpl(normalized_relative_path, expected)) return true; #if defined(OS_WIN) base::FilePath::StringType trimmed_relative_path; if (TrimDotSpaceSuffix(normalized_relative_path, &trimmed_relative_path)) return TreeHashRootEqualsImpl(trimmed_relative_path, expected); #endif // defined(OS_WIN) return false; } // We're loosely following the "JSON Web Signature" draft spec for signing // a JSON payload: // // http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-26 // // The idea is that you have some JSON that you want to sign, so you // base64-encode that and put it as the "payload" field in a containing // dictionary. There might be signatures of it done with multiple // algorithms/parameters, so the payload is followed by a list of one or more // signature sections. Each signature section specifies the // algorithm/parameters in a JSON object which is base64url encoded into one // string and put into a "protected" field in the signature. Then the encoded // "payload" and "protected" strings are concatenated with a "." in between // them and those bytes are signed and the resulting signature is base64url // encoded and placed in the "signature" field. To allow for extensibility, we // wrap this, so we can include additional kinds of payloads in the future. E.g. // [ // { // "description": "treehash per file", // "signed_content": { // "payload": "", // "signatures": [ // { // "protected": "", // "header": { // // } // "signature": // "" // }, // ... ... // ] // } // } // ] // There might be both a signature generated with a webstore private key and a // signature generated with the extension's private key - for now we only // verify the webstore one (since the id is in the payload, so we can trust // that it is for a given extension), but in the future we may validate using // the extension's key too (eg for non-webstore hosted extensions such as // enterprise installs). bool VerifiedContents::GetPayload(const base::FilePath& path, std::string* payload) { std::string contents; if (!base::ReadFileToString(path, &contents)) return false; std::unique_ptr value(base::JSONReader::Read(contents)); if (!value.get() || !value->is_list()) return false; ListValue* top_list = static_cast(value.get()); // Find the "treehash per file" signed content, e.g. // [ // { // "description": "treehash per file", // "signed_content": { // "signatures": [ ... ], // "payload": "..." // } // } // ] const DictionaryValue* dictionary = FindDictionaryWithValue(top_list, kDescriptionKey, kTreeHashPerFile); const DictionaryValue* signed_content = NULL; if (!dictionary || !dictionary->GetDictionaryWithoutPathExpansion(kSignedContentKey, &signed_content)) { return false; } const ListValue* signatures = NULL; if (!signed_content->GetList(kSignaturesKey, &signatures)) return false; const DictionaryValue* signature_dict = FindDictionaryWithValue(signatures, kHeaderKidKey, kWebstoreKId); if (!signature_dict) return false; std::string protected_value; std::string encoded_signature; std::string decoded_signature; if (!signature_dict->GetString(kProtectedKey, &protected_value) || !signature_dict->GetString(kSignatureKey, &encoded_signature) || !base::Base64UrlDecode(encoded_signature, base::Base64UrlDecodePolicy::IGNORE_PADDING, &decoded_signature)) return false; std::string encoded_payload; if (!signed_content->GetString(kPayloadKey, &encoded_payload)) return false; valid_signature_ = VerifySignature(protected_value, encoded_payload, decoded_signature); if (!valid_signature_) return false; if (!base::Base64UrlDecode(encoded_payload, base::Base64UrlDecodePolicy::IGNORE_PADDING, payload)) return false; return true; } bool VerifiedContents::VerifySignature(const std::string& protected_value, const std::string& payload, const std::string& signature_bytes) { crypto::SignatureVerifier signature_verifier; if (!signature_verifier.VerifyInit( crypto::SignatureVerifier::RSA_PKCS1_SHA256, base::as_bytes(base::make_span(signature_bytes)), public_key_)) { VLOG(1) << "Could not verify signature - VerifyInit failure"; return false; } signature_verifier.VerifyUpdate( base::as_bytes(base::make_span(protected_value))); std::string dot("."); signature_verifier.VerifyUpdate(base::as_bytes(base::make_span(dot))); signature_verifier.VerifyUpdate(base::as_bytes(base::make_span(payload))); if (!signature_verifier.VerifyFinal()) { VLOG(1) << "Could not verify signature - VerifyFinal failure"; return false; } return true; } bool VerifiedContents::TreeHashRootEqualsImpl( const base::FilePath::StringType& normalized_relative_path, const std::string& expected) const { std::pair hashes = root_hashes_.equal_range(normalized_relative_path); for (auto iter = hashes.first; iter != hashes.second; ++iter) { if (expected == iter->second) return true; } return false; } } // namespace extensions