1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "extensions/browser/verified_contents.h"
6
7 #include <stddef.h>
8 #include <algorithm>
9
10 #include "base/base64url.h"
11 #include "base/files/file_util.h"
12 #include "base/json/json_reader.h"
13 #include "base/memory/ptr_util.h"
14 #include "base/strings/string_util.h"
15 #include "base/timer/elapsed_timer.h"
16 #include "base/values.h"
17 #include "build/build_config.h"
18 #include "components/crx_file/id_util.h"
19 #include "crypto/signature_verifier.h"
20 #include "extensions/browser/content_verifier/content_verifier_utils.h"
21 #include "extensions/browser/content_verifier/scoped_uma_recorder.h"
22 #include "extensions/common/extension.h"
23
24 namespace {
25
26 const char kBlockSizeKey[] = "block_size";
27 const char kContentHashesKey[] = "content_hashes";
28 const char kDescriptionKey[] = "description";
29 const char kFilesKey[] = "files";
30 const char kFormatKey[] = "format";
31 const char kHashBlockSizeKey[] = "hash_block_size";
32 const char kHeaderKidKey[] = "header.kid";
33 const char kItemIdKey[] = "item_id";
34 const char kItemVersionKey[] = "item_version";
35 const char kPathKey[] = "path";
36 const char kPayloadKey[] = "payload";
37 const char kProtectedKey[] = "protected";
38 const char kRootHashKey[] = "root_hash";
39 const char kSignatureKey[] = "signature";
40 const char kSignaturesKey[] = "signatures";
41 const char kSignedContentKey[] = "signed_content";
42 const char kTreeHashPerFile[] = "treehash per file";
43 const char kTreeHash[] = "treehash";
44 const char kWebstoreKId[] = "webstore";
45
46 // Helper function to iterate over a list of dictionaries, returning the
47 // dictionary that has |key| -> |value| in it, if any, or null.
FindDictionaryWithValue(const base::Value & list,const std::string & key,const std::string & value)48 const base::Value* FindDictionaryWithValue(const base::Value& list,
49 const std::string& key,
50 const std::string& value) {
51 DCHECK(list.is_list());
52 for (const base::Value& item : list.GetList()) {
53 if (!item.is_dict())
54 continue;
55 // Finds a path because the |key| may include '.'.
56 const std::string* found_value = item.FindStringPath(key);
57 if (found_value && *found_value == value)
58 return &item;
59 }
60 return nullptr;
61 }
62
63 constexpr char kUMAVerifiedContentsInitResult[] =
64 "Extensions.ContentVerification.VerifiedContentsInitResult";
65 constexpr char kUMAVerifiedContentsInitTime[] =
66 "Extensions.ContentVerification.VerifiedContentsInitTime";
67
68 } // namespace
69
70 namespace extensions {
71
VerifiedContents(base::span<const uint8_t> public_key)72 VerifiedContents::VerifiedContents(base::span<const uint8_t> public_key)
73 : public_key_(public_key),
74 valid_signature_(false), // Guilty until proven innocent.
75 block_size_(0) {}
76
~VerifiedContents()77 VerifiedContents::~VerifiedContents() {
78 }
79
80 // The format of the payload json is:
81 // {
82 // "item_id": "<extension id>",
83 // "item_version": "<extension version>",
84 // "content_hashes": [
85 // {
86 // "block_size": 4096,
87 // "hash_block_size": 4096,
88 // "format": "treehash",
89 // "files": [
90 // {
91 // "path": "foo/bar",
92 // "root_hash": "<base64url encoded bytes>"
93 // },
94 // ...
95 // ]
96 // }
97 // ]
98 // }
99 // static.
Create(base::span<const uint8_t> public_key,const base::FilePath & path)100 std::unique_ptr<VerifiedContents> VerifiedContents::Create(
101 base::span<const uint8_t> public_key,
102 const base::FilePath& path) {
103 ScopedUMARecorder<kUMAVerifiedContentsInitTime,
104 kUMAVerifiedContentsInitResult>
105 uma_recorder;
106 // Note: VerifiedContents constructor is private.
107 auto verified_contents = base::WrapUnique(new VerifiedContents(public_key));
108 std::string payload;
109 if (!verified_contents->GetPayload(path, &payload))
110 return nullptr;
111
112 base::Optional<base::Value> dictionary = base::JSONReader::Read(payload);
113 if (!dictionary || !dictionary->is_dict())
114 return nullptr;
115
116 const std::string* item_id = dictionary->FindStringKey(kItemIdKey);
117 if (!item_id || !crx_file::id_util::IdIsValid(*item_id))
118 return nullptr;
119
120 verified_contents->extension_id_ = *item_id;
121
122 const std::string* version_string =
123 dictionary->FindStringKey(kItemVersionKey);
124 if (!version_string)
125 return nullptr;
126
127 verified_contents->version_ = base::Version(*version_string);
128 if (!verified_contents->version_.IsValid())
129 return nullptr;
130
131 const base::Value* hashes_list = dictionary->FindListKey(kContentHashesKey);
132 if (!hashes_list)
133 return nullptr;
134
135 for (const base::Value& hashes : hashes_list->GetList()) {
136 if (!hashes.is_dict())
137 return nullptr;
138
139 const std::string* format = hashes.FindStringKey(kFormatKey);
140 if (!format || *format != kTreeHash)
141 continue;
142
143 base::Optional<int> block_size = hashes.FindIntKey(kBlockSizeKey);
144 base::Optional<int> hash_block_size = hashes.FindIntKey(kHashBlockSizeKey);
145 if (!block_size || !hash_block_size)
146 return nullptr;
147
148 verified_contents->block_size_ = *block_size;
149
150 // We don't support using a different block_size and hash_block_size at
151 // the moment.
152 if (verified_contents->block_size_ != *hash_block_size)
153 return nullptr;
154
155 const base::Value* files = hashes.FindListKey(kFilesKey);
156 if (!files)
157 return nullptr;
158
159 for (const base::Value& data : files->GetList()) {
160 if (!data.is_dict())
161 return nullptr;
162
163 const std::string* file_path_string = data.FindStringKey(kPathKey);
164 const std::string* encoded_root_hash = data.FindStringKey(kRootHashKey);
165 std::string root_hash;
166 if (!file_path_string || !encoded_root_hash ||
167 !base::IsStringUTF8(*file_path_string) ||
168 !base::Base64UrlDecode(*encoded_root_hash,
169 base::Base64UrlDecodePolicy::IGNORE_PADDING,
170 &root_hash)) {
171 return nullptr;
172 }
173
174 content_verifier_utils::CanonicalRelativePath canonical_path =
175 content_verifier_utils::CanonicalizeRelativePath(
176 base::FilePath::FromUTF8Unsafe(*file_path_string));
177 auto i = verified_contents->root_hashes_.insert(
178 std::make_pair(canonical_path, std::string()));
179 i->second.swap(root_hash);
180 }
181
182 break;
183 }
184 uma_recorder.RecordSuccess();
185 return verified_contents;
186 }
187
HasTreeHashRoot(const base::FilePath & relative_path) const188 bool VerifiedContents::HasTreeHashRoot(
189 const base::FilePath& relative_path) const {
190 return base::Contains(
191 root_hashes_,
192 content_verifier_utils::CanonicalizeRelativePath(relative_path));
193 }
194
TreeHashRootEquals(const base::FilePath & relative_path,const std::string & expected) const195 bool VerifiedContents::TreeHashRootEquals(const base::FilePath& relative_path,
196 const std::string& expected) const {
197 return TreeHashRootEqualsForCanonicalPath(
198 content_verifier_utils::CanonicalizeRelativePath(relative_path),
199 expected);
200 }
201
202 // We're loosely following the "JSON Web Signature" draft spec for signing
203 // a JSON payload:
204 //
205 // http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-26
206 //
207 // The idea is that you have some JSON that you want to sign, so you
208 // base64-encode that and put it as the "payload" field in a containing
209 // dictionary. There might be signatures of it done with multiple
210 // algorithms/parameters, so the payload is followed by a list of one or more
211 // signature sections. Each signature section specifies the
212 // algorithm/parameters in a JSON object which is base64url encoded into one
213 // string and put into a "protected" field in the signature. Then the encoded
214 // "payload" and "protected" strings are concatenated with a "." in between
215 // them and those bytes are signed and the resulting signature is base64url
216 // encoded and placed in the "signature" field. To allow for extensibility, we
217 // wrap this, so we can include additional kinds of payloads in the future. E.g.
218 // [
219 // {
220 // "description": "treehash per file",
221 // "signed_content": {
222 // "payload": "<base64url encoded JSON to sign>",
223 // "signatures": [
224 // {
225 // "protected": "<base64url encoded JSON with algorithm/parameters>",
226 // "header": {
227 // <object with metadata about this signature, eg a key identifier>
228 // }
229 // "signature":
230 // "<base64url encoded signature over payload || . || protected>"
231 // },
232 // ... <zero or more additional signatures> ...
233 // ]
234 // }
235 // }
236 // ]
237 // There might be both a signature generated with a webstore private key and a
238 // signature generated with the extension's private key - for now we only
239 // verify the webstore one (since the id is in the payload, so we can trust
240 // that it is for a given extension), but in the future we may validate using
241 // the extension's key too (eg for non-webstore hosted extensions such as
242 // enterprise installs).
GetPayload(const base::FilePath & path,std::string * payload)243 bool VerifiedContents::GetPayload(const base::FilePath& path,
244 std::string* payload) {
245 std::string contents;
246 if (!base::ReadFileToString(path, &contents))
247 return false;
248 base::Optional<base::Value> top_list = base::JSONReader::Read(contents);
249 if (!top_list || !top_list->is_list())
250 return false;
251
252 // Find the "treehash per file" signed content, e.g.
253 // [
254 // {
255 // "description": "treehash per file",
256 // "signed_content": {
257 // "signatures": [ ... ],
258 // "payload": "..."
259 // }
260 // }
261 // ]
262 const base::Value* dictionary =
263 FindDictionaryWithValue(*top_list, kDescriptionKey, kTreeHashPerFile);
264 if (!dictionary)
265 return false;
266
267 const base::Value* signed_content =
268 dictionary->FindDictKey(kSignedContentKey);
269 if (!signed_content)
270 return false;
271
272 const base::Value* signatures = signed_content->FindListKey(kSignaturesKey);
273 if (!signatures)
274 return false;
275
276 const base::Value* signature_dict =
277 FindDictionaryWithValue(*signatures, kHeaderKidKey, kWebstoreKId);
278 if (!signature_dict)
279 return false;
280
281 const std::string* protected_value =
282 signature_dict->FindStringKey(kProtectedKey);
283 const std::string* encoded_signature =
284 signature_dict->FindStringKey(kSignatureKey);
285 std::string decoded_signature;
286 if (!protected_value || !encoded_signature ||
287 !base::Base64UrlDecode(*encoded_signature,
288 base::Base64UrlDecodePolicy::IGNORE_PADDING,
289 &decoded_signature))
290 return false;
291
292 const std::string* encoded_payload =
293 signed_content->FindStringKey(kPayloadKey);
294 if (!encoded_payload)
295 return false;
296
297 valid_signature_ =
298 VerifySignature(*protected_value, *encoded_payload, decoded_signature);
299 if (!valid_signature_)
300 return false;
301
302 if (!base::Base64UrlDecode(*encoded_payload,
303 base::Base64UrlDecodePolicy::IGNORE_PADDING,
304 payload))
305 return false;
306
307 return true;
308 }
309
VerifySignature(const std::string & protected_value,const std::string & payload,const std::string & signature_bytes)310 bool VerifiedContents::VerifySignature(const std::string& protected_value,
311 const std::string& payload,
312 const std::string& signature_bytes) {
313 crypto::SignatureVerifier signature_verifier;
314 if (!signature_verifier.VerifyInit(
315 crypto::SignatureVerifier::RSA_PKCS1_SHA256,
316 base::as_bytes(base::make_span(signature_bytes)), public_key_)) {
317 VLOG(1) << "Could not verify signature - VerifyInit failure";
318 return false;
319 }
320
321 signature_verifier.VerifyUpdate(
322 base::as_bytes(base::make_span(protected_value)));
323
324 std::string dot(".");
325 signature_verifier.VerifyUpdate(base::as_bytes(base::make_span(dot)));
326
327 signature_verifier.VerifyUpdate(base::as_bytes(base::make_span(payload)));
328
329 if (!signature_verifier.VerifyFinal()) {
330 VLOG(1) << "Could not verify signature - VerifyFinal failure";
331 return false;
332 }
333 return true;
334 }
335
TreeHashRootEqualsForCanonicalPath(const content_verifier_utils::CanonicalRelativePath & canonical_relative_path,const std::string & expected) const336 bool VerifiedContents::TreeHashRootEqualsForCanonicalPath(
337 const content_verifier_utils::CanonicalRelativePath&
338 canonical_relative_path,
339 const std::string& expected) const {
340 std::pair<RootHashes::const_iterator, RootHashes::const_iterator> hashes =
341 root_hashes_.equal_range(canonical_relative_path);
342 for (auto iter = hashes.first; iter != hashes.second; ++iter) {
343 if (expected == iter->second)
344 return true;
345 }
346 return false;
347 }
348
349 } // namespace extensions
350