From d8469727bc02eecad125b397892e4eb0f718ce17 Mon Sep 17 00:00:00 2001 From: Tao Bao Date: Fri, 15 Mar 2019 11:03:53 -0700 Subject: [PATCH 1/3] releasetools: check_target_files_signatures.py checks APEXes. Only the container certs will be checked and reported. For the payload within an APEX, we can't easily extract the cert info. It needs to go along a longer path, if ever needed, by: - extracting public keys from all the available certs; - using each of them to verify against an APEX payload to find a match (`avbtool verify_image --image payload --key public_key`). Bug: 123716522 Test: Run check_target_files_signatures.py on target_files with APEXes. Change-Id: I2ef318e05433d2d65ab84e2dff9e01fb6ee3373d --- tools/releasetools/check_target_files_signatures.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/releasetools/check_target_files_signatures.py b/tools/releasetools/check_target_files_signatures.py index 9b7695479b..4b0d4c752d 100755 --- a/tools/releasetools/check_target_files_signatures.py +++ b/tools/releasetools/check_target_files_signatures.py @@ -168,6 +168,7 @@ def CertFromPKCS7(data, filename): class APK(object): + def __init__(self, full_filename, filename): self.filename = filename self.certs = None @@ -244,12 +245,12 @@ class TargetFiles(object): # must decompress them individually before we perform any analysis. # This is the list of wildcards of files we extract from |filename|. - apk_extensions = ['*.apk'] + apk_extensions = ['*.apk', '*.apex'] self.certmap, compressed_extension = common.ReadApkCerts( - zipfile.ZipFile(filename, "r")) + zipfile.ZipFile(filename)) if compressed_extension: - apk_extensions.append("*.apk" + compressed_extension) + apk_extensions.append('*.apk' + compressed_extension) d = common.UnzipTemp(filename, apk_extensions) self.apks = {} @@ -272,7 +273,7 @@ class TargetFiles(object): os.remove(os.path.join(dirpath, fn)) fn = uncompressed_fn - if fn.endswith(".apk"): + if fn.endswith(('.apk', '.apex')): fullname = os.path.join(dirpath, fn) displayname = fullname[len(d)+1:] apk = APK(fullname, displayname) From 1cd59f2a26d592741b69d60afca59d12d3950b4c Mon Sep 17 00:00:00 2001 From: Tao Bao Date: Fri, 15 Mar 2019 15:13:01 -0700 Subject: [PATCH 2/3] releasetools: Add apex_utils.py. The added `testdata/testkey_with_passwd.key` is generated with: $ openssl pkcs8 -inform DER -in testdata/testkey_with_passwd.pk8 \ -out testdata/testkey_with_passwd.key -passin pass:foo Bug: 123716522 Test: python -m unittest test_apex_utils Change-Id: I7b7f00178ecab35c3e41ecf4d10295fe9bf480ab --- tools/releasetools/apex_utils.py | 147 ++++++++++++++++++ tools/releasetools/test_apex_utils.py | 87 +++++++++++ tools/releasetools/testdata/signing_helper.sh | 21 +++ .../testdata/testkey_with_passwd.key | 28 ++++ 4 files changed, 283 insertions(+) create mode 100644 tools/releasetools/apex_utils.py create mode 100644 tools/releasetools/test_apex_utils.py create mode 100755 tools/releasetools/testdata/signing_helper.sh create mode 100644 tools/releasetools/testdata/testkey_with_passwd.key diff --git a/tools/releasetools/apex_utils.py b/tools/releasetools/apex_utils.py new file mode 100644 index 0000000000..d14c94f7dc --- /dev/null +++ b/tools/releasetools/apex_utils.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# +# Copyright (C) 2019 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os.path +import re +import shlex +import sys + +import common + +logger = logging.getLogger(__name__) + + +class ApexInfoError(Exception): + """An Exception raised during Apex Information command.""" + + def __init__(self, message): + Exception.__init__(self, message) + + +class ApexSigningError(Exception): + """An Exception raised during Apex Payload signing.""" + + def __init__(self, message): + Exception.__init__(self, message) + + +def SignApexPayload(payload_file, payload_key_path, payload_key_name, algorithm, + salt, signing_args=None): + """Signs a given payload_file with the payload key.""" + # Add the new footer. Old footer, if any, will be replaced by avbtool. + cmd = ['avbtool', 'add_hashtree_footer', + '--do_not_generate_fec', + '--algorithm', algorithm, + '--key', payload_key_path, + '--prop', 'apex.key:{}'.format(payload_key_name), + '--image', payload_file, + '--salt', salt] + if signing_args: + cmd.extend(shlex.split(signing_args)) + + try: + common.RunAndCheckOutput(cmd) + except common.ExternalError as e: + raise ApexSigningError, \ + 'Failed to sign APEX payload {} with {}:\n{}'.format( + payload_file, payload_key_path, e), sys.exc_info()[2] + + # Verify the signed payload image with specified public key. + logger.info('Verifying %s', payload_file) + VerifyApexPayload(payload_file, payload_key_path) + + +def VerifyApexPayload(payload_file, payload_key): + """Verifies the APEX payload signature with the given key.""" + cmd = ['avbtool', 'verify_image', '--image', payload_file, + '--key', payload_key] + try: + common.RunAndCheckOutput(cmd) + except common.ExternalError as e: + raise ApexSigningError, \ + 'Failed to validate payload signing for {} with {}:\n{}'.format( + payload_file, payload_key, e), sys.exc_info()[2] + + +def ParseApexPayloadInfo(payload_path): + """Parses the APEX payload info. + + Args: + payload_path: The path to the payload image. + + Raises: + ApexInfoError on parsing errors. + + Returns: + A dict that contains payload property-value pairs. The dict should at least + contain Algorithm, Salt and apex.key. + """ + if not os.path.exists(payload_path): + raise ApexInfoError('Failed to find image: {}'.format(payload_path)) + + cmd = ['avbtool', 'info_image', '--image', payload_path] + try: + output = common.RunAndCheckOutput(cmd) + except common.ExternalError as e: + raise ApexInfoError, \ + 'Failed to get APEX payload info for {}:\n{}'.format( + payload_path, e), sys.exc_info()[2] + + # Extract the Algorithm / Salt / Prop info from payload (i.e. an image signed + # with avbtool). For example, + # Algorithm: SHA256_RSA4096 + PAYLOAD_INFO_PATTERN = ( + r'^\s*(?PAlgorithm|Salt|Prop)\:\s*(?P.*?)$') + payload_info_matcher = re.compile(PAYLOAD_INFO_PATTERN) + + payload_info = {} + for line in output.split('\n'): + line_info = payload_info_matcher.match(line) + if not line_info: + continue + + key, value = line_info.group('key'), line_info.group('value') + + if key == 'Prop': + # Further extract the property key-value pair, from a 'Prop:' line. For + # example, + # Prop: apex.key -> 'com.android.runtime' + # Note that avbtool writes single or double quotes around values. + PROPERTY_DESCRIPTOR_PATTERN = r'^\s*(?P.*?)\s->\s*(?P.*?)$' + + prop_matcher = re.compile(PROPERTY_DESCRIPTOR_PATTERN) + prop = prop_matcher.match(value) + if not prop: + raise ApexInfoError( + 'Failed to parse prop string {}'.format(value)) + + prop_key, prop_value = prop.group('key'), prop.group('value') + if prop_key == 'apex.key': + # avbtool dumps the prop value with repr(), which contains single / + # double quotes that we don't want. + payload_info[prop_key] = prop_value.strip('\"\'') + + else: + payload_info[key] = value + + # Sanity check. + for key in ('Algorithm', 'Salt', 'apex.key'): + if key not in payload_info: + raise ApexInfoError( + 'Failed to find {} prop in {}'.format(key, payload_path)) + + return payload_info diff --git a/tools/releasetools/test_apex_utils.py b/tools/releasetools/test_apex_utils.py new file mode 100644 index 0000000000..2f8ee49823 --- /dev/null +++ b/tools/releasetools/test_apex_utils.py @@ -0,0 +1,87 @@ +# +# Copyright (C) 2019 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import os.path + +import apex_utils +import common +import test_utils + + +class ApexUtilsTest(test_utils.ReleaseToolsTestCase): + + # echo "foo" | sha256sum + SALT = 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c' + + def setUp(self): + self.testdata_dir = test_utils.get_testdata_dir() + # The default payload signing key. + self.payload_key = os.path.join(self.testdata_dir, 'testkey.key') + + @staticmethod + def _GetTestPayload(): + payload_file = common.MakeTempFile(prefix='apex-', suffix='.img') + with open(payload_file, 'wb') as payload_fp: + payload_fp.write(os.urandom(8192)) + return payload_file + + def test_ParseApexPayloadInfo(self): + payload_file = self._GetTestPayload() + apex_utils.SignApexPayload( + payload_file, self.payload_key, 'testkey', 'SHA256_RSA2048', self.SALT) + payload_info = apex_utils.ParseApexPayloadInfo(payload_file) + self.assertEqual('SHA256_RSA2048', payload_info['Algorithm']) + self.assertEqual(self.SALT, payload_info['Salt']) + self.assertEqual('testkey', payload_info['apex.key']) + + def test_SignApexPayload(self): + payload_file = self._GetTestPayload() + apex_utils.SignApexPayload( + payload_file, self.payload_key, 'testkey', 'SHA256_RSA2048', self.SALT) + apex_utils.VerifyApexPayload(payload_file, self.payload_key) + + def test_SignApexPayload_withSignerHelper(self): + payload_file = self._GetTestPayload() + payload_signer_args = '--signing_helper_with_files {}'.format( + os.path.join(self.testdata_dir, 'signing_helper.sh')) + apex_utils.SignApexPayload( + payload_file, + self.payload_key, + 'testkey', 'SHA256_RSA2048', self.SALT, + payload_signer_args) + apex_utils.VerifyApexPayload(payload_file, self.payload_key) + + def test_SignApexPayload_invalidKey(self): + self.assertRaises( + apex_utils.ApexSigningError, + apex_utils.SignApexPayload, + self._GetTestPayload(), + os.path.join(self.testdata_dir, 'testkey.x509.pem'), + 'testkey', + 'SHA256_RSA2048', + self.SALT) + + def test_VerifyApexPayload_wrongKey(self): + payload_file = self._GetTestPayload() + apex_utils.SignApexPayload( + payload_file, self.payload_key, 'testkey', 'SHA256_RSA2048', self.SALT) + apex_utils.VerifyApexPayload(payload_file, self.payload_key) + self.assertRaises( + apex_utils.ApexSigningError, + apex_utils.VerifyApexPayload, + payload_file, + os.path.join(self.testdata_dir, 'testkey_with_passwd.key')) diff --git a/tools/releasetools/testdata/signing_helper.sh b/tools/releasetools/testdata/signing_helper.sh new file mode 100755 index 0000000000..364e0238b6 --- /dev/null +++ b/tools/releasetools/testdata/signing_helper.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# +# Copyright (C) 2019 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +tmpfile=$(mktemp) +cat $3 | openssl rsautl -sign -inkey $2 -raw > $tmpfile +cat $tmpfile > $3 +rm $tmpfile diff --git a/tools/releasetools/testdata/testkey_with_passwd.key b/tools/releasetools/testdata/testkey_with_passwd.key new file mode 100644 index 0000000000..2f0a199645 --- /dev/null +++ b/tools/releasetools/testdata/testkey_with_passwd.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCwaAOHPqgkCmqU +AFRnJW6LrAuSfy9EzWSRHSkltp811ByMIE0N6/Nttu8ZCL456lzArHNKt/zdoBik +eLB6gN9CTvQ8n4LMdSEmkRl3uXBtOPJuVObJ6ZUILz6L7WofWcr8DT81j2At7nHi +Wg8SkCsFXbFfpjljOlpqUG3Szt+48X8rcgG82s97BuRwNxUgfK1/8QzOiH9fDbMU +h6XI2jo2VwuBYOsJadJJWOf6oRRHZonrts0FXpV46CXykpLvLT2u5GXg1Pxd7i1K +v1P8bxZOzVbEVfkL2DnUCtUBAnP98r9UyjQDd4blk4Mwl+mzB5otPTacNzEGhmNK +Et+HB/cdAgMBAAECggEATsn2IXa7tHUuivHmwLb4O8vY01KY8xrleubSVPTPAUS+ +h1t57ujerbcR7VV5WPay/J9JUyr/9qClwPfioqRikwQek+EOk3ERIF+YR1/8tdvE +c8DZ337DQIeRYP/l8SCyx4bHH43tADbKiLV+m+TmQhxJt5XPdeE/NtK7andZdwkv +xEoG9l2aONE4z9pY1x+c1SdDSsq92/iLHLgSkQJmWo+lrfeh6gshXgQgDY8n6rgY +GsCgSawLphvd8Tvo86CL04l0pWtY1gEW3s6sdYo1YDkpWQzSRCtGm0GlhEt2fyq5 +coTK2sLHguE7NL5VZo4zlGtM3QBdvRksTO1mJOt6JQKBgQDaT4oGjZp1rtKdObvn +ElaUo5EOyJjmXkRBBndrbiG3078eOqTJHXx45DJUv8hj9+g6vSULiIeFk1FiiMQD +vcnsBEaGaSc886wXY6TQgIIzvVfzDHGYTuQydiYQbLClH6S28HLqdlZjUIlHwxb9 +wBm8JwmTiVeAEvO8LTzeEqfkLwKBgQDO3He8Ei8XDeqtIK0lzcZ83yw9OGP23/gK +8GDaf8J+cOtOyYkDlcV0rBNFvE8+TzIpIUlo47b2RSaART3iPSfRJTaySZjKWCVo +s2A0/zQcrj7GgD2gaHRrgI9bmnWW1j95a9n/6AUEyEIJ6K8tYK819Vl4GAyhNHEQ +sRbxa69qcwKBgQC5F8jxx2tXLdM6JLIQtzabLZcWTrN8Vh5Od3oWpriF0EzxB02h +ipN3OBsISdZQE+dcrfNTtP0aHo5ZGZX/ihFCP1nAKjVvczXMWtppQRujXHzOABXr +ya+mrQ+Wy2B1j7+qr3DvI0gZSjYqltjOaeon4X04DrEWUHtAZ6Z8rpqUVwKBgQCB +o8mmI/8/A4m/Vmss9fke6P5gn6aGYXah5GPOi6Loevv9NHCZvpMwu2aYnZtMAXX+ +MM5A3fUcAdpPKRXPY2RAvoG42kbXCMbpBwGUNRwDnW/aFySIEu5jMP6m+fYXwc2l +2uGUb2Q1ywsYCqs+VQl5V3nquaewn5z8SP+H7WTR4QKBgQCO5CRpyNOjEwMxTPR1 +GYUKAEiVtmzknHAxUE6drTgGEZSquAXiau0B5+7+/G5gwqxCLGpnstMByI+dhkR6 ++ybAc/bzb2aoGK4pZf/PuwxQQsHBnG0oaSFU6RZlbVV20j7FZ04+cYnKHwCYkKjN +DwA1Ae+H+u95raB4vYhk7IzD4A== +-----END PRIVATE KEY----- From aa7e993a221c1a215180b25ac604d802b639c19a Mon Sep 17 00:00:00 2001 From: Tao Bao Date: Fri, 15 Mar 2019 09:37:01 -0700 Subject: [PATCH 3/3] releasetools: Support signing APEXes. Bug: 123716522 Test: Run sign_target_files_apks.py to sign a target_files with APEXes. Test: Run check_target_files_signatures.py on signed artifact. Test: python -m unittest test_sign_target_files_apks Change-Id: I3fa13e3d9461cf5e0838e0572d436e218164fe41 --- tools/releasetools/sign_target_files_apks.py | 294 ++++++++++++++++-- .../test_sign_target_files_apks.py | 80 ++++- 2 files changed, 340 insertions(+), 34 deletions(-) diff --git a/tools/releasetools/sign_target_files_apks.py b/tools/releasetools/sign_target_files_apks.py index 85897c28e2..a9d22182b2 100755 --- a/tools/releasetools/sign_target_files_apks.py +++ b/tools/releasetools/sign_target_files_apks.py @@ -21,11 +21,17 @@ target-files zip. Usage: sign_target_files_apks [flags] input_target_files output_target_files -e (--extra_apks) - Add extra APK name/key pairs as though they appeared in - apkcerts.txt (so mappings specified by -k and -d are applied). - Keys specified in -e override any value for that app contained - in the apkcerts.txt file. Option may be repeated to give - multiple extra packages. + Add extra APK/APEX name/key pairs as though they appeared in apkcerts.txt + or apexkeys.txt (so mappings specified by -k and -d are applied). Keys + specified in -e override any value for that app contained in the + apkcerts.txt file, or the container key for an APEX. Option may be + repeated to give multiple extra packages. + + --extra_apex_payload_key + Add a mapping for APEX package name to payload signing key, which will + override the default payload signing key in apexkeys.txt. Note that the + container key should be overridden via the `--extra_apks` flag above. + Option may be repeated for multiple APEXes. --skip_apks_with_path_prefix Skip signing an APK if it has the matching prefix in its path. The prefix @@ -90,7 +96,7 @@ Usage: sign_target_files_apks [flags] input_target_files output_target_files Use the specified algorithm (e.g. SHA256_RSA4096) and the key to AVB-sign the specified image. Otherwise it uses the existing values in info dict. - --avb_{boot,system,vendor,dtbo,vbmeta}_extra_args + --avb_{apex,boot,system,vendor,dtbo,vbmeta}_extra_args 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. @@ -102,6 +108,7 @@ import base64 import copy import errno import gzip +import itertools import logging import os import re @@ -114,6 +121,7 @@ import zipfile from xml.etree import ElementTree import add_img_to_target_files +import apex_utils import common @@ -127,6 +135,7 @@ logger = logging.getLogger(__name__) OPTIONS = common.OPTIONS OPTIONS.extra_apks = {} +OPTIONS.extra_apex_payload_keys = {} OPTIONS.skip_apks_with_path_prefix = set() OPTIONS.key_map = {} OPTIONS.rebuild_recovery = False @@ -154,6 +163,41 @@ def GetApkCerts(certmap): return certmap +def GetApexKeys(keys_info, key_map): + """Gets APEX payload and container signing keys by applying the mapping rules. + + We currently don't allow PRESIGNED payload / container keys. + + Args: + keys_info: A dict that maps from APEX filenames to a tuple of (payload_key, + container_key). + key_map: A dict that overrides the keys, specified via command-line input. + + Returns: + A dict that contains the updated APEX key mapping, which should be used for + the current signing. + """ + # Apply all the --extra_apex_payload_key options to override the payload + # signing keys in the given keys_info. + for apex, key in OPTIONS.extra_apex_payload_keys.items(): + assert key, 'Presigned APEX payload for {} is not allowed'.format(apex) + keys_info[apex] = (key, keys_info[apex][1]) + + # Apply the key remapping to container keys. + for apex, (payload_key, container_key) in keys_info.items(): + keys_info[apex] = (payload_key, key_map.get(container_key, container_key)) + + # Apply all the --extra_apks options to override the container keys. + for apex, key in OPTIONS.extra_apks.items(): + # Skip non-APEX containers. + if apex not in keys_info: + continue + assert key, 'Presigned APEX container for {} is not allowed'.format(apex) + keys_info[apex][1] = key_map.get(key, key) + + return keys_info + + def GetApkFileInfo(filename, compressed_extension, skipped_prefixes): """Returns the APK info based on the given filename. @@ -200,34 +244,45 @@ def GetApkFileInfo(filename, compressed_extension, skipped_prefixes): return (True, is_compressed, should_be_skipped) -def CheckAllApksSigned(input_tf_zip, apk_key_map, compressed_extension): - """Checks that all the APKs have keys specified, otherwise errors out. +def CheckApkAndApexKeysAvailable(input_tf_zip, known_keys, + compressed_extension): + """Checks that all the APKs and APEXes have keys specified. Args: input_tf_zip: An open target_files zip file. - apk_key_map: A dict of known signing keys key'd by APK names. + known_keys: A set of APKs and APEXes that have known signing keys. compressed_extension: The extension string of compressed APKs, such as - ".gz", or None if there's no compressed APKs. + '.gz', or None if there's no compressed APKs. Raises: - AssertionError: On finding unknown APKs. + AssertionError: On finding unknown APKs and APEXes. """ - unknown_apks = [] + unknown_files = [] for info in input_tf_zip.infolist(): + # Handle APEXes first, e.g. SYSTEM/apex/com.android.tzdata.apex. + if (info.filename.startswith('SYSTEM/apex') and + info.filename.endswith('.apex')): + name = os.path.basename(info.filename) + if name not in known_keys: + unknown_files.append(name) + continue + + # And APKs. (is_apk, is_compressed, should_be_skipped) = GetApkFileInfo( info.filename, compressed_extension, OPTIONS.skip_apks_with_path_prefix) if not is_apk or should_be_skipped: continue + name = os.path.basename(info.filename) if is_compressed: name = name[:-len(compressed_extension)] - if name not in apk_key_map: - unknown_apks.append(name) + if name not in known_keys: + unknown_files.append(name) - assert not unknown_apks, \ + assert not unknown_files, \ ("No key specified for:\n {}\n" "Use '-e =' to specify a key (which may be an empty string to " - "not sign this apk).".format("\n ".join(unknown_apks))) + "not sign this apk).".format("\n ".join(unknown_files))) def SignApk(data, keyname, pw, platform_api_level, codename_to_api_level_map, @@ -293,9 +348,69 @@ def SignApk(data, keyname, pw, platform_api_level, codename_to_api_level_map, return data +def SignApex(apex_data, payload_key, container_key, container_pw, + codename_to_api_level_map, signing_args=None): + """Signs the current APEX with the given payload/container keys. + + Args: + apex_data: Raw APEX data. + payload_key: The path to payload signing key (w/o extension). + container_key: The path to container signing key (w/o extension). + container_pw: The matching password of the container_key, or None. + codename_to_api_level_map: A dict that maps from codename to API level. + signing_args: Additional args to be passed to the payload signer. + + Returns: + (signed_apex, payload_key_name): signed_apex is the path to the signed APEX + file; payload_key_name is a str of the payload signing key name (e.g. + com.android.tzdata). + """ + apex_file = common.MakeTempFile(prefix='apex-', suffix='.apex') + with open(apex_file, 'wb') as apex_fp: + apex_fp.write(apex_data) + + APEX_PAYLOAD_IMAGE = 'apex_payload.img' + + # Signing an APEX is a two step process. + # 1. 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: + payload_file = apex_fd.extract(APEX_PAYLOAD_IMAGE, payload_dir) + + payload_info = apex_utils.ParseApexPayloadInfo(payload_file) + apex_utils.SignApexPayload( + payload_file, + payload_key, + payload_info['apex.key'], + payload_info['Algorithm'], + payload_info['Salt'], + signing_args) + + common.ZipDelete(apex_file, APEX_PAYLOAD_IMAGE) + apex_zip = zipfile.ZipFile(apex_file, 'a') + common.ZipWrite(apex_zip, payload_file, arcname=APEX_PAYLOAD_IMAGE) + common.ZipClose(apex_zip) + + # 2. Sign the overall APEX container with container_key. + signed_apex = common.MakeTempFile(prefix='apex-container-', suffix='.apex') + common.SignFile( + apex_file, + signed_apex, + container_key, + container_pw, + codename_to_api_level_map=codename_to_api_level_map) + + signed_and_aligned_apex = common.MakeTempFile( + prefix='apex-container-', suffix='.apex') + common.RunAndCheckOutput( + ['zipalign', '-f', '4096', signed_apex, signed_and_aligned_apex]) + + return (signed_and_aligned_apex, payload_info['apex.key']) + + def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, - apk_key_map, key_passwords, platform_api_level, - codename_to_api_level_map, + apk_keys, apex_keys, key_passwords, + platform_api_level, codename_to_api_level_map, compressed_extension): # maxsize measures the maximum filename length, including the ones to be # skipped. @@ -304,6 +419,10 @@ def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, if GetApkFileInfo(i.filename, compressed_extension, [])[0]]) system_root_image = misc_info.get("system_root_image") == "true" + # A dict of APEX payload public keys that should be updated, i.e. the files + # under '/system/etc/security/apex/'. + updated_apex_payload_keys = {} + for info in input_tf_zip.infolist(): filename = info.filename if filename.startswith("IMAGES/"): @@ -331,7 +450,7 @@ def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, if is_compressed: name = name[:-len(compressed_extension)] - key = apk_key_map[name] + key = apk_keys[name] if key not in common.SPECIAL_CERT_STRINGS: print(" signing: %-*s (%s)" % (maxsize, name, key)) signed_data = SignApk(data, key, key_passwords[key], platform_api_level, @@ -344,6 +463,30 @@ def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, " (skipped due to special cert string)" % (name,)) common.ZipWriteStr(output_tf_zip, out_info, data) + # Sign bundled APEX files. + elif filename.startswith("SYSTEM/apex") and filename.endswith(".apex"): + name = os.path.basename(filename) + payload_key, container_key = apex_keys[name] + + print(" signing: %-*s container (%s)" % (maxsize, name, container_key)) + print(" : %-*s payload (%s)" % (maxsize, name, payload_key)) + + (signed_apex, payload_key_name) = SignApex( + data, + payload_key, + container_key, + key_passwords[container_key], + codename_to_api_level_map, + OPTIONS.avb_extra_args.get('apex')) + common.ZipWrite(output_tf_zip, signed_apex, filename) + + updated_apex_payload_keys[payload_key_name] = payload_key + + # AVB public keys for the installed APEXes, which will be updated later. + elif (os.path.dirname(filename) == 'SYSTEM/etc/security/apex' and + filename != 'SYSTEM/etc/security/apex/'): + continue + # System properties. elif filename in ("SYSTEM/build.prop", "VENDOR/build.prop", @@ -406,6 +549,30 @@ def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, else: common.ZipWriteStr(output_tf_zip, out_info, data) + # Update APEX payload public keys. + for info in input_tf_zip.infolist(): + filename = info.filename + if (os.path.dirname(filename) != 'SYSTEM/etc/security/apex' or + filename == 'SYSTEM/etc/security/apex/'): + continue + + name = os.path.basename(filename) + assert name in updated_apex_payload_keys, \ + 'Unsigned APEX payload key: {}'.format(filename) + + key_path = updated_apex_payload_keys[name] + if not os.path.exists(key_path) and not key_path.endswith('.pem'): + key_path = '{}.pem'.format(key_path) + assert os.path.exists(key_path), \ + 'Failed to find public key file {} for APEX {}'.format( + updated_apex_payload_keys[name], name) + + print('Replacing APEX payload public key for {} with {}'.format( + name, key_path)) + + public_key = common.ExtractAvbPublicKey(key_path) + common.ZipWrite(output_tf_zip, public_key, arcname=filename) + if OPTIONS.replace_ota_keys: ReplaceOtaKeys(input_tf_zip, output_tf_zip, misc_info) @@ -821,6 +988,67 @@ def GetCodenameToApiLevelMap(input_tf_zip): return result +def ReadApexKeysInfo(tf_zip): + """Parses the APEX keys info from a given target-files zip. + + Given a target-files ZipFile, parses the META/apexkeys.txt entry and returns a + dict that contains the mapping from APEX names (e.g. com.android.tzdata) to a + tuple of (payload_key, container_key). + + Args: + tf_zip: The input target_files ZipFile (already open). + + Returns: + (payload_key, container_key): payload_key contains the path to the payload + signing key; container_key contains the path to the container signing + key. + """ + keys = {} + for line in tf_zip.read("META/apexkeys.txt").split("\n"): + line = line.strip() + if not line: + continue + matches = re.match( + r'^name="(?P.*)"\s+' + r'public_key="(?P.*)"\s+' + r'private_key="(?P.*)"\s+' + r'container_certificate="(?P.*)"\s+' + r'container_private_key="(?P.*)"$', + line) + if not matches: + continue + + name = matches.group('NAME') + payload_public_key = matches.group("PAYLOAD_PUBLIC_KEY") + payload_private_key = matches.group("PAYLOAD_PRIVATE_KEY") + + def CompareKeys(pubkey, pubkey_suffix, privkey, privkey_suffix): + pubkey_suffix_len = len(pubkey_suffix) + privkey_suffix_len = len(privkey_suffix) + return (pubkey.endswith(pubkey_suffix) and + privkey.endswith(privkey_suffix) and + pubkey[:-pubkey_suffix_len] == privkey[:-privkey_suffix_len]) + + PAYLOAD_PUBLIC_KEY_SUFFIX = '.avbpubkey' + PAYLOAD_PRIVATE_KEY_SUFFIX = '.pem' + if not CompareKeys( + payload_public_key, PAYLOAD_PUBLIC_KEY_SUFFIX, + payload_private_key, PAYLOAD_PRIVATE_KEY_SUFFIX): + raise ValueError("Failed to parse payload keys: \n{}".format(line)) + + container_cert = matches.group("CONTAINER_CERT") + container_private_key = matches.group("CONTAINER_PRIVATE_KEY") + if not CompareKeys( + container_cert, OPTIONS.public_key_suffix, + container_private_key, OPTIONS.private_key_suffix): + raise ValueError("Failed to parse container keys: \n{}".format(line)) + + keys[name] = (payload_private_key, + container_cert[:-len(OPTIONS.public_key_suffix)]) + + return keys + + def main(argv): key_mapping_options = [] @@ -831,6 +1059,9 @@ def main(argv): names = names.split(",") for n in names: OPTIONS.extra_apks[n] = key + elif o == "--extra_apex_payload_key": + apex_name, key = a.split("=") + OPTIONS.extra_apex_payload_keys[apex_name] = key elif o == "--skip_apks_with_path_prefix": # Sanity check the prefix, which must be in all upper case. prefix = a.split('/')[0] @@ -887,6 +1118,8 @@ def main(argv): OPTIONS.avb_algorithms['vendor'] = a elif o == "--avb_vendor_extra_args": OPTIONS.avb_extra_args['vendor'] = a + elif o == "--avb_apex_extra_args": + OPTIONS.avb_extra_args['apex'] = a else: return False return True @@ -896,6 +1129,7 @@ def main(argv): extra_opts="e:d:k:ot:", extra_long_opts=[ "extra_apks=", + "extra_apex_payload_key=", "skip_apks_with_path_prefix=", "default_key_mappings=", "key_mapping=", @@ -904,6 +1138,7 @@ def main(argv): "replace_verity_public_key=", "replace_verity_private_key=", "replace_verity_keyid=", + "avb_apex_extra_args=", "avb_vbmeta_algorithm=", "avb_vbmeta_key=", "avb_vbmeta_extra_args=", @@ -937,18 +1172,25 @@ def main(argv): BuildKeyMap(misc_info, key_mapping_options) - certmap, compressed_extension = common.ReadApkCerts(input_zip) - apk_key_map = GetApkCerts(certmap) - CheckAllApksSigned(input_zip, apk_key_map, compressed_extension) + apk_keys_info, compressed_extension = common.ReadApkCerts(input_zip) + apk_keys = GetApkCerts(apk_keys_info) - key_passwords = common.GetKeyPasswords(set(apk_key_map.values())) + apex_keys_info = ReadApexKeysInfo(input_zip) + apex_keys = GetApexKeys(apex_keys_info, apk_keys) + + CheckApkAndApexKeysAvailable( + input_zip, + set(apk_keys.keys()) | set(apex_keys.keys()), + compressed_extension) + + key_passwords = common.GetKeyPasswords( + set(apk_keys.values()) | set(itertools.chain(*apex_keys.values()))) platform_api_level, _ = GetApiLevelAndCodename(input_zip) codename_to_api_level_map = GetCodenameToApiLevelMap(input_zip) ProcessTargetFiles(input_zip, output_zip, misc_info, - apk_key_map, key_passwords, - platform_api_level, - codename_to_api_level_map, + apk_keys, apex_keys, key_passwords, + platform_api_level, codename_to_api_level_map, compressed_extension) common.ZipClose(input_zip) diff --git a/tools/releasetools/test_sign_target_files_apks.py b/tools/releasetools/test_sign_target_files_apks.py index 18762eebad..9d214296c3 100644 --- a/tools/releasetools/test_sign_target_files_apks.py +++ b/tools/releasetools/test_sign_target_files_apks.py @@ -21,8 +21,8 @@ import zipfile import common import test_utils from sign_target_files_apks import ( - CheckAllApksSigned, EditTags, GetApkFileInfo, ReplaceCerts, - ReplaceVerityKeyId, RewriteProps) + CheckApkAndApexKeysAvailable, EditTags, GetApkFileInfo, ReadApexKeysInfo, + ReplaceCerts, ReplaceVerityKeyId, RewriteProps) class SignTargetFilesApksTest(test_utils.ReleaseToolsTestCase): @@ -33,6 +33,10 @@ class SignTargetFilesApksTest(test_utils.ReleaseToolsTestCase): """ + APEX_KEYS_TXT = """name="apex.apexd_test.apex" public_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package.avbpubkey" private_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package.pem" container_certificate="build/target/product/security/testkey.x509.pem" container_private_key="build/target/product/security/testkey.pk8" +name="apex.apexd_test_different_app.apex" public_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.avbpubkey" private_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.pem" container_certificate="build/target/product/security/testkey.x509.pem" container_private_key="build/target/product/security/testkey.pk8" +""" + def setUp(self): self.testdata_dir = test_utils.get_testdata_dir() @@ -207,7 +211,7 @@ class SignTargetFilesApksTest(test_utils.ReleaseToolsTestCase): } self.assertEqual(output_xml, ReplaceCerts(input_xml)) - def test_CheckAllApksSigned(self): + def test_CheckApkAndApexKeysAvailable(self): input_file = common.MakeTempFile(suffix='.zip') with zipfile.ZipFile(input_file, 'w') as input_zip: input_zip.writestr('SYSTEM/app/App1.apk', "App1-content") @@ -219,16 +223,17 @@ class SignTargetFilesApksTest(test_utils.ReleaseToolsTestCase): 'App3.apk' : 'key3', } with zipfile.ZipFile(input_file) as input_zip: - CheckAllApksSigned(input_zip, apk_key_map, None) - CheckAllApksSigned(input_zip, apk_key_map, '.gz') + CheckApkAndApexKeysAvailable(input_zip, apk_key_map, None) + CheckApkAndApexKeysAvailable(input_zip, apk_key_map, '.gz') # 'App2.apk.gz' won't be considered as an APK. - CheckAllApksSigned(input_zip, apk_key_map, None) - CheckAllApksSigned(input_zip, apk_key_map, '.xz') + CheckApkAndApexKeysAvailable(input_zip, apk_key_map, None) + CheckApkAndApexKeysAvailable(input_zip, apk_key_map, '.xz') del apk_key_map['App2.apk'] self.assertRaises( - AssertionError, CheckAllApksSigned, input_zip, apk_key_map, '.gz') + AssertionError, CheckApkAndApexKeysAvailable, input_zip, apk_key_map, + '.gz') def test_GetApkFileInfo(self): (is_apk, is_compressed, should_be_skipped) = GetApkFileInfo( @@ -344,3 +349,62 @@ class SignTargetFilesApksTest(test_utils.ReleaseToolsTestCase): self.assertRaises( AssertionError, GetApkFileInfo, "SYSTEM_OTHER/preloads/apps/Chats.apk", None, None) + + def test_ReadApexKeysInfo(self): + target_files = common.MakeTempFile(suffix='.zip') + with zipfile.ZipFile(target_files, 'w') as target_files_zip: + target_files_zip.writestr('META/apexkeys.txt', self.APEX_KEYS_TXT) + + with zipfile.ZipFile(target_files) as target_files_zip: + keys_info = ReadApexKeysInfo(target_files_zip) + + self.assertEqual( + { + 'apex.apexd_test.apex': ( + 'system/apex/apexd/apexd_testdata/com.android.apex.test_package.pem', + 'build/target/product/security/testkey'), + 'apex.apexd_test_different_app.apex': ( + 'system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.pem', + 'build/target/product/security/testkey'), + }, + keys_info) + + def test_ReadApexKeysInfo_mismatchingKeys(self): + # Mismatching payload public / private keys. + apex_keys = self.APEX_KEYS_TXT + ( + 'name="apex.apexd_test_different_app2.apex" ' + 'public_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.avbpubkey" ' + 'private_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package_3.pem" ' + 'container_certificate="build/target/product/security/testkey.x509.pem" ' + 'container_private_key="build/target/product/security/testkey.pk8"') + target_files = common.MakeTempFile(suffix='.zip') + with zipfile.ZipFile(target_files, 'w') as target_files_zip: + target_files_zip.writestr('META/apexkeys.txt', apex_keys) + + with zipfile.ZipFile(target_files) as target_files_zip: + self.assertRaises(ValueError, ReadApexKeysInfo, target_files_zip) + + def test_ReadApexKeysInfo_missingPrivateKey(self): + # Invalid lines will be skipped. + apex_keys = self.APEX_KEYS_TXT + ( + 'name="apex.apexd_test_different_app2.apex" ' + 'public_key="system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.avbpubkey" ' + 'container_certificate="build/target/product/security/testkey.x509.pem" ' + 'container_private_key="build/target/product/security/testkey.pk8"') + target_files = common.MakeTempFile(suffix='.zip') + with zipfile.ZipFile(target_files, 'w') as target_files_zip: + target_files_zip.writestr('META/apexkeys.txt', apex_keys) + + with zipfile.ZipFile(target_files) as target_files_zip: + keys_info = ReadApexKeysInfo(target_files_zip) + + self.assertEqual( + { + 'apex.apexd_test.apex': ( + 'system/apex/apexd/apexd_testdata/com.android.apex.test_package.pem', + 'build/target/product/security/testkey'), + 'apex.apexd_test_different_app.apex': ( + 'system/apex/apexd/apexd_testdata/com.android.apex.test_package_2.pem', + 'build/target/product/security/testkey'), + }, + keys_info)