blob: ee197e03eb95a60cf8dfeb8fc3e78660fdcfca56 [file] [log] [blame]
Tianjie Xu67c7cbb2018-08-30 00:32:07 -07001#!/usr/bin/env python
2#
3# Copyright (C) 2018 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
Jiyong Parkb92b8f42021-03-15 23:13:42 +090017"""
18Signs a given image using avbtool
19
20Usage: verity_utils properties_file output_image
21"""
22
Tianjie Xu67c7cbb2018-08-30 00:32:07 -070023from __future__ import print_function
24
Tao Bao32fcdab2018-10-12 10:30:39 -070025import logging
Tao Bao71197512018-10-11 14:08:45 -070026import os.path
27import shlex
Tianjie Xu67c7cbb2018-08-30 00:32:07 -070028import struct
Tianjiebbde59f2021-05-03 21:18:56 -070029import sys
Tianjie Xu67c7cbb2018-08-30 00:32:07 -070030
31import common
Tao Bao71197512018-10-11 14:08:45 -070032import sparse_img
Tianjie Xu67c7cbb2018-08-30 00:32:07 -070033from rangelib import RangeSet
34
Tao Bao32fcdab2018-10-12 10:30:39 -070035logger = logging.getLogger(__name__)
36
Tao Bao71197512018-10-11 14:08:45 -070037OPTIONS = common.OPTIONS
38BLOCK_SIZE = common.BLOCK_SIZE
39FIXED_SALT = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7"
40
Jiyong Parkb92b8f42021-03-15 23:13:42 +090041# From external/avb/avbtool.py
42MAX_VBMETA_SIZE = 64 * 1024
43MAX_FOOTER_SIZE = 4096
Tao Bao71197512018-10-11 14:08:45 -070044
45class BuildVerityImageError(Exception):
46 """An Exception raised during verity image building."""
47
48 def __init__(self, message):
49 Exception.__init__(self, message)
50
51
Tao Bao7549e5e2018-10-03 14:23:59 -070052def CreateVerityImageBuilder(prop_dict):
53 """Returns a verity image builder based on the given build properties.
Tao Bao71197512018-10-11 14:08:45 -070054
55 Args:
Tao Bao7549e5e2018-10-03 14:23:59 -070056 prop_dict: A dict that contains the build properties. In particular, it will
57 look for verity-related property values.
Tao Bao71197512018-10-11 14:08:45 -070058
59 Returns:
Tao Bao7549e5e2018-10-03 14:23:59 -070060 A VerityImageBuilder instance for Verified Boot 1.0 or Verified Boot 2.0; or
61 None if the given build doesn't support Verified Boot.
Tao Bao71197512018-10-11 14:08:45 -070062 """
Tao Bao7549e5e2018-10-03 14:23:59 -070063 partition_size = prop_dict.get("partition_size")
64 # partition_size could be None at this point, if using dynamic partitions.
65 if partition_size:
66 partition_size = int(partition_size)
Tao Bao71197512018-10-11 14:08:45 -070067
Tao Bao7549e5e2018-10-03 14:23:59 -070068 # Verified Boot 2.0
69 if (prop_dict.get("avb_hash_enable") == "true" or
70 prop_dict.get("avb_hashtree_enable") == "true"):
71 # key_path and algorithm are only available when chain partition is used.
72 key_path = prop_dict.get("avb_key_path")
73 algorithm = prop_dict.get("avb_algorithm")
Tao Bao9e893c32019-06-20 16:14:55 -070074
75 # Image uses hash footer.
Tao Bao7549e5e2018-10-03 14:23:59 -070076 if prop_dict.get("avb_hash_enable") == "true":
77 return VerifiedBootVersion2VerityImageBuilder(
78 prop_dict["partition_name"],
79 partition_size,
80 VerifiedBootVersion2VerityImageBuilder.AVB_HASH_FOOTER,
81 prop_dict["avb_avbtool"],
82 key_path,
83 algorithm,
84 prop_dict.get("avb_salt"),
85 prop_dict["avb_add_hash_footer_args"])
Tao Bao9e893c32019-06-20 16:14:55 -070086
87 # Image uses hashtree footer.
88 return VerifiedBootVersion2VerityImageBuilder(
89 prop_dict["partition_name"],
90 partition_size,
91 VerifiedBootVersion2VerityImageBuilder.AVB_HASHTREE_FOOTER,
92 prop_dict["avb_avbtool"],
93 key_path,
94 algorithm,
95 prop_dict.get("avb_salt"),
96 prop_dict["avb_add_hashtree_footer_args"])
Tao Bao71197512018-10-11 14:08:45 -070097
Tao Bao7549e5e2018-10-03 14:23:59 -070098 return None
Tao Bao71197512018-10-11 14:08:45 -070099
100
Tao Bao7549e5e2018-10-03 14:23:59 -0700101class VerityImageBuilder(object):
102 """A builder that generates an image with verity metadata for Verified Boot.
Tao Bao71197512018-10-11 14:08:45 -0700103
Tao Bao7549e5e2018-10-03 14:23:59 -0700104 A VerityImageBuilder instance handles the works for building an image with
105 verity metadata for supporting Android Verified Boot. This class defines the
106 common interface between Verified Boot 1.0 and Verified Boot 2.0. A matching
107 builder will be returned based on the given build properties.
108
109 More info on the verity image generation can be found at the following link.
110 https://source.android.com/security/verifiedboot/dm-verity#implementation
Tao Bao71197512018-10-11 14:08:45 -0700111 """
Tao Bao71197512018-10-11 14:08:45 -0700112
Tao Bao7549e5e2018-10-03 14:23:59 -0700113 def CalculateMaxImageSize(self, partition_size):
114 """Calculates the filesystem image size for the given partition size."""
115 raise NotImplementedError
Tao Bao71197512018-10-11 14:08:45 -0700116
Tao Bao7549e5e2018-10-03 14:23:59 -0700117 def CalculateDynamicPartitionSize(self, image_size):
118 """Calculates and sets the partition size for a dynamic partition."""
119 raise NotImplementedError
Tao Bao71197512018-10-11 14:08:45 -0700120
Tao Bao7549e5e2018-10-03 14:23:59 -0700121 def PadSparseImage(self, out_file):
122 """Adds padding to the generated sparse image."""
123 raise NotImplementedError
124
125 def Build(self, out_file):
126 """Builds the verity image and writes it to the given file."""
127 raise NotImplementedError
128
129
Tao Bao7549e5e2018-10-03 14:23:59 -0700130class VerifiedBootVersion2VerityImageBuilder(VerityImageBuilder):
131 """A VerityImageBuilder for Verified Boot 2.0."""
132
133 AVB_HASH_FOOTER = 1
134 AVB_HASHTREE_FOOTER = 2
135
136 def __init__(self, partition_name, partition_size, footer_type, avbtool,
137 key_path, algorithm, salt, signing_args):
138 self.version = 2
139 self.partition_name = partition_name
140 self.partition_size = partition_size
141 self.footer_type = footer_type
142 self.avbtool = avbtool
143 self.algorithm = algorithm
144 self.key_path = key_path
Oleksiy Avramchenko166d8192022-01-20 22:10:52 +0100145 if key_path and not os.path.exists(key_path) and OPTIONS.search_path:
146 new_key_path = os.path.join(OPTIONS.search_path, key_path)
147 if os.path.exists(new_key_path):
148 self.key_path = new_key_path
149
Tao Bao7549e5e2018-10-03 14:23:59 -0700150 self.salt = salt
151 self.signing_args = signing_args
152 self.image_size = None
153
154 def CalculateMinPartitionSize(self, image_size, size_calculator=None):
155 """Calculates min partition size for a given image size.
156
157 This is used when determining the partition size for a dynamic partition,
158 which should be cover the given image size (for filesystem files) as well as
159 the verity metadata size.
160
161 Args:
162 image_size: The size of the image in question.
163 size_calculator: The function to calculate max image size
164 for a given partition size.
165
166 Returns:
167 The minimum partition size required to accommodate the image size.
168 """
169 if size_calculator is None:
170 size_calculator = self.CalculateMaxImageSize
171
172 # Use image size as partition size to approximate final partition size.
173 image_ratio = size_calculator(image_size) / float(image_size)
174
175 # Prepare a binary search for the optimal partition size.
176 lo = int(image_size / image_ratio) // BLOCK_SIZE * BLOCK_SIZE - BLOCK_SIZE
177
178 # Ensure lo is small enough: max_image_size should <= image_size.
179 delta = BLOCK_SIZE
180 max_image_size = size_calculator(lo)
181 while max_image_size > image_size:
182 image_ratio = max_image_size / float(lo)
183 lo = int(image_size / image_ratio) // BLOCK_SIZE * BLOCK_SIZE - delta
184 delta *= 2
185 max_image_size = size_calculator(lo)
186
187 hi = lo + BLOCK_SIZE
188
189 # Ensure hi is large enough: max_image_size should >= image_size.
190 delta = BLOCK_SIZE
191 max_image_size = size_calculator(hi)
192 while max_image_size < image_size:
193 image_ratio = max_image_size / float(hi)
194 hi = int(image_size / image_ratio) // BLOCK_SIZE * BLOCK_SIZE + delta
195 delta *= 2
196 max_image_size = size_calculator(hi)
197
198 partition_size = hi
199
200 # Start to binary search.
201 while lo < hi:
202 mid = ((lo + hi) // (2 * BLOCK_SIZE)) * BLOCK_SIZE
203 max_image_size = size_calculator(mid)
204 if max_image_size >= image_size: # if mid can accommodate image_size
205 if mid < partition_size: # if a smaller partition size is found
206 partition_size = mid
207 hi = mid
208 else:
209 lo = mid + BLOCK_SIZE
210
211 logger.info(
212 "CalculateMinPartitionSize(%d): partition_size %d.", image_size,
213 partition_size)
214
215 return partition_size
216
217 def CalculateDynamicPartitionSize(self, image_size):
218 self.partition_size = self.CalculateMinPartitionSize(image_size)
219 return self.partition_size
220
221 def CalculateMaxImageSize(self, partition_size=None):
222 """Calculates max image size for a given partition size.
223
224 Args:
225 partition_size: The partition size, which defaults to self.partition_size
226 if unspecified.
227
228 Returns:
229 The maximum image size.
230
231 Raises:
232 BuildVerityImageError: On error or getting invalid image size.
233 """
234 if partition_size is None:
235 partition_size = self.partition_size
236 assert partition_size > 0, \
237 "Invalid partition size: {}".format(partition_size)
238
239 add_footer = ("add_hash_footer" if self.footer_type == self.AVB_HASH_FOOTER
240 else "add_hashtree_footer")
241 cmd = [self.avbtool, add_footer, "--partition_size",
242 str(partition_size), "--calc_max_image_size"]
243 cmd.extend(shlex.split(self.signing_args))
244
245 proc = common.Run(cmd)
246 output, _ = proc.communicate()
247 if proc.returncode != 0:
248 raise BuildVerityImageError(
249 "Failed to calculate max image size:\n{}".format(output))
250 image_size = int(output)
251 if image_size <= 0:
252 raise BuildVerityImageError(
253 "Invalid max image size: {}".format(output))
254 self.image_size = image_size
255 return image_size
256
257 def PadSparseImage(self, out_file):
258 # No-op as the padding is taken care of by avbtool.
259 pass
260
261 def Build(self, out_file):
262 """Adds dm-verity hashtree and AVB metadata to an image.
263
264 Args:
265 out_file: Path to image to modify.
266 """
267 add_footer = ("add_hash_footer" if self.footer_type == self.AVB_HASH_FOOTER
268 else "add_hashtree_footer")
269 cmd = [self.avbtool, add_footer,
270 "--partition_size", str(self.partition_size),
271 "--partition_name", self.partition_name,
272 "--image", out_file]
273 if self.key_path and self.algorithm:
274 cmd.extend(["--key", self.key_path, "--algorithm", self.algorithm])
275 if self.salt:
276 cmd.extend(["--salt", self.salt])
277 cmd.extend(shlex.split(self.signing_args))
278
279 proc = common.Run(cmd)
280 output, _ = proc.communicate()
281 if proc.returncode != 0:
282 raise BuildVerityImageError("Failed to add AVB footer: {}".format(output))
Tao Bao71197512018-10-11 14:08:45 -0700283
Tianjie Xu67c7cbb2018-08-30 00:32:07 -0700284
285class HashtreeInfoGenerationError(Exception):
286 """An Exception raised during hashtree info generation."""
287
288 def __init__(self, message):
289 Exception.__init__(self, message)
290
291
292class HashtreeInfo(object):
293 def __init__(self):
294 self.hashtree_range = None
295 self.filesystem_range = None
296 self.hash_algorithm = None
297 self.salt = None
298 self.root_hash = None
299
300
301def CreateHashtreeInfoGenerator(partition_name, block_size, info_dict):
hungweichen65ba3752022-08-17 10:14:01 +0000302 return None
Tianjie Xu67c7cbb2018-08-30 00:32:07 -0700303
304
305class HashtreeInfoGenerator(object):
306 def Generate(self, image):
307 raise NotImplementedError
308
309 def DecomposeSparseImage(self, image):
310 raise NotImplementedError
311
312 def ValidateHashtree(self):
313 raise NotImplementedError
314
315
Hongguang Chenf23364d2020-04-27 18:36:36 -0700316def CreateCustomImageBuilder(info_dict, partition_name, partition_size,
317 key_path, algorithm, signing_args):
318 builder = None
319 if info_dict.get("avb_enable") == "true":
320 builder = VerifiedBootVersion2VerityImageBuilder(
321 partition_name,
322 partition_size,
323 VerifiedBootVersion2VerityImageBuilder.AVB_HASHTREE_FOOTER,
324 info_dict.get("avb_avbtool"),
325 key_path,
326 algorithm,
327 # Salt is None because custom images have no fingerprint property to be
328 # used as the salt.
329 None,
330 signing_args)
331
332 return builder
Jiyong Parkb92b8f42021-03-15 23:13:42 +0900333
334
335def GetDiskUsage(path):
336 """Returns the number of bytes that "path" occupies on host.
337
338 Args:
339 path: The directory or file to calculate size on.
340
341 Returns:
342 The number of bytes based on a 1K block_size.
343 """
344 cmd = ["du", "-b", "-k", "-s", path]
345 output = common.RunAndCheckOutput(cmd, verbose=False)
346 return int(output.split()[0]) * 1024
347
348
Tianjiebbde59f2021-05-03 21:18:56 -0700349def CalculateVbmetaDigest(extracted_dir, avbtool):
350 """Calculates the vbmeta digest of the images in the extracted target_file"""
351
352 images_dir = common.MakeTempDir()
353 for name in ("PREBUILT_IMAGES", "RADIO", "IMAGES"):
354 path = os.path.join(extracted_dir, name)
355 if not os.path.exists(path):
356 continue
357
358 # Create symlink for image files under PREBUILT_IMAGES, RADIO and IMAGES,
359 # and put them into one directory.
360 for filename in os.listdir(path):
361 if not filename.endswith(".img"):
362 continue
363 symlink_path = os.path.join(images_dir, filename)
364 # The files in latter directory overwrite the existing links
365 common.RunAndCheckOutput(
366 ['ln', '-sf', os.path.join(path, filename), symlink_path])
367
368 cmd = [avbtool, "calculate_vbmeta_digest", "--image",
369 os.path.join(images_dir, 'vbmeta.img')]
370 return common.RunAndCheckOutput(cmd)
371
372
Jiyong Parkb92b8f42021-03-15 23:13:42 +0900373def main(argv):
374 if len(argv) != 2:
375 print(__doc__)
376 sys.exit(1)
377
378 common.InitLogging()
379
380 dict_file = argv[0]
381 out_file = argv[1]
382
383 prop_dict = {}
384 with open(dict_file, 'r') as f:
385 for line in f:
386 line = line.strip()
387 if not line or line.startswith("#"):
388 continue
389 k, v = line.split("=", 1)
390 prop_dict[k] = v
391
392 builder = CreateVerityImageBuilder(prop_dict)
393
394 if "partition_size" not in prop_dict:
395 image_size = GetDiskUsage(out_file)
396 # make sure that the image is big enough to hold vbmeta and footer
397 image_size = image_size + (MAX_VBMETA_SIZE + MAX_FOOTER_SIZE)
398 size = builder.CalculateDynamicPartitionSize(image_size)
399 prop_dict["partition_size"] = size
400
401 builder.Build(out_file)
402
403
404if __name__ == '__main__':
405 try:
406 main(sys.argv[1:])
407 finally:
408 common.Cleanup()