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