• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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