Resign apks contained in apex
Some apex payload images contain apk files. And these apks need to be signed during the signing processed when sign_target_files_apks is called. To support the signing, we can extract the payload and repack the apex file with the (de)apexer tool. Add the signing support in the apex_util. Bug: 146508800 Test: unit tests pass, run sign_apex, sign_target_files_apks Change-Id: If6d58975248709a144b07dbabf47c27916e5695e
This commit is contained in:
parent
eb8a0a0036
commit
88a759d651
|
@ -18,6 +18,7 @@ import logging
|
|||
import os.path
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
import common
|
||||
|
@ -41,6 +42,130 @@ class ApexSigningError(Exception):
|
|||
Exception.__init__(self, message)
|
||||
|
||||
|
||||
class ApexApkSigner(object):
|
||||
"""Class to sign the apk files in a apex payload image and repack the apex"""
|
||||
|
||||
def __init__(self, apex_path, key_passwords, codename_to_api_level_map):
|
||||
self.apex_path = apex_path
|
||||
self.key_passwords = key_passwords
|
||||
self.codename_to_api_level_map = codename_to_api_level_map
|
||||
|
||||
def ProcessApexFile(self, apk_keys, payload_key, payload_public_key):
|
||||
"""Scans and signs the apk files and repack the apex
|
||||
|
||||
Args:
|
||||
apk_keys: A dict that holds the signing keys for apk files.
|
||||
payload_key: The path to the apex payload signing key.
|
||||
payload_public_key: The path to the public key corresponding to the
|
||||
payload signing key.
|
||||
|
||||
Returns:
|
||||
The repacked apex file containing the signed apk files.
|
||||
"""
|
||||
list_cmd = ['deapexer', 'list', self.apex_path]
|
||||
entries_names = common.RunAndCheckOutput(list_cmd).split()
|
||||
apk_entries = [name for name in entries_names if name.endswith('.apk')]
|
||||
|
||||
# No need to sign and repack, return the original apex path.
|
||||
if not apk_entries:
|
||||
logger.info('No apk file to sign in %s', self.apex_path)
|
||||
return self.apex_path
|
||||
|
||||
for entry in apk_entries:
|
||||
apk_name = os.path.basename(entry)
|
||||
if apk_name not in apk_keys:
|
||||
raise ApexSigningError('Failed to find signing keys for apk file {} in'
|
||||
' apex {}. Use "-e <apkname>=" to specify a key'
|
||||
.format(entry, self.apex_path))
|
||||
if not any(dirname in entry for dirname in ['app/', 'priv-app/',
|
||||
'overlay/']):
|
||||
logger.warning('Apk path does not contain the intended directory name:'
|
||||
' %s', entry)
|
||||
|
||||
payload_dir, has_signed_apk = self.ExtractApexPayloadAndSignApks(
|
||||
apk_entries, apk_keys)
|
||||
if not has_signed_apk:
|
||||
logger.info('No apk file has been signed in %s', self.apex_path)
|
||||
return self.apex_path
|
||||
|
||||
return self.RepackApexPayload(payload_dir, payload_key, payload_public_key)
|
||||
|
||||
def ExtractApexPayloadAndSignApks(self, apk_entries, apk_keys):
|
||||
"""Extracts the payload image and signs the containing apk files."""
|
||||
payload_dir = common.MakeTempDir()
|
||||
extract_cmd = ['deapexer', 'extract', self.apex_path, payload_dir]
|
||||
common.RunAndCheckOutput(extract_cmd)
|
||||
|
||||
has_signed_apk = False
|
||||
for entry in apk_entries:
|
||||
apk_path = os.path.join(payload_dir, entry)
|
||||
assert os.path.exists(self.apex_path)
|
||||
|
||||
key_name = apk_keys.get(os.path.basename(entry))
|
||||
if key_name in common.SPECIAL_CERT_STRINGS:
|
||||
logger.info('Not signing: %s due to special cert string', apk_path)
|
||||
continue
|
||||
|
||||
logger.info('Signing apk file %s in apex %s', apk_path, self.apex_path)
|
||||
# Rename the unsigned apk and overwrite the original apk path with the
|
||||
# signed apk file.
|
||||
unsigned_apk = common.MakeTempFile()
|
||||
os.rename(apk_path, unsigned_apk)
|
||||
common.SignFile(unsigned_apk, apk_path, key_name, self.key_passwords,
|
||||
codename_to_api_level_map=self.codename_to_api_level_map)
|
||||
has_signed_apk = True
|
||||
return payload_dir, has_signed_apk
|
||||
|
||||
def RepackApexPayload(self, payload_dir, payload_key, payload_public_key):
|
||||
"""Rebuilds the apex file with the updated payload directory."""
|
||||
apex_dir = common.MakeTempDir()
|
||||
# Extract the apex file and reuse its meta files as repack parameters.
|
||||
common.UnzipToDir(self.apex_path, apex_dir)
|
||||
|
||||
android_jar_path = common.OPTIONS.android_jar_path
|
||||
if not android_jar_path:
|
||||
android_jar_path = os.path.join(os.environ.get(
|
||||
'ANDROID_BUILD_TOP'), 'prebuilts/sdk/current/public/android.jar')
|
||||
logger.warning('android_jar_path not found in options, falling back to'
|
||||
' use %s', android_jar_path)
|
||||
|
||||
arguments_dict = {
|
||||
'manifest': os.path.join(apex_dir, 'apex_manifest.pb'),
|
||||
'build_info': os.path.join(apex_dir, 'apex_build_info.pb'),
|
||||
'assets_dir': os.path.join(apex_dir, 'assets'),
|
||||
'android_jar_path': android_jar_path,
|
||||
'key': payload_key,
|
||||
'pubkey': payload_public_key,
|
||||
}
|
||||
for filename in arguments_dict.values():
|
||||
assert os.path.exists(filename), 'file {} not found'.format(filename)
|
||||
|
||||
# The repack process will add back these files later in the payload image.
|
||||
for name in ['apex_manifest.pb', 'apex_manifest.json', 'lost+found']:
|
||||
path = os.path.join(payload_dir, name)
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
elif os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
|
||||
repacked_apex = common.MakeTempFile(suffix='.apex')
|
||||
repack_cmd = ['apexer', '--force', '--include_build_info',
|
||||
'--do_not_check_keyname', '--apexer_tool_path',
|
||||
os.getenv('PATH')]
|
||||
for key, val in arguments_dict.items():
|
||||
repack_cmd.append('--' + key)
|
||||
repack_cmd.append(val)
|
||||
manifest_json = os.path.join(apex_dir, 'apex_manifest.json')
|
||||
if os.path.exists(manifest_json):
|
||||
repack_cmd.append('--manifest_json')
|
||||
repack_cmd.append(manifest_json)
|
||||
repack_cmd.append(payload_dir)
|
||||
repack_cmd.append(repacked_apex)
|
||||
common.RunAndCheckOutput(repack_cmd)
|
||||
|
||||
return repacked_apex
|
||||
|
||||
|
||||
def SignApexPayload(avbtool, payload_file, payload_key_path, payload_key_name,
|
||||
algorithm, salt, no_hashtree, signing_args=None):
|
||||
"""Signs a given payload_file with the payload key."""
|
||||
|
@ -155,7 +280,8 @@ def ParseApexPayloadInfo(avbtool, payload_path):
|
|||
|
||||
|
||||
def SignApex(avbtool, apex_data, payload_key, container_key, container_pw,
|
||||
codename_to_api_level_map, no_hashtree, signing_args=None):
|
||||
apk_keys, codename_to_api_level_map,
|
||||
no_hashtree, signing_args=None):
|
||||
"""Signs the current APEX with the given payload/container keys.
|
||||
|
||||
Args:
|
||||
|
@ -163,6 +289,7 @@ def SignApex(avbtool, apex_data, payload_key, container_key, container_pw,
|
|||
payload_key: The path to payload signing key (w/ extension).
|
||||
container_key: The path to container signing key (w/o extension).
|
||||
container_pw: The matching password of the container_key, or None.
|
||||
apk_keys: A dict that holds the signing keys for apk files.
|
||||
codename_to_api_level_map: A dict that maps from codename to API level.
|
||||
no_hashtree: Don't include hashtree in the signed APEX.
|
||||
signing_args: Additional args to be passed to the payload signer.
|
||||
|
@ -177,7 +304,15 @@ def SignApex(avbtool, apex_data, payload_key, container_key, container_pw,
|
|||
APEX_PAYLOAD_IMAGE = 'apex_payload.img'
|
||||
APEX_PUBKEY = 'apex_pubkey'
|
||||
|
||||
# 1a. Extract and sign the APEX_PAYLOAD_IMAGE entry with the given
|
||||
# 1. Extract the apex payload image and sign the containing apk files. Repack
|
||||
# the apex file after signing.
|
||||
payload_public_key = common.ExtractAvbPublicKey(avbtool, payload_key)
|
||||
apk_signer = ApexApkSigner(apex_file, container_pw,
|
||||
codename_to_api_level_map)
|
||||
apex_file = apk_signer.ProcessApexFile(apk_keys, payload_key,
|
||||
payload_public_key)
|
||||
|
||||
# 2a. Extract and sign the APEX_PAYLOAD_IMAGE entry with the given
|
||||
# payload_key.
|
||||
payload_dir = common.MakeTempDir(prefix='apex-payload-')
|
||||
with zipfile.ZipFile(apex_file) as apex_fd:
|
||||
|
@ -195,8 +330,7 @@ def SignApex(avbtool, apex_data, payload_key, container_key, container_pw,
|
|||
no_hashtree,
|
||||
signing_args)
|
||||
|
||||
# 1b. Update the embedded payload public key.
|
||||
payload_public_key = common.ExtractAvbPublicKey(avbtool, payload_key)
|
||||
# 2b. Update the embedded payload public key.
|
||||
|
||||
common.ZipDelete(apex_file, APEX_PAYLOAD_IMAGE)
|
||||
if APEX_PUBKEY in zip_items:
|
||||
|
@ -206,11 +340,11 @@ def SignApex(avbtool, apex_data, payload_key, container_key, container_pw,
|
|||
common.ZipWrite(apex_zip, payload_public_key, arcname=APEX_PUBKEY)
|
||||
common.ZipClose(apex_zip)
|
||||
|
||||
# 2. Align the files at page boundary (same as in apexer).
|
||||
# 3. Align the files at page boundary (same as in apexer).
|
||||
aligned_apex = common.MakeTempFile(prefix='apex-container-', suffix='.apex')
|
||||
common.RunAndCheckOutput(['zipalign', '-f', '4096', apex_file, aligned_apex])
|
||||
|
||||
# 3. Sign the APEX container with container_key.
|
||||
# 4. Sign the APEX container with container_key.
|
||||
signed_apex = common.MakeTempFile(prefix='apex-container-', suffix='.apex')
|
||||
|
||||
# Specify the 4K alignment when calling SignApk.
|
||||
|
|
|
@ -69,6 +69,7 @@ class Options(object):
|
|||
self.extra_signapk_args = []
|
||||
self.java_path = "java" # Use the one on the path by default.
|
||||
self.java_args = ["-Xmx2048m"] # The default JVM args.
|
||||
self.android_jar_path = None
|
||||
self.public_key_suffix = ".x509.pem"
|
||||
self.private_key_suffix = ".pk8"
|
||||
# use otatools built boot_signer by default
|
||||
|
@ -1823,7 +1824,7 @@ def ParseOptions(argv,
|
|||
argv, "hvp:s:x:" + extra_opts,
|
||||
["help", "verbose", "path=", "signapk_path=",
|
||||
"signapk_shared_library_path=", "extra_signapk_args=",
|
||||
"java_path=", "java_args=", "public_key_suffix=",
|
||||
"java_path=", "java_args=", "android_jar_path=", "public_key_suffix=",
|
||||
"private_key_suffix=", "boot_signer_path=", "boot_signer_args=",
|
||||
"verity_signer_path=", "verity_signer_args=", "device_specific=",
|
||||
"extra=", "logfile=", "aftl_server=", "aftl_key_path=",
|
||||
|
@ -1852,6 +1853,8 @@ def ParseOptions(argv,
|
|||
OPTIONS.java_path = a
|
||||
elif o in ("--java_args",):
|
||||
OPTIONS.java_args = shlex.split(a)
|
||||
elif o in ("--android_jar_path",):
|
||||
OPTIONS.android_jar_path = a
|
||||
elif o in ("--public_key_suffix",):
|
||||
OPTIONS.public_key_suffix = a
|
||||
elif o in ("--private_key_suffix",):
|
||||
|
|
|
@ -31,6 +31,10 @@ Usage: sign_apex [flags] input_apex_file output_apex_file
|
|||
--payload_extra_args <args>
|
||||
Optional flag that specifies any extra args to be passed to payload signer
|
||||
(e.g. --payload_extra_args="--signing_helper_with_files /path/to/helper").
|
||||
|
||||
-e (--extra_apks) <name,name,...=key>
|
||||
Add extra APK name/key pairs. This is useful to sign the apk files in the
|
||||
apex payload image.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
@ -43,8 +47,8 @@ import common
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def SignApexFile(avbtool, apex_file, payload_key, container_key,
|
||||
no_hashtree, signing_args=None):
|
||||
def SignApexFile(avbtool, apex_file, payload_key, container_key, no_hashtree,
|
||||
apk_keys=None, signing_args=None):
|
||||
"""Signs the given apex file."""
|
||||
with open(apex_file, 'rb') as input_fp:
|
||||
apex_data = input_fp.read()
|
||||
|
@ -57,6 +61,7 @@ def SignApexFile(avbtool, apex_file, payload_key, container_key,
|
|||
container_pw=None,
|
||||
codename_to_api_level_map=None,
|
||||
no_hashtree=no_hashtree,
|
||||
apk_keys=apk_keys,
|
||||
signing_args=signing_args)
|
||||
|
||||
|
||||
|
@ -77,18 +82,26 @@ def main(argv):
|
|||
options['payload_key'] = a
|
||||
elif o == '--payload_extra_args':
|
||||
options['payload_extra_args'] = a
|
||||
elif o in ("-e", "--extra_apks"):
|
||||
names, key = a.split("=")
|
||||
names = names.split(",")
|
||||
for n in names:
|
||||
if 'extra_apks' not in options:
|
||||
options['extra_apks'] = {}
|
||||
options['extra_apks'].update({n: key})
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
args = common.ParseOptions(
|
||||
argv, __doc__,
|
||||
extra_opts='',
|
||||
extra_opts='e:',
|
||||
extra_long_opts=[
|
||||
'avbtool=',
|
||||
'container_key=',
|
||||
'payload_extra_args=',
|
||||
'payload_key=',
|
||||
'extra_apks=',
|
||||
],
|
||||
extra_option_handler=option_handler)
|
||||
|
||||
|
@ -105,6 +118,7 @@ def main(argv):
|
|||
options['payload_key'],
|
||||
options['container_key'],
|
||||
no_hashtree=False,
|
||||
apk_keys=options.get('extra_apks', {}),
|
||||
signing_args=options.get('payload_extra_args'))
|
||||
shutil.copyfile(signed_apex, args[1])
|
||||
logger.info("done.")
|
||||
|
|
|
@ -103,6 +103,9 @@ Usage: sign_target_files_apks [flags] input_target_files output_target_files
|
|||
Specify any additional args that are needed to AVB-sign the image
|
||||
(e.g. "--signing_helper /path/to/helper"). The args will be appended to
|
||||
the existing ones in info dict.
|
||||
|
||||
--android_jar_path <path>
|
||||
Path to the android.jar to repack the apex file.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
@ -151,6 +154,7 @@ OPTIONS.tag_changes = ("-test-keys", "-dev-keys", "+release-keys")
|
|||
OPTIONS.avb_keys = {}
|
||||
OPTIONS.avb_algorithms = {}
|
||||
OPTIONS.avb_extra_args = {}
|
||||
OPTIONS.android_jar_path = None
|
||||
|
||||
|
||||
AVB_FOOTER_ARGS_BY_PARTITION = {
|
||||
|
@ -492,6 +496,7 @@ def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info,
|
|||
payload_key,
|
||||
container_key,
|
||||
key_passwords[container_key],
|
||||
apk_keys,
|
||||
codename_to_api_level_map,
|
||||
no_hashtree=True,
|
||||
signing_args=OPTIONS.avb_extra_args.get('apex'))
|
||||
|
@ -1247,6 +1252,8 @@ def main(argv):
|
|||
apex_keys_info = ReadApexKeysInfo(input_zip)
|
||||
apex_keys = GetApexKeys(apex_keys_info, apk_keys)
|
||||
|
||||
# TODO(xunchang) check for the apks inside the apex files, and abort early if
|
||||
# the keys are not available.
|
||||
CheckApkAndApexKeysAvailable(
|
||||
input_zip,
|
||||
set(apk_keys.keys()) | set(apex_keys.keys()),
|
||||
|
|
|
@ -32,6 +32,8 @@ class ApexUtilsTest(test_utils.ReleaseToolsTestCase):
|
|||
# The default payload signing key.
|
||||
self.payload_key = os.path.join(self.testdata_dir, 'testkey.key')
|
||||
|
||||
common.OPTIONS.search_path = test_utils.get_search_path()
|
||||
|
||||
@staticmethod
|
||||
def _GetTestPayload():
|
||||
payload_file = common.MakeTempFile(prefix='apex-', suffix='.img')
|
||||
|
@ -126,3 +128,30 @@ class ApexUtilsTest(test_utils.ReleaseToolsTestCase):
|
|||
payload_file,
|
||||
os.path.join(self.testdata_dir, 'testkey_with_passwd.key'),
|
||||
no_hashtree=True)
|
||||
|
||||
@test_utils.SkipIfExternalToolsUnavailable()
|
||||
def test_ApexApkSigner_noApkPresent(self):
|
||||
apex_path = os.path.join(self.testdata_dir, 'foo.apex')
|
||||
signer = apex_utils.ApexApkSigner(apex_path, None, None)
|
||||
processed_apex = signer.ProcessApexFile({}, self.payload_key,
|
||||
None)
|
||||
self.assertEqual(apex_path, processed_apex)
|
||||
|
||||
@test_utils.SkipIfExternalToolsUnavailable()
|
||||
def test_ApexApkSigner_apkKeyNotPresent(self):
|
||||
apex_path = os.path.join(self.testdata_dir, 'has_apk.apex')
|
||||
signer = apex_utils.ApexApkSigner(apex_path, None, None)
|
||||
self.assertRaises(apex_utils.ApexSigningError, signer.ProcessApexFile, {},
|
||||
self.payload_key, None)
|
||||
|
||||
@test_utils.SkipIfExternalToolsUnavailable()
|
||||
def test_ApexApkSigner_signApk(self):
|
||||
apex_path = os.path.join(self.testdata_dir, 'has_apk.apex')
|
||||
signer = apex_utils.ApexApkSigner(apex_path, None, None)
|
||||
apk_keys = {'wifi-service-resources.apk': os.path.join(
|
||||
self.testdata_dir, 'testkey')}
|
||||
|
||||
self.payload_key = os.path.join(self.testdata_dir, 'testkey_RSA4096.key')
|
||||
payload_pubkey = common.ExtractAvbPublicKey('avbtool',
|
||||
self.payload_key)
|
||||
signer.ProcessApexFile(apk_keys, self.payload_key, payload_pubkey)
|
||||
|
|
|
@ -41,3 +41,19 @@ class SignApexTest(test_utils.ReleaseToolsTestCase):
|
|||
container_key,
|
||||
False)
|
||||
self.assertTrue(os.path.exists(signed_foo_apex))
|
||||
|
||||
@test_utils.SkipIfExternalToolsUnavailable()
|
||||
def test_SignApexWithApk(self):
|
||||
test_apex = os.path.join(self.testdata_dir, 'has_apk.apex')
|
||||
payload_key = os.path.join(self.testdata_dir, 'testkey_RSA4096.key')
|
||||
container_key = os.path.join(self.testdata_dir, 'testkey')
|
||||
apk_keys = {'wifi-service-resources.apk': os.path.join(
|
||||
self.testdata_dir, 'testkey')}
|
||||
signed_test_apex = sign_apex.SignApexFile(
|
||||
'avbtool',
|
||||
test_apex,
|
||||
payload_key,
|
||||
container_key,
|
||||
False,
|
||||
apk_keys)
|
||||
self.assertTrue(os.path.exists(signed_test_apex))
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue