blob: 9677b4186d39e5c36dc9a71537c7edf96b0c83fd [file] [log] [blame]
// 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 <stddef.h>
#include <algorithm>
#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<const uint8_t> 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": "<extension id>",
// "item_version": "<extension version>",
// "content_hashes": [
// {
// "block_size": 4096,
// "hash_block_size": 4096,
// "format": "treehash",
// "files": [
// {
// "path": "foo/bar",
// "root_hash": "<base64url encoded bytes>"
// },
// ...
// ]
// }
// ]
// }
// static.
std::unique_ptr<VerifiedContents> VerifiedContents::Create(
base::span<const uint8_t> 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<base::Value> value(base::JSONReader::Read(payload));
if (!value.get() || !value->is_dict())
return nullptr;
DictionaryValue* dictionary = static_cast<DictionaryValue*>(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": "<base64url encoded JSON to sign>",
// "signatures": [
// {
// "protected": "<base64url encoded JSON with algorithm/parameters>",
// "header": {
// <object with metadata about this signature, eg a key identifier>
// }
// "signature":
// "<base64url encoded signature over payload || . || protected>"
// },
// ... <zero or more additional signatures> ...
// ]
// }
// }
// ]
// 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<base::Value> value(base::JSONReader::Read(contents));
if (!value.get() || !value->is_list())
return false;
ListValue* top_list = static_cast<ListValue*>(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<RootHashes::const_iterator, RootHashes::const_iterator> 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