blob: 2040ab9852844d4f02e3938004d1228cf83c5361 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2019, The Android Open Source Project
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use, copy,
# modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
"""Tool for verifying VBMeta & calculate VBMeta Digests of Pixel factory images.
If given an HTTPS URL it will download the file first before processing.
$ pixel_factory_image_verify.py https://dl.google.com/dl/android/aosp/image.zip
Otherwise, the argument is considered to be a local file.
$ pixel_factory_image_verify.py image.zip
The list of canonical Pixel factory images can be found here:
https://developers.google.com/android/images
Supported: all factory images of Pixel 6 and later devices.
In order for the tool to run correct the following utilities need to be
pre-installed: grep, wget or curl, unzip.
Additionally, make sure that the bootloader unpacker script is separately
downloaded, made executable, and symlinked as 'fbpacktool', and made accessible
via your shell $PATH.
The tool also runs outside of the repository location as long as the working
directory is writable.
"""
from __future__ import print_function
import glob
import os
import shutil
import subprocess
import sys
import tempfile
import distutils.spawn
class PixelFactoryImageVerifier(object):
"""Object for the pixel_factory_image_verify command line tool."""
ERR_TOOL_UNAVAIL_FMT_STR = 'Necessary command line tool needs to be installed first: %s'
def __init__(self):
self.working_dir = os.getcwd()
self.script_path = os.path.realpath(__file__)
self.script_dir = os.path.split(self.script_path)[0]
self.avbtool_path = os.path.abspath(os.path.join(self.script_path,
'../../../avbtool'))
self.fw_unpacker_path = distutils.spawn.find_executable('fbpacktool')
self.wget_path = distutils.spawn.find_executable('wget')
self.curl_path = distutils.spawn.find_executable('curl')
def run(self, argv):
"""Command line processor.
Args:
argv: The command line parameter list.
"""
# Checks for command line parameters and show help if non given.
if len(argv) != 2:
print('No command line parameter given. At least a filename or URL for a '
'Pixel 3 or later factory image needs to be specified.')
sys.exit(1)
# Checks if necessary commands are available.
for cmd in ['grep', 'unzip']:
if not distutils.spawn.find_executable(cmd):
print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % cmd)
sys.exit(1)
# Checks if `fbpacktool` is available.
if not self.fw_unpacker_path:
print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % 'fbpacktool')
sys.exit(1)
# Checks if either `wget` or `curl` is available.
if not self.wget_path and not self.curl_path:
print(PixelFactoryImageVerifier.ERR_TOOL_UNAVAIL_FMT_STR % 'wget or curl')
sys.exit(1)
# Downloads factory image if URL is specified; otherwise treat it as file.
if argv[1].lower().startswith('https://'):
factory_image_zip = self._download_factory_image(argv[1])
if not factory_image_zip:
sys.exit(1)
else:
factory_image_zip = os.path.abspath(argv[1])
# Unpacks the factory image into partition images.
partition_image_dir = self._unpack_factory_image(factory_image_zip)
if not partition_image_dir:
sys.exit(1)
# Unpacks bootloader image into individual component images.
unpack_successful = self._unpack_bootloader(partition_image_dir)
if not unpack_successful:
sys.exit(1)
# Validates the VBMeta of the factory image.
verified = self._verify_vbmeta_partitions(partition_image_dir)
if not verified:
sys.exit(1)
fingerprint = self._extract_build_fingerprint(partition_image_dir)
if not fingerprint:
sys.exit(1)
# Calculates the VBMeta Digest for the factory image.
vbmeta_digest = self._calculate_vbmeta_digest(partition_image_dir)
if not vbmeta_digest:
sys.exit(1)
print('The build fingerprint for factory image is: %s' % fingerprint)
print('The VBMeta Digest for factory image is: %s' % vbmeta_digest)
with open('payload.txt', 'w') as f_out:
f_out.write(fingerprint.strip() + '\n')
f_out.write(vbmeta_digest.strip() + '\n')
print('A corresponding "payload.txt" file has been created.')
sys.exit(0)
def _download_factory_image(self, url):
"""Downloads the factory image to the working directory.
Args:
url: The download URL for the factory image.
Returns:
The absolute path to the factory image or None if it failed.
"""
# Creates temporary download folder.
download_path = tempfile.mkdtemp(dir=self.working_dir)
# Downloads the factory image to the temporary folder.
download_filename = self._download_file(download_path, url)
if not download_filename:
return None
# Moves the downloaded file into the working directory.
download_file = os.path.join(download_path, download_filename)
target_file = os.path.join(self.working_dir, download_filename)
if os.path.exists(target_file):
try:
os.remove(target_file)
except OSError as e:
print('File %s already exists and cannot be deleted.' % download_file)
return None
try:
shutil.move(download_file, self.working_dir)
except shutil.Error as e:
print('File %s cannot be moved to %s: %s' % (download_file,
target_file, e))
return None
# Removes temporary download folder.
try:
shutil.rmtree(download_path)
except shutil.Error as e:
print('Temporary download folder %s could not be removed.'
% download_path)
return os.path.join(self.working_dir, download_filename)
def _download_file(self, download_dir, url):
"""Downloads a file from the Internet.
Args:
download_dir: The folder the file should be downloaded to.
url: The download URL for the file.
Returns:
The name of the downloaded file as it apears on disk; otherwise None
if download failed.
"""
print('Fetching file from: %s' % url)
os.chdir(download_dir)
args = []
if self.wget_path:
args = [self.wget_path, url]
else:
args = [self.curl_path, '-O', url]
result, _ = self._run_command(args,
'Successfully downloaded file.',
'File download failed.')
os.chdir(self.working_dir)
if not result:
return None
# Figure out the file name of what was downloaded: It will be the only file
# in the download folder.
files = os.listdir(download_dir)
if files and len(files) == 1:
return files[0]
else:
return None
def _unpack_bootloader(self, factory_image_folder):
"""Unpacks the bootloader to produce individual images.
Args:
factory_image_folder: path to the directory containing factory images.
Returns:
True if unpack is successful. False if otherwise.
"""
os.chdir(factory_image_folder)
bootloader_path = os.path.join(factory_image_folder, 'bootloader*.img')
glob_result = glob.glob(bootloader_path)
if not glob_result:
return False
args = [self.fw_unpacker_path, 'unpack', glob_result[0]]
result, _ = self._run_command(args,
'Successfully unpacked bootloader image.',
'Failed to unpack bootloader image.')
return result
def _unpack_factory_image(self, factory_image_file):
"""Unpacks the factory image zip file.
Args:
factory_image_file: path and file name to the image file.
Returns:
The path to the folder which contains the unpacked factory image files or
None if it failed.
"""
unpack_dir = tempfile.mkdtemp(dir=self.working_dir)
args = ['unzip', factory_image_file, '-d', unpack_dir]
result, _ = self._run_command(args,
'Successfully unpacked factory image.',
'Failed to unpack factory image.')
if not result:
return None
# Locate the directory which contains the image files.
files = os.listdir(unpack_dir)
image_name = None
for f in files:
path = os.path.join(self.working_dir, unpack_dir, f)
if os.path.isdir(path):
image_name = f
break
if not image_name:
print('No image found: %s' % image_name)
return None
# Move image file directory to the working directory
image_dir = os.path.join(unpack_dir, image_name)
target_dir = os.path.join(self.working_dir, image_name)
if os.path.exists(target_dir):
try:
shutil.rmtree(target_dir)
except shutil.Error as e:
print('Directory %s already exists and cannot be deleted.' % target_dir)
return None
try:
shutil.move(image_dir, self.working_dir)
except shutil.Error as e:
print('Directory %s could not be moved to %s: %s' % (image_dir,
self.working_dir, e))
return None
# Removes tmp unpack directory.
try:
shutil.rmtree(unpack_dir)
except shutil.Error as e:
print('Temporary download folder %s could not be removed.'
% unpack_dir)
# Unzip the secondary zip file which contain the individual images.
image_filename = 'image-%s' % image_name
image_folder = os.path.join(self.working_dir, image_name)
os.chdir(image_folder)
args = ['unzip', image_filename]
result, _ = self._run_command(
args,
'Successfully unpacked factory image partitions.',
'Failed to unpack factory image partitions.')
if not result:
return None
return image_folder
def _verify_vbmeta_partitions(self, image_dir):
"""Verifies all partitions protected by VBMeta using avbtool verify_image.
Args:
image_dir: The folder containing the unpacked factory image partitions,
which contains a vbmeta.img patition.
Returns:
True if the VBMeta protected partitions verify.
"""
os.chdir(image_dir)
args = [self.avbtool_path,
'verify_image',
'--image', 'vbmeta.img',
'--follow_chain_partitions']
result, _ = self._run_command(args,
'Successfully verified VBmeta.',
'Verification of VBmeta failed.')
os.chdir(self.working_dir)
return result
def _extract_build_fingerprint(self, image_dir):
"""Extracts the build fingerprint from the system.img.
Args:
image_dir: The folder containing the unpacked factory image partitions,
which contains a vbmeta.img patition.
Returns:
The build fingerprint string, e.g.
google/blueline/blueline:9/PQ2A.190305.002/5240760:user/release-keys
"""
os.chdir(image_dir)
args = ['grep',
'-a',
'ro\..*build\.fingerprint=google/.*/release-keys',
'system.img']
result, output = self._run_command(
args,
'Successfully extracted build fingerprint.',
'Build fingerprint extraction failed.')
os.chdir(self.working_dir)
if result:
_, fingerprint = output.split('=', 1)
return fingerprint.rstrip()
else:
return None
def _calculate_vbmeta_digest(self, image_dir):
"""Calculates the VBMeta Digest for given partitions using avbtool.
Args:
image_dir: The folder containing the unpacked factory image partitions,
which contains a vbmeta.img partition.
Returns:
Hex string with the VBmeta Digest value or None if it failed.
"""
os.chdir(image_dir)
args = [self.avbtool_path,
'calculate_vbmeta_digest',
'--image', 'vbmeta.img']
result, output = self._run_command(args,
'Successfully calculated VBMeta Digest.',
'Failed to calculate VBmeta Digest.')
os.chdir(self.working_dir)
if result:
return output
else:
return None
def _run_command(self, args, success_msg, fail_msg):
"""Runs command line tools."""
p = subprocess.Popen(args, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
encoding='utf-8')
pout, _ = p.communicate()
if p.wait() == 0:
print(success_msg)
return True, pout
else:
print(fail_msg)
return False, pout
if __name__ == '__main__':
tool = PixelFactoryImageVerifier()
tool.run(sys.argv)