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 17from __future__ import print_function 18 19import logging 20import os.path 21import shlex 22import struct 23 24import common 25import sparse_img 26from rangelib import RangeSet 27 28logger = logging.getLogger(__name__) 29 30OPTIONS = common.OPTIONS 31BLOCK_SIZE = common.BLOCK_SIZE 32FIXED_SALT = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7" 33 34 35class BuildVerityImageError(Exception): 36 """An Exception raised during verity image building.""" 37 38 def __init__(self, message): 39 Exception.__init__(self, message) 40 41 42def GetVerityFECSize(image_size): 43 cmd = ["fec", "-s", str(image_size)] 44 output = common.RunAndCheckOutput(cmd, verbose=False) 45 return int(output) 46 47 48def GetVerityTreeSize(image_size): 49 cmd = ["build_verity_tree", "-s", str(image_size)] 50 output = common.RunAndCheckOutput(cmd, verbose=False) 51 return int(output) 52 53 54def GetVerityMetadataSize(image_size): 55 cmd = ["build_verity_metadata.py", "size", str(image_size)] 56 output = common.RunAndCheckOutput(cmd, verbose=False) 57 return int(output) 58 59 60def GetVeritySize(image_size, fec_supported): 61 verity_tree_size = GetVerityTreeSize(image_size) 62 verity_metadata_size = GetVerityMetadataSize(image_size) 63 verity_size = verity_tree_size + verity_metadata_size 64 if fec_supported: 65 fec_size = GetVerityFECSize(image_size + verity_size) 66 return verity_size + fec_size 67 return verity_size 68 69 70def GetSimgSize(image_file): 71 simg = sparse_img.SparseImage(image_file, build_map=False) 72 return simg.blocksize * simg.total_blocks 73 74 75def ZeroPadSimg(image_file, pad_size): 76 blocks = pad_size // BLOCK_SIZE 77 logger.info("Padding %d blocks (%d bytes)", blocks, pad_size) 78 simg = sparse_img.SparseImage(image_file, mode="r+b", build_map=False) 79 simg.AppendFillChunk(0, blocks) 80 81 82def BuildVerityFEC(sparse_image_path, verity_path, verity_fec_path, 83 padding_size): 84 cmd = ["fec", "-e", "-p", str(padding_size), sparse_image_path, 85 verity_path, verity_fec_path] 86 common.RunAndCheckOutput(cmd) 87 88 89def BuildVerityTree(sparse_image_path, verity_image_path): 90 cmd = ["build_verity_tree", "-A", FIXED_SALT, sparse_image_path, 91 verity_image_path] 92 output = common.RunAndCheckOutput(cmd) 93 root, salt = output.split() 94 return root, salt 95 96 97def BuildVerityMetadata(image_size, verity_metadata_path, root_hash, salt, 98 block_device, signer_path, key, signer_args, 99 verity_disable): 100 cmd = ["build_verity_metadata.py", "build", str(image_size), 101 verity_metadata_path, root_hash, salt, block_device, signer_path, key] 102 if signer_args: 103 cmd.append("--signer_args=\"%s\"" % (' '.join(signer_args),)) 104 if verity_disable: 105 cmd.append("--verity_disable") 106 common.RunAndCheckOutput(cmd) 107 108 109def Append2Simg(sparse_image_path, unsparse_image_path, error_message): 110 """Appends the unsparse image to the given sparse image. 111 112 Args: 113 sparse_image_path: the path to the (sparse) image 114 unsparse_image_path: the path to the (unsparse) image 115 116 Raises: 117 BuildVerityImageError: On error. 118 """ 119 cmd = ["append2simg", sparse_image_path, unsparse_image_path] 120 try: 121 common.RunAndCheckOutput(cmd) 122 except: 123 logger.exception(error_message) 124 raise BuildVerityImageError(error_message) 125 126 127def Append(target, file_to_append, error_message): 128 """Appends file_to_append to target. 129 130 Raises: 131 BuildVerityImageError: On error. 132 """ 133 try: 134 with open(target, "a") as out_file, open(file_to_append, "r") as input_file: 135 for line in input_file: 136 out_file.write(line) 137 except IOError: 138 logger.exception(error_message) 139 raise BuildVerityImageError(error_message) 140 141 142def CreateVerityImageBuilder(prop_dict): 143 """Returns a verity image builder based on the given build properties. 144 145 Args: 146 prop_dict: A dict that contains the build properties. In particular, it will 147 look for verity-related property values. 148 149 Returns: 150 A VerityImageBuilder instance for Verified Boot 1.0 or Verified Boot 2.0; or 151 None if the given build doesn't support Verified Boot. 152 """ 153 partition_size = prop_dict.get("partition_size") 154 # partition_size could be None at this point, if using dynamic partitions. 155 if partition_size: 156 partition_size = int(partition_size) 157 158 # Verified Boot 1.0 159 verity_supported = prop_dict.get("verity") == "true" 160 is_verity_partition = "verity_block_device" in prop_dict 161 if verity_supported and is_verity_partition: 162 if OPTIONS.verity_signer_path is not None: 163 signer_path = OPTIONS.verity_signer_path 164 else: 165 signer_path = prop_dict["verity_signer_cmd"] 166 return Version1VerityImageBuilder( 167 partition_size, 168 prop_dict["verity_block_device"], 169 prop_dict.get("verity_fec") == "true", 170 signer_path, 171 prop_dict["verity_key"] + ".pk8", 172 OPTIONS.verity_signer_args, 173 "verity_disable" in prop_dict) 174 175 # Verified Boot 2.0 176 if (prop_dict.get("avb_hash_enable") == "true" or 177 prop_dict.get("avb_hashtree_enable") == "true"): 178 # key_path and algorithm are only available when chain partition is used. 179 key_path = prop_dict.get("avb_key_path") 180 algorithm = prop_dict.get("avb_algorithm") 181 if prop_dict.get("avb_hash_enable") == "true": 182 return VerifiedBootVersion2VerityImageBuilder( 183 prop_dict["partition_name"], 184 partition_size, 185 VerifiedBootVersion2VerityImageBuilder.AVB_HASH_FOOTER, 186 prop_dict["avb_avbtool"], 187 key_path, 188 algorithm, 189 prop_dict.get("avb_salt"), 190 prop_dict["avb_add_hash_footer_args"]) 191 else: 192 return VerifiedBootVersion2VerityImageBuilder( 193 prop_dict["partition_name"], 194 partition_size, 195 VerifiedBootVersion2VerityImageBuilder.AVB_HASHTREE_FOOTER, 196 prop_dict["avb_avbtool"], 197 key_path, 198 algorithm, 199 prop_dict.get("avb_salt"), 200 prop_dict["avb_add_hashtree_footer_args"]) 201 202 return None 203 204 205class VerityImageBuilder(object): 206 """A builder that generates an image with verity metadata for Verified Boot. 207 208 A VerityImageBuilder instance handles the works for building an image with 209 verity metadata for supporting Android Verified Boot. This class defines the 210 common interface between Verified Boot 1.0 and Verified Boot 2.0. A matching 211 builder will be returned based on the given build properties. 212 213 More info on the verity image generation can be found at the following link. 214 https://source.android.com/security/verifiedboot/dm-verity#implementation 215 """ 216 217 def CalculateMaxImageSize(self, partition_size): 218 """Calculates the filesystem image size for the given partition size.""" 219 raise NotImplementedError 220 221 def CalculateDynamicPartitionSize(self, image_size): 222 """Calculates and sets the partition size for a dynamic partition.""" 223 raise NotImplementedError 224 225 def PadSparseImage(self, out_file): 226 """Adds padding to the generated sparse image.""" 227 raise NotImplementedError 228 229 def Build(self, out_file): 230 """Builds the verity image and writes it to the given file.""" 231 raise NotImplementedError 232 233 234class Version1VerityImageBuilder(VerityImageBuilder): 235 """A VerityImageBuilder for Verified Boot 1.0.""" 236 237 def __init__(self, partition_size, block_dev, fec_supported, signer_path, 238 signer_key, signer_args, verity_disable): 239 self.version = 1 240 self.partition_size = partition_size 241 self.block_device = block_dev 242 self.fec_supported = fec_supported 243 self.signer_path = signer_path 244 self.signer_key = signer_key 245 self.signer_args = signer_args 246 self.verity_disable = verity_disable 247 self.image_size = None 248 self.verity_size = None 249 250 def CalculateDynamicPartitionSize(self, image_size): 251 # This needs to be implemented. Note that returning the given image size as 252 # the partition size doesn't make sense, as it will fail later. 253 raise NotImplementedError 254 255 def CalculateMaxImageSize(self, partition_size=None): 256 """Calculates the max image size by accounting for the verity metadata. 257 258 Args: 259 partition_size: The partition size, which defaults to self.partition_size 260 if unspecified. 261 262 Returns: 263 The size of the image adjusted for verity metadata. 264 """ 265 if partition_size is None: 266 partition_size = self.partition_size 267 assert partition_size > 0, \ 268 "Invalid partition size: {}".format(partition_size) 269 270 hi = partition_size 271 if hi % BLOCK_SIZE != 0: 272 hi = (hi // BLOCK_SIZE) * BLOCK_SIZE 273 274 # verity tree and fec sizes depend on the partition size, which 275 # means this estimate is always going to be unnecessarily small 276 verity_size = GetVeritySize(hi, self.fec_supported) 277 lo = partition_size - verity_size 278 result = lo 279 280 # do a binary search for the optimal size 281 while lo < hi: 282 i = ((lo + hi) // (2 * BLOCK_SIZE)) * BLOCK_SIZE 283 v = GetVeritySize(i, self.fec_supported) 284 if i + v <= partition_size: 285 if result < i: 286 result = i 287 verity_size = v 288 lo = i + BLOCK_SIZE 289 else: 290 hi = i 291 292 self.image_size = result 293 self.verity_size = verity_size 294 295 logger.info( 296 "Calculated image size for verity: partition_size %d, image_size %d, " 297 "verity_size %d", partition_size, result, verity_size) 298 return result 299 300 def Build(self, out_file): 301 """Creates an image that is verifiable using dm-verity. 302 303 Args: 304 out_file: the output image. 305 306 Returns: 307 AssertionError: On invalid partition sizes. 308 BuildVerityImageError: On other errors. 309 """ 310 image_size = int(self.image_size) 311 tempdir_name = common.MakeTempDir(suffix="_verity_images") 312 313 # Get partial image paths. 314 verity_image_path = os.path.join(tempdir_name, "verity.img") 315 verity_metadata_path = os.path.join(tempdir_name, "verity_metadata.img") 316 317 # Build the verity tree and get the root hash and salt. 318 root_hash, salt = BuildVerityTree(out_file, verity_image_path) 319 320 # Build the metadata blocks. 321 BuildVerityMetadata( 322 image_size, verity_metadata_path, root_hash, salt, self.block_device, 323 self.signer_path, self.signer_key, self.signer_args, 324 self.verity_disable) 325 326 padding_size = self.partition_size - self.image_size - self.verity_size 327 assert padding_size >= 0 328 329 # Build the full verified image. 330 Append( 331 verity_image_path, verity_metadata_path, 332 "Failed to append verity metadata") 333 334 if self.fec_supported: 335 # Build FEC for the entire partition, including metadata. 336 verity_fec_path = os.path.join(tempdir_name, "verity_fec.img") 337 BuildVerityFEC( 338 out_file, verity_image_path, verity_fec_path, padding_size) 339 Append(verity_image_path, verity_fec_path, "Failed to append FEC") 340 341 Append2Simg( 342 out_file, verity_image_path, "Failed to append verity data") 343 344 def PadSparseImage(self, out_file): 345 sparse_image_size = GetSimgSize(out_file) 346 if sparse_image_size > self.image_size: 347 raise BuildVerityImageError( 348 "Error: image size of {} is larger than partition size of " 349 "{}".format(sparse_image_size, self.image_size)) 350 ZeroPadSimg(out_file, self.image_size - sparse_image_size) 351 352 353class VerifiedBootVersion2VerityImageBuilder(VerityImageBuilder): 354 """A VerityImageBuilder for Verified Boot 2.0.""" 355 356 AVB_HASH_FOOTER = 1 357 AVB_HASHTREE_FOOTER = 2 358 359 def __init__(self, partition_name, partition_size, footer_type, avbtool, 360 key_path, algorithm, salt, signing_args): 361 self.version = 2 362 self.partition_name = partition_name 363 self.partition_size = partition_size 364 self.footer_type = footer_type 365 self.avbtool = avbtool 366 self.algorithm = algorithm 367 self.key_path = key_path 368 self.salt = salt 369 self.signing_args = signing_args 370 self.image_size = None 371 372 def CalculateMinPartitionSize(self, image_size, size_calculator=None): 373 """Calculates min partition size for a given image size. 374 375 This is used when determining the partition size for a dynamic partition, 376 which should be cover the given image size (for filesystem files) as well as 377 the verity metadata size. 378 379 Args: 380 image_size: The size of the image in question. 381 size_calculator: The function to calculate max image size 382 for a given partition size. 383 384 Returns: 385 The minimum partition size required to accommodate the image size. 386 """ 387 if size_calculator is None: 388 size_calculator = self.CalculateMaxImageSize 389 390 # Use image size as partition size to approximate final partition size. 391 image_ratio = size_calculator(image_size) / float(image_size) 392 393 # Prepare a binary search for the optimal partition size. 394 lo = int(image_size / image_ratio) // BLOCK_SIZE * BLOCK_SIZE - BLOCK_SIZE 395 396 # Ensure lo is small enough: max_image_size should <= image_size. 397 delta = BLOCK_SIZE 398 max_image_size = size_calculator(lo) 399 while max_image_size > image_size: 400 image_ratio = max_image_size / float(lo) 401 lo = int(image_size / image_ratio) // BLOCK_SIZE * BLOCK_SIZE - delta 402 delta *= 2 403 max_image_size = size_calculator(lo) 404 405 hi = lo + BLOCK_SIZE 406 407 # Ensure hi is large enough: max_image_size should >= image_size. 408 delta = BLOCK_SIZE 409 max_image_size = size_calculator(hi) 410 while max_image_size < image_size: 411 image_ratio = max_image_size / float(hi) 412 hi = int(image_size / image_ratio) // BLOCK_SIZE * BLOCK_SIZE + delta 413 delta *= 2 414 max_image_size = size_calculator(hi) 415 416 partition_size = hi 417 418 # Start to binary search. 419 while lo < hi: 420 mid = ((lo + hi) // (2 * BLOCK_SIZE)) * BLOCK_SIZE 421 max_image_size = size_calculator(mid) 422 if max_image_size >= image_size: # if mid can accommodate image_size 423 if mid < partition_size: # if a smaller partition size is found 424 partition_size = mid 425 hi = mid 426 else: 427 lo = mid + BLOCK_SIZE 428 429 logger.info( 430 "CalculateMinPartitionSize(%d): partition_size %d.", image_size, 431 partition_size) 432 433 return partition_size 434 435 def CalculateDynamicPartitionSize(self, image_size): 436 self.partition_size = self.CalculateMinPartitionSize(image_size) 437 return self.partition_size 438 439 def CalculateMaxImageSize(self, partition_size=None): 440 """Calculates max image size for a given partition size. 441 442 Args: 443 partition_size: The partition size, which defaults to self.partition_size 444 if unspecified. 445 446 Returns: 447 The maximum image size. 448 449 Raises: 450 BuildVerityImageError: On error or getting invalid image size. 451 """ 452 if partition_size is None: 453 partition_size = self.partition_size 454 assert partition_size > 0, \ 455 "Invalid partition size: {}".format(partition_size) 456 457 add_footer = ("add_hash_footer" if self.footer_type == self.AVB_HASH_FOOTER 458 else "add_hashtree_footer") 459 cmd = [self.avbtool, add_footer, "--partition_size", 460 str(partition_size), "--calc_max_image_size"] 461 cmd.extend(shlex.split(self.signing_args)) 462 463 proc = common.Run(cmd) 464 output, _ = proc.communicate() 465 if proc.returncode != 0: 466 raise BuildVerityImageError( 467 "Failed to calculate max image size:\n{}".format(output)) 468 image_size = int(output) 469 if image_size <= 0: 470 raise BuildVerityImageError( 471 "Invalid max image size: {}".format(output)) 472 self.image_size = image_size 473 return image_size 474 475 def PadSparseImage(self, out_file): 476 # No-op as the padding is taken care of by avbtool. 477 pass 478 479 def Build(self, out_file): 480 """Adds dm-verity hashtree and AVB metadata to an image. 481 482 Args: 483 out_file: Path to image to modify. 484 """ 485 add_footer = ("add_hash_footer" if self.footer_type == self.AVB_HASH_FOOTER 486 else "add_hashtree_footer") 487 cmd = [self.avbtool, add_footer, 488 "--partition_size", str(self.partition_size), 489 "--partition_name", self.partition_name, 490 "--image", out_file] 491 if self.key_path and self.algorithm: 492 cmd.extend(["--key", self.key_path, "--algorithm", self.algorithm]) 493 if self.salt: 494 cmd.extend(["--salt", self.salt]) 495 cmd.extend(shlex.split(self.signing_args)) 496 497 proc = common.Run(cmd) 498 output, _ = proc.communicate() 499 if proc.returncode != 0: 500 raise BuildVerityImageError("Failed to add AVB footer: {}".format(output)) 501 502 503class HashtreeInfoGenerationError(Exception): 504 """An Exception raised during hashtree info generation.""" 505 506 def __init__(self, message): 507 Exception.__init__(self, message) 508 509 510class HashtreeInfo(object): 511 def __init__(self): 512 self.hashtree_range = None 513 self.filesystem_range = None 514 self.hash_algorithm = None 515 self.salt = None 516 self.root_hash = None 517 518 519def CreateHashtreeInfoGenerator(partition_name, block_size, info_dict): 520 generator = None 521 if (info_dict.get("verity") == "true" and 522 info_dict.get("{}_verity_block_device".format(partition_name))): 523 partition_size = info_dict["{}_size".format(partition_name)] 524 fec_supported = info_dict.get("verity_fec") == "true" 525 generator = VerifiedBootVersion1HashtreeInfoGenerator( 526 partition_size, block_size, fec_supported) 527 528 return generator 529 530 531class HashtreeInfoGenerator(object): 532 def Generate(self, image): 533 raise NotImplementedError 534 535 def DecomposeSparseImage(self, image): 536 raise NotImplementedError 537 538 def ValidateHashtree(self): 539 raise NotImplementedError 540 541 542class VerifiedBootVersion1HashtreeInfoGenerator(HashtreeInfoGenerator): 543 """A class that parses the metadata of hashtree for a given partition.""" 544 545 def __init__(self, partition_size, block_size, fec_supported): 546 """Initialize VerityTreeInfo with the sparse image and input property. 547 548 Arguments: 549 partition_size: The whole size in bytes of a partition, including the 550 filesystem size, padding size, and verity size. 551 block_size: Expected size in bytes of each block for the sparse image. 552 fec_supported: True if the verity section contains fec data. 553 """ 554 555 self.block_size = block_size 556 self.partition_size = partition_size 557 self.fec_supported = fec_supported 558 559 self.image = None 560 self.filesystem_size = None 561 self.hashtree_size = None 562 self.metadata_size = None 563 564 prop_dict = { 565 'partition_size': str(partition_size), 566 'verity': 'true', 567 'verity_fec': 'true' if fec_supported else None, 568 # 'verity_block_device' needs to be present to indicate a verity-enabled 569 # partition. 570 'verity_block_device': '', 571 # We don't need the following properties that are needed for signing the 572 # verity metadata. 573 'verity_key': '', 574 'verity_signer_cmd': None, 575 } 576 self.verity_image_builder = CreateVerityImageBuilder(prop_dict) 577 578 self.hashtree_info = HashtreeInfo() 579 580 def DecomposeSparseImage(self, image): 581 """Calculate the verity size based on the size of the input image. 582 583 Since we already know the structure of a verity enabled image to be: 584 [filesystem, verity_hashtree, verity_metadata, fec_data]. We can then 585 calculate the size and offset of each section. 586 """ 587 588 self.image = image 589 assert self.block_size == image.blocksize 590 assert self.partition_size == image.total_blocks * self.block_size, \ 591 "partition size {} doesn't match with the calculated image size." \ 592 " total_blocks: {}".format(self.partition_size, image.total_blocks) 593 594 adjusted_size = self.verity_image_builder.CalculateMaxImageSize() 595 assert adjusted_size % self.block_size == 0 596 597 verity_tree_size = GetVerityTreeSize(adjusted_size) 598 assert verity_tree_size % self.block_size == 0 599 600 metadata_size = GetVerityMetadataSize(adjusted_size) 601 assert metadata_size % self.block_size == 0 602 603 self.filesystem_size = adjusted_size 604 self.hashtree_size = verity_tree_size 605 self.metadata_size = metadata_size 606 607 self.hashtree_info.filesystem_range = RangeSet( 608 data=[0, adjusted_size / self.block_size]) 609 self.hashtree_info.hashtree_range = RangeSet( 610 data=[adjusted_size / self.block_size, 611 (adjusted_size + verity_tree_size) / self.block_size]) 612 613 def _ParseHashtreeMetadata(self): 614 """Parses the hash_algorithm, root_hash, salt from the metadata block.""" 615 616 metadata_start = self.filesystem_size + self.hashtree_size 617 metadata_range = RangeSet( 618 data=[metadata_start / self.block_size, 619 (metadata_start + self.metadata_size) / self.block_size]) 620 meta_data = ''.join(self.image.ReadRangeSet(metadata_range)) 621 622 # More info about the metadata structure available in: 623 # system/extras/verity/build_verity_metadata.py 624 META_HEADER_SIZE = 268 625 header_bin = meta_data[0:META_HEADER_SIZE] 626 header = struct.unpack("II256sI", header_bin) 627 628 # header: magic_number, version, signature, table_len 629 assert header[0] == 0xb001b001, header[0] 630 table_len = header[3] 631 verity_table = meta_data[META_HEADER_SIZE: META_HEADER_SIZE + table_len] 632 table_entries = verity_table.rstrip().split() 633 634 # Expected verity table format: "1 block_device block_device block_size 635 # block_size data_blocks data_blocks hash_algorithm root_hash salt" 636 assert len(table_entries) == 10, "Unexpected verity table size {}".format( 637 len(table_entries)) 638 assert (int(table_entries[3]) == self.block_size and 639 int(table_entries[4]) == self.block_size) 640 assert (int(table_entries[5]) * self.block_size == self.filesystem_size and 641 int(table_entries[6]) * self.block_size == self.filesystem_size) 642 643 self.hashtree_info.hash_algorithm = table_entries[7] 644 self.hashtree_info.root_hash = table_entries[8] 645 self.hashtree_info.salt = table_entries[9] 646 647 def ValidateHashtree(self): 648 """Checks that we can reconstruct the verity hash tree.""" 649 650 # Writes the filesystem section to a temp file; and calls the executable 651 # build_verity_tree to construct the hash tree. 652 adjusted_partition = common.MakeTempFile(prefix="adjusted_partition") 653 with open(adjusted_partition, "wb") as fd: 654 self.image.WriteRangeDataToFd(self.hashtree_info.filesystem_range, fd) 655 656 generated_verity_tree = common.MakeTempFile(prefix="verity") 657 root_hash, salt = BuildVerityTree(adjusted_partition, generated_verity_tree) 658 659 # The salt should be always identical, as we use fixed value. 660 assert salt == self.hashtree_info.salt, \ 661 "Calculated salt {} doesn't match the one in metadata {}".format( 662 salt, self.hashtree_info.salt) 663 664 if root_hash != self.hashtree_info.root_hash: 665 logger.warning( 666 "Calculated root hash %s doesn't match the one in metadata %s", 667 root_hash, self.hashtree_info.root_hash) 668 return False 669 670 # Reads the generated hash tree and checks if it has the exact same bytes 671 # as the one in the sparse image. 672 with open(generated_verity_tree, "rb") as fd: 673 return fd.read() == ''.join(self.image.ReadRangeSet( 674 self.hashtree_info.hashtree_range)) 675 676 def Generate(self, image): 677 """Parses and validates the hashtree info in a sparse image. 678 679 Returns: 680 hashtree_info: The information needed to reconstruct the hashtree. 681 682 Raises: 683 HashtreeInfoGenerationError: If we fail to generate the exact bytes of 684 the hashtree. 685 """ 686 687 self.DecomposeSparseImage(image) 688 self._ParseHashtreeMetadata() 689 690 if not self.ValidateHashtree(): 691 raise HashtreeInfoGenerationError("Failed to reconstruct the verity tree") 692 693 return self.hashtree_info 694