1#!/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 17""" 18Signs a given image using avbtool 19 20Usage: verity_utils properties_file output_image 21""" 22 23from __future__ import print_function 24 25import logging 26import os.path 27import shlex 28import struct 29import sys 30 31import common 32import sparse_img 33from rangelib import RangeSet 34 35logger = logging.getLogger(__name__) 36 37OPTIONS = common.OPTIONS 38BLOCK_SIZE = common.BLOCK_SIZE 39FIXED_SALT = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7" 40 41# From external/avb/avbtool.py 42MAX_VBMETA_SIZE = 64 * 1024 43MAX_FOOTER_SIZE = 4096 44 45class BuildVerityImageError(Exception): 46 """An Exception raised during verity image building.""" 47 48 def __init__(self, message): 49 Exception.__init__(self, message) 50 51 52def CreateVerityImageBuilder(prop_dict): 53 """Returns a verity image builder based on the given build properties. 54 55 Args: 56 prop_dict: A dict that contains the build properties. In particular, it will 57 look for verity-related property values. 58 59 Returns: 60 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. 62 """ 63 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) 67 68 # 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") 74 75 # Image uses hash footer. 76 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"]) 86 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"]) 97 98 return None 99 100 101class VerityImageBuilder(object): 102 """A builder that generates an image with verity metadata for Verified Boot. 103 104 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 111 """ 112 113 def CalculateMaxImageSize(self, partition_size): 114 """Calculates the filesystem image size for the given partition size.""" 115 raise NotImplementedError 116 117 def CalculateDynamicPartitionSize(self, image_size): 118 """Calculates and sets the partition size for a dynamic partition.""" 119 raise NotImplementedError 120 121 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 130class 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 = common.ResolveAVBSigningPathArgs(key_path) 145 146 self.salt = salt 147 self.signing_args = signing_args 148 self.image_size = None 149 150 def CalculateMinPartitionSize(self, image_size, size_calculator=None): 151 """Calculates min partition size for a given image size. 152 153 This is used when determining the partition size for a dynamic partition, 154 which should be cover the given image size (for filesystem files) as well as 155 the verity metadata size. 156 157 Args: 158 image_size: The size of the image in question. 159 size_calculator: The function to calculate max image size 160 for a given partition size. 161 162 Returns: 163 The minimum partition size required to accommodate the image size. 164 """ 165 if size_calculator is None: 166 size_calculator = self.CalculateMaxImageSize 167 168 # Use image size as partition size to approximate final partition size. 169 image_ratio = size_calculator(image_size) / float(image_size) 170 171 # Prepare a binary search for the optimal partition size. 172 lo = int(image_size / image_ratio) // BLOCK_SIZE * BLOCK_SIZE - BLOCK_SIZE 173 174 # Ensure lo is small enough: max_image_size should <= image_size. 175 delta = BLOCK_SIZE 176 max_image_size = size_calculator(lo) 177 while max_image_size > image_size: 178 image_ratio = max_image_size / float(lo) 179 lo = int(image_size / image_ratio) // BLOCK_SIZE * BLOCK_SIZE - delta 180 delta *= 2 181 max_image_size = size_calculator(lo) 182 183 hi = lo + BLOCK_SIZE 184 185 # Ensure hi is large enough: max_image_size should >= image_size. 186 delta = BLOCK_SIZE 187 max_image_size = size_calculator(hi) 188 while max_image_size < image_size: 189 image_ratio = max_image_size / float(hi) 190 hi = int(image_size / image_ratio) // BLOCK_SIZE * BLOCK_SIZE + delta 191 delta *= 2 192 max_image_size = size_calculator(hi) 193 194 partition_size = hi 195 196 # Start to binary search. 197 while lo < hi: 198 mid = ((lo + hi) // (2 * BLOCK_SIZE)) * BLOCK_SIZE 199 max_image_size = size_calculator(mid) 200 if max_image_size >= image_size: # if mid can accommodate image_size 201 if mid < partition_size: # if a smaller partition size is found 202 partition_size = mid 203 hi = mid 204 else: 205 lo = mid + BLOCK_SIZE 206 207 logger.info( 208 "CalculateMinPartitionSize(%d): partition_size %d.", image_size, 209 partition_size) 210 211 return partition_size 212 213 def CalculateDynamicPartitionSize(self, image_size): 214 self.partition_size = self.CalculateMinPartitionSize(image_size) 215 return self.partition_size 216 217 def CalculateMaxImageSize(self, partition_size=None): 218 """Calculates max image size for a given partition size. 219 220 Args: 221 partition_size: The partition size, which defaults to self.partition_size 222 if unspecified. 223 224 Returns: 225 The maximum image size. 226 227 Raises: 228 BuildVerityImageError: On error or getting invalid image size. 229 """ 230 if partition_size is None: 231 partition_size = self.partition_size 232 assert partition_size > 0, \ 233 "Invalid partition size: {}".format(partition_size) 234 235 add_footer = ("add_hash_footer" if self.footer_type == self.AVB_HASH_FOOTER 236 else "add_hashtree_footer") 237 cmd = [self.avbtool, add_footer, "--partition_size", 238 str(partition_size), "--calc_max_image_size"] 239 cmd.extend(shlex.split(self.signing_args)) 240 241 proc = common.Run(cmd) 242 output, _ = proc.communicate() 243 if proc.returncode != 0: 244 raise BuildVerityImageError( 245 "Failed to calculate max image size:\n{}".format(output)) 246 image_size = int(output) 247 if image_size <= 0: 248 raise BuildVerityImageError( 249 "Invalid max image size: {}".format(output)) 250 self.image_size = image_size 251 return image_size 252 253 def PadSparseImage(self, out_file): 254 # No-op as the padding is taken care of by avbtool. 255 pass 256 257 def Build(self, out_file): 258 """Adds dm-verity hashtree and AVB metadata to an image. 259 260 Args: 261 out_file: Path to image to modify. 262 """ 263 add_footer = ("add_hash_footer" if self.footer_type == self.AVB_HASH_FOOTER 264 else "add_hashtree_footer") 265 cmd = [self.avbtool, add_footer, 266 "--partition_size", str(self.partition_size), 267 "--partition_name", self.partition_name, 268 "--image", out_file] 269 if self.key_path and self.algorithm: 270 cmd.extend(["--key", self.key_path, "--algorithm", self.algorithm]) 271 if self.salt: 272 cmd.extend(["--salt", self.salt]) 273 cmd.extend(shlex.split(self.signing_args)) 274 275 proc = common.Run(cmd) 276 output, _ = proc.communicate() 277 if proc.returncode != 0: 278 raise BuildVerityImageError("Failed to add AVB footer: {}".format(output)) 279 280 281def CreateCustomImageBuilder(info_dict, partition_name, partition_size, 282 key_path, algorithm, signing_args): 283 builder = None 284 if info_dict.get("avb_enable") == "true": 285 builder = VerifiedBootVersion2VerityImageBuilder( 286 partition_name, 287 partition_size, 288 VerifiedBootVersion2VerityImageBuilder.AVB_HASHTREE_FOOTER, 289 info_dict.get("avb_avbtool"), 290 key_path, 291 algorithm, 292 # Salt is None because custom images have no fingerprint property to be 293 # used as the salt. 294 None, 295 signing_args) 296 297 return builder 298 299 300def GetDiskUsage(path): 301 """Returns the number of bytes that "path" occupies on host. 302 303 Args: 304 path: The directory or file to calculate size on. 305 306 Returns: 307 The number of bytes based on a 1K block_size. 308 """ 309 cmd = ["du", "-b", "-k", "-s", path] 310 output = common.RunAndCheckOutput(cmd, verbose=False) 311 return int(output.split()[0]) * 1024 312 313 314def CalculateVbmetaDigest(extracted_dir, avbtool): 315 """Calculates the vbmeta digest of the images in the extracted target_file""" 316 317 images_dir = common.MakeTempDir() 318 for name in ("PREBUILT_IMAGES", "RADIO", "IMAGES"): 319 path = os.path.join(extracted_dir, name) 320 if not os.path.exists(path): 321 continue 322 323 # Create symlink for image files under PREBUILT_IMAGES, RADIO and IMAGES, 324 # and put them into one directory. 325 for filename in os.listdir(path): 326 if not filename.endswith(".img"): 327 continue 328 symlink_path = os.path.join(images_dir, filename) 329 # The files in latter directory overwrite the existing links 330 common.RunAndCheckOutput( 331 ['ln', '-sf', os.path.join(path, filename), symlink_path]) 332 333 cmd = [avbtool, "calculate_vbmeta_digest", "--image", 334 os.path.join(images_dir, 'vbmeta.img')] 335 return common.RunAndCheckOutput(cmd) 336 337 338def main(argv): 339 if len(argv) != 2: 340 print(__doc__) 341 sys.exit(1) 342 343 common.InitLogging() 344 345 dict_file = argv[0] 346 out_file = argv[1] 347 348 prop_dict = {} 349 with open(dict_file, 'r') as f: 350 for line in f: 351 line = line.strip() 352 if not line or line.startswith("#"): 353 continue 354 k, v = line.split("=", 1) 355 prop_dict[k] = v 356 357 builder = CreateVerityImageBuilder(prop_dict) 358 359 if "partition_size" not in prop_dict: 360 image_size = GetDiskUsage(out_file) 361 # make sure that the image is big enough to hold vbmeta and footer 362 image_size = image_size + (MAX_VBMETA_SIZE + MAX_FOOTER_SIZE) 363 size = builder.CalculateDynamicPartitionSize(image_size) 364 prop_dict["partition_size"] = size 365 366 builder.Build(out_file) 367 368 369if __name__ == '__main__': 370 try: 371 main(sys.argv[1:]) 372 finally: 373 common.Cleanup() 374