xref: /aosp_15_r20/external/avb/tools/transparency/pixel_factory_image_verify.py (revision d289c2ba6de359471b23d594623b906876bc48a0)
1#!/usr/bin/env python
2
3# Copyright 2019, The Android Open Source Project
4#
5# Permission is hereby granted, free of charge, to any person
6# obtaining a copy of this software and associated documentation
7# files (the "Software"), to deal in the Software without
8# restriction, including without limitation the rights to use, copy,
9# modify, merge, publish, distribute, sublicense, and/or sell copies
10# of the Software, and to permit persons to whom the Software is
11# furnished to do so, subject to the following conditions:
12#
13# The above copyright notice and this permission notice shall be
14# included in all copies or substantial portions of the Software.
15#
16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
20# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
21# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
22# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23# SOFTWARE.
24#
25
26"""Tool for verifying VBMeta & calculate VBMeta Digests of Pixel factory images.
27
28If given an HTTPS URL it will download the file first before processing.
29$ pixel_factory_image_verify.py https://dl.google.com/dl/android/aosp/image.zip
30
31Otherwise, the argument is considered to be a local file.
32$ pixel_factory_image_verify.py image.zip
33
34The list of canonical Pixel factory images can be found here:
35https://developers.google.com/android/images
36
37Supported: all factory images of Pixel 6 and later devices.
38
39In order for the tool to run correct the following utilities need to be
40pre-installed: grep, wget or curl, unzip.
41
42Additionally, make sure that the bootloader unpacker script is separately
43downloaded, made executable, and symlinked as 'fbpacktool', and made accessible
44via your shell $PATH.
45
46The tool also runs outside of the repository location as long as the working
47directory is writable.
48"""
49
50from __future__ import print_function
51
52import glob
53import os
54import shutil
55import subprocess
56import sys
57import tempfile
58import distutils.spawn
59
60
61class PixelFactoryImageVerifier(object):
62  """Object for the pixel_factory_image_verify command line tool."""
63
64  ERR_TOOL_UNAVAIL_FMT_STR = 'Necessary command line tool needs to be installed first: %s'
65
66  def __init__(self):
67    self.working_dir = os.getcwd()
68    self.script_path = os.path.realpath(__file__)
69    self.script_dir = os.path.split(self.script_path)[0]
70    self.avbtool_path = os.path.abspath(os.path.join(self.script_path,
71                                                     '../../../avbtool.py'))
72    self.fw_unpacker_path = distutils.spawn.find_executable('fbpacktool')
73    self.wget_path = distutils.spawn.find_executable('wget')
74    self.curl_path = distutils.spawn.find_executable('curl')
75
76  def run(self, argv):
77    """Command line processor.
78
79    Args:
80       argv: The command line parameter list.
81    """
82    # Checks for command line parameters and show help if non given.
83    if len(argv) != 2:
84      print('No command line parameter given. At least a filename or URL for a '
85            'Pixel 3 or later factory image needs to be specified.')
86      sys.exit(1)
87
88    # Checks if necessary commands are available.
89    for cmd in ['grep', 'unzip']:
90      if not distutils.spawn.find_executable(cmd):
91        print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % cmd)
92        sys.exit(1)
93
94    # Checks if `fbpacktool` is available.
95    if not self.fw_unpacker_path:
96      print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % 'fbpacktool')
97      sys.exit(1)
98
99    # Checks if either `wget` or `curl` is available.
100    if not self.wget_path and not self.curl_path:
101      print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % 'wget or curl')
102      sys.exit(1)
103
104    # Downloads factory image if URL is specified; otherwise treat it as file.
105    if argv[1].lower().startswith('https://'):
106      factory_image_zip = self._download_factory_image(argv[1])
107      if not factory_image_zip:
108        sys.exit(1)
109    else:
110      factory_image_zip = os.path.abspath(argv[1])
111
112    # Unpacks the factory image into partition images.
113    partition_image_dir = self._unpack_factory_image(factory_image_zip)
114    if not partition_image_dir:
115      sys.exit(1)
116
117    # Unpacks bootloader image into individual component images.
118    unpack_successful = self._unpack_bootloader(partition_image_dir)
119    if not unpack_successful:
120      sys.exit(1)
121
122    # Validates the VBMeta of the factory image.
123    verified = self._verify_vbmeta_partitions(partition_image_dir)
124    if not verified:
125      sys.exit(1)
126
127    fingerprint = self._extract_build_fingerprint(partition_image_dir)
128    if not fingerprint:
129      sys.exit(1)
130
131    # Calculates the VBMeta Digest for the factory image.
132    vbmeta_digest = self._calculate_vbmeta_digest(partition_image_dir)
133    if not vbmeta_digest:
134      sys.exit(1)
135
136    print('The build fingerprint for factory image is: %s' % fingerprint)
137    print('The VBMeta Digest for factory image is: %s' % vbmeta_digest)
138
139    with open('payload.txt', 'w') as f_out:
140      f_out.write(fingerprint.strip() + '\n')
141      f_out.write(vbmeta_digest.strip() + '\n')
142    print('A corresponding "payload.txt" file has been created.')
143    sys.exit(0)
144
145  def _download_factory_image(self, url):
146    """Downloads the factory image to the working directory.
147
148    Args:
149      url: The download URL for the factory image.
150
151    Returns:
152      The absolute path to the factory image or None if it failed.
153    """
154    # Creates temporary download folder.
155    download_path = tempfile.mkdtemp(dir=self.working_dir)
156
157    # Downloads the factory image to the temporary folder.
158    download_filename = self._download_file(download_path, url)
159    if not download_filename:
160      return None
161
162    # Moves the downloaded file into the working directory.
163    download_file = os.path.join(download_path, download_filename)
164    target_file = os.path.join(self.working_dir, download_filename)
165    if os.path.exists(target_file):
166      try:
167        os.remove(target_file)
168      except OSError as e:
169        print('File %s already exists and cannot be deleted.' % download_file)
170        return None
171    try:
172      shutil.move(download_file, self.working_dir)
173    except shutil.Error as e:
174      print('File %s cannot be moved to %s: %s' % (download_file,
175                                                   target_file, e))
176      return None
177
178    # Removes temporary download folder.
179    try:
180      shutil.rmtree(download_path)
181    except shutil.Error as e:
182      print('Temporary download folder %s could not be removed.'
183            % download_path)
184    return os.path.join(self.working_dir, download_filename)
185
186  def _download_file(self, download_dir, url):
187    """Downloads a file from the Internet.
188
189    Args:
190      download_dir: The folder the file should be downloaded to.
191      url: The download URL for the file.
192
193    Returns:
194      The name of the downloaded file as it apears on disk; otherwise None
195      if download failed.
196    """
197    print('Fetching file from: %s' % url)
198    os.chdir(download_dir)
199    args = []
200    if self.wget_path:
201      args = [self.wget_path, url]
202    else:
203      args = [self.curl_path, '-O', url]
204
205    result, _ = self._run_command(args,
206                                  'Successfully downloaded file.',
207                                  'File download failed.')
208    os.chdir(self.working_dir)
209    if not result:
210      return None
211
212    # Figure out the file name of what was downloaded: It will be the only file
213    # in the download folder.
214    files = os.listdir(download_dir)
215    if files and len(files) == 1:
216      return files[0]
217    else:
218      return None
219
220  def _unpack_bootloader(self, factory_image_folder):
221    """Unpacks the bootloader to produce individual images.
222
223    Args:
224      factory_image_folder: path to the directory containing factory images.
225
226    Returns:
227      True if unpack is successful. False if otherwise.
228    """
229    os.chdir(factory_image_folder)
230    bootloader_path = os.path.join(factory_image_folder, 'bootloader*.img')
231    glob_result = glob.glob(bootloader_path)
232    if not glob_result:
233      return False
234
235    args = [self.fw_unpacker_path, 'unpack', glob_result[0]]
236    result, _ = self._run_command(args,
237                                  'Successfully unpacked bootloader image.',
238                                  'Failed to unpack bootloader image.')
239    return result
240
241  def _unpack_factory_image(self, factory_image_file):
242    """Unpacks the factory image zip file.
243
244    Args:
245      factory_image_file: path and file name to the image file.
246
247    Returns:
248      The path to the folder which contains the unpacked factory image files or
249      None if it failed.
250    """
251    unpack_dir = tempfile.mkdtemp(dir=self.working_dir)
252    args = ['unzip', factory_image_file, '-d', unpack_dir]
253    result, _ = self._run_command(args,
254                                  'Successfully unpacked factory image.',
255                                  'Failed to unpack factory image.')
256    if not result:
257      return None
258
259    # Locate the directory which contains the image files.
260    files = os.listdir(unpack_dir)
261    image_name = None
262    for f in files:
263      path = os.path.join(self.working_dir, unpack_dir, f)
264      if os.path.isdir(path):
265        image_name = f
266        break
267    if not image_name:
268      print('No image found: %s' % image_name)
269      return None
270
271    # Move image file directory to the working directory
272    image_dir = os.path.join(unpack_dir, image_name)
273    target_dir = os.path.join(self.working_dir, image_name)
274    if os.path.exists(target_dir):
275      try:
276        shutil.rmtree(target_dir)
277      except shutil.Error as e:
278        print('Directory %s already exists and cannot be deleted.' % target_dir)
279        return None
280
281    try:
282      shutil.move(image_dir, self.working_dir)
283    except shutil.Error as e:
284      print('Directory %s could not be moved to %s: %s' % (image_dir,
285                                                           self.working_dir, e))
286      return None
287
288    # Removes tmp unpack directory.
289    try:
290      shutil.rmtree(unpack_dir)
291    except shutil.Error as e:
292      print('Temporary download folder %s could not be removed.'
293            % unpack_dir)
294
295    # Unzip the secondary zip file which contain the individual images.
296    image_filename = 'image-%s' % image_name
297    image_folder = os.path.join(self.working_dir, image_name)
298    os.chdir(image_folder)
299
300    args = ['unzip', image_filename]
301    result, _ = self._run_command(
302        args,
303        'Successfully unpacked factory image partitions.',
304        'Failed to unpack factory image partitions.')
305    if not result:
306      return None
307    return image_folder
308
309  def _verify_vbmeta_partitions(self, image_dir):
310    """Verifies all partitions protected by VBMeta using avbtool verify_image.
311
312    Args:
313      image_dir: The folder containing the unpacked factory image partitions,
314      which contains a vbmeta.img patition.
315
316    Returns:
317      True if the VBMeta protected partitions verify.
318    """
319    os.chdir(image_dir)
320    args = [self.avbtool_path,
321            'verify_image',
322            '--image', 'vbmeta.img',
323            '--follow_chain_partitions']
324    result, _ = self._run_command(args,
325                                  'Successfully verified VBmeta.',
326                                  'Verification of VBmeta failed.')
327    os.chdir(self.working_dir)
328    return result
329
330  def _extract_build_fingerprint(self, image_dir):
331    """Extracts the build fingerprint from the system.img.
332    Args:
333      image_dir: The folder containing the unpacked factory image partitions,
334        which contains a vbmeta.img patition.
335
336    Returns:
337      The build fingerprint string, e.g.
338      google/blueline/blueline:9/PQ2A.190305.002/5240760:user/release-keys
339    """
340    os.chdir(image_dir)
341    args = ['grep',
342            '-a',
343            'ro\..*build\.fingerprint=google/.*/release-keys',
344            'system.img']
345
346    result, output = self._run_command(
347        args,
348        'Successfully extracted build fingerprint.',
349        'Build fingerprint extraction failed.')
350    os.chdir(self.working_dir)
351    if result:
352      _, fingerprint = output.split('=', 1)
353      return fingerprint.rstrip()
354    else:
355      return None
356
357  def _calculate_vbmeta_digest(self, image_dir):
358    """Calculates the VBMeta Digest for given partitions using avbtool.
359
360    Args:
361      image_dir: The folder containing the unpacked factory image partitions,
362        which contains a vbmeta.img partition.
363
364    Returns:
365      Hex string with the VBmeta Digest value or None if it failed.
366    """
367    os.chdir(image_dir)
368    args = [self.avbtool_path,
369            'calculate_vbmeta_digest',
370            '--image', 'vbmeta.img']
371    result, output = self._run_command(args,
372                                       'Successfully calculated VBMeta Digest.',
373                                       'Failed to calculate VBmeta Digest.')
374    os.chdir(self.working_dir)
375    if result:
376      return output
377    else:
378      return None
379
380  def _run_command(self, args, success_msg, fail_msg):
381    """Runs command line tools."""
382    p = subprocess.Popen(args, stdin=subprocess.PIPE,
383                         stdout=subprocess.PIPE, stderr=subprocess.PIPE,
384                         encoding='utf-8')
385    pout, _ = p.communicate()
386    if p.wait() == 0:
387      print(success_msg)
388      return True, pout
389    else:
390      print(fail_msg)
391      return False, pout
392
393
394if __name__ == '__main__':
395  tool = PixelFactoryImageVerifier()
396  tool.run(sys.argv)
397
398