d97eba9f39
This tag is obsolete; we no longer need to distinguish the OTA keys from the app keys. This change does not affect device code.
374 lines
12 KiB
Python
Executable file
374 lines
12 KiB
Python
Executable file
#!/usr/bin/env python
|
|
#
|
|
# Copyright (C) 2008 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.
|
|
|
|
"""
|
|
Signs all the APK files in a target-files zipfile, producing a new
|
|
target-files zip.
|
|
|
|
Usage: sign_target_files_apks [flags] input_target_files output_target_files
|
|
|
|
-e (--extra_apks) <name,name,...=key>
|
|
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.
|
|
|
|
-k (--key_mapping) <src_key=dest_key>
|
|
Add a mapping from the key name as specified in apkcerts.txt (the
|
|
src_key) to the real key you wish to sign the package with
|
|
(dest_key). Option may be repeated to give multiple key
|
|
mappings.
|
|
|
|
-d (--default_key_mappings) <dir>
|
|
Set up the following key mappings:
|
|
|
|
build/target/product/security/testkey ==> $dir/releasekey
|
|
build/target/product/security/media ==> $dir/media
|
|
build/target/product/security/shared ==> $dir/shared
|
|
build/target/product/security/platform ==> $dir/platform
|
|
|
|
-d and -k options are added to the set of mappings in the order
|
|
in which they appear on the command line.
|
|
|
|
-o (--replace_ota_keys)
|
|
Replace the certificate (public key) used by OTA package
|
|
verification with the one specified in the input target_files
|
|
zip (in the META/otakeys.txt file). Key remapping (-k and -d)
|
|
is performed on this key.
|
|
|
|
-t (--tag_changes) <+tag>,<-tag>,...
|
|
Comma-separated list of changes to make to the set of tags (in
|
|
the last component of the build fingerprint). Prefix each with
|
|
'+' or '-' to indicate whether that tag should be added or
|
|
removed. Changes are processed in the order they appear.
|
|
Default value is "-test-keys,+release-keys".
|
|
|
|
"""
|
|
|
|
import sys
|
|
|
|
if sys.hexversion < 0x02040000:
|
|
print >> sys.stderr, "Python 2.4 or newer is required."
|
|
sys.exit(1)
|
|
|
|
import cStringIO
|
|
import copy
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import zipfile
|
|
|
|
import common
|
|
|
|
OPTIONS = common.OPTIONS
|
|
|
|
OPTIONS.extra_apks = {}
|
|
OPTIONS.key_map = {}
|
|
OPTIONS.replace_ota_keys = False
|
|
OPTIONS.tag_changes = ("-test-keys", "+release-keys")
|
|
|
|
def GetApkCerts(tf_zip):
|
|
certmap = {}
|
|
for line in tf_zip.read("META/apkcerts.txt").split("\n"):
|
|
line = line.strip()
|
|
if not line: continue
|
|
m = re.match(r'^name="(.*)"\s+certificate="(.*)\.x509\.pem"\s+'
|
|
r'private_key="\2\.pk8"$', line)
|
|
if not m:
|
|
raise SigningError("failed to parse line from apkcerts.txt:\n" + line)
|
|
certmap[m.group(1)] = OPTIONS.key_map.get(m.group(2), m.group(2))
|
|
for apk, cert in OPTIONS.extra_apks.iteritems():
|
|
certmap[apk] = OPTIONS.key_map.get(cert, cert)
|
|
return certmap
|
|
|
|
|
|
def CheckAllApksSigned(input_tf_zip, apk_key_map):
|
|
"""Check that all the APKs we want to sign have keys specified, and
|
|
error out if they don't."""
|
|
unknown_apks = []
|
|
for info in input_tf_zip.infolist():
|
|
if info.filename.endswith(".apk"):
|
|
name = os.path.basename(info.filename)
|
|
if name not in apk_key_map:
|
|
unknown_apks.append(name)
|
|
if unknown_apks:
|
|
print "ERROR: no key specified for:\n\n ",
|
|
print "\n ".join(unknown_apks)
|
|
print "\nUse '-e <apkname>=' to specify a key (which may be an"
|
|
print "empty string to not sign this apk)."
|
|
sys.exit(1)
|
|
|
|
|
|
def SharedUserForApk(data):
|
|
tmp = tempfile.NamedTemporaryFile()
|
|
tmp.write(data)
|
|
tmp.flush()
|
|
|
|
p = common.Run(["aapt", "dump", "xmltree", tmp.name, "AndroidManifest.xml"],
|
|
stdout=subprocess.PIPE)
|
|
data, _ = p.communicate()
|
|
if p.returncode != 0:
|
|
raise ExternalError("failed to run aapt dump")
|
|
lines = data.split("\n")
|
|
for i in lines:
|
|
m = re.match(r'^\s*A: android:sharedUserId\([0-9a-fx]*\)="([^"]*)" .*$', i)
|
|
if m:
|
|
return m.group(1)
|
|
return None
|
|
|
|
|
|
def CheckSharedUserIdsConsistent(input_tf_zip, apk_key_map):
|
|
"""Check that all packages that request the same shared user id are
|
|
going to be signed with the same key."""
|
|
|
|
shared_user_apks = {}
|
|
maxlen = len("(unknown key)")
|
|
|
|
for info in input_tf_zip.infolist():
|
|
if info.filename.endswith(".apk"):
|
|
data = input_tf_zip.read(info.filename)
|
|
|
|
name = os.path.basename(info.filename)
|
|
shared_user = SharedUserForApk(data)
|
|
key = apk_key_map[name]
|
|
maxlen = max(maxlen, len(key))
|
|
|
|
if shared_user is not None:
|
|
shared_user_apks.setdefault(
|
|
shared_user, {}).setdefault(key, []).append(name)
|
|
|
|
errors = []
|
|
for k, v in shared_user_apks.iteritems():
|
|
# each shared user should have exactly one key used for all the
|
|
# apks that want that user.
|
|
if len(v) > 1:
|
|
errors.append((k, v))
|
|
|
|
if not errors: return
|
|
|
|
print "ERROR: shared user inconsistency. All apks wanting to use"
|
|
print " a given shared user must be signed with the same key."
|
|
print
|
|
errors.sort()
|
|
for user, keys in errors:
|
|
print 'shared user id "%s":' % (user,)
|
|
for key, apps in keys.iteritems():
|
|
print ' %-*s %s' % (maxlen, key or "(unknown key)", apps[0])
|
|
for a in apps[1:]:
|
|
print (' ' * (maxlen+5)) + a
|
|
print
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
def SignApk(data, keyname, pw):
|
|
unsigned = tempfile.NamedTemporaryFile()
|
|
unsigned.write(data)
|
|
unsigned.flush()
|
|
|
|
signed = tempfile.NamedTemporaryFile()
|
|
|
|
common.SignFile(unsigned.name, signed.name, keyname, pw, align=4)
|
|
|
|
data = signed.read()
|
|
unsigned.close()
|
|
signed.close()
|
|
|
|
return data
|
|
|
|
|
|
def SignApks(input_tf_zip, output_tf_zip, apk_key_map, key_passwords):
|
|
maxsize = max([len(os.path.basename(i.filename))
|
|
for i in input_tf_zip.infolist()
|
|
if i.filename.endswith('.apk')])
|
|
|
|
for info in input_tf_zip.infolist():
|
|
data = input_tf_zip.read(info.filename)
|
|
out_info = copy.copy(info)
|
|
if info.filename.endswith(".apk"):
|
|
name = os.path.basename(info.filename)
|
|
key = apk_key_map[name]
|
|
if key:
|
|
print " signing: %-*s (%s)" % (maxsize, name, key)
|
|
signed_data = SignApk(data, key, key_passwords[key])
|
|
output_tf_zip.writestr(out_info, signed_data)
|
|
else:
|
|
# an APK we're not supposed to sign.
|
|
print "NOT signing: %s" % (name,)
|
|
output_tf_zip.writestr(out_info, data)
|
|
elif info.filename in ("SYSTEM/build.prop",
|
|
"RECOVERY/RAMDISK/default.prop"):
|
|
print "rewriting %s:" % (info.filename,)
|
|
new_data = RewriteProps(data)
|
|
output_tf_zip.writestr(out_info, new_data)
|
|
else:
|
|
# a non-APK file; copy it verbatim
|
|
output_tf_zip.writestr(out_info, data)
|
|
|
|
|
|
def RewriteProps(data):
|
|
output = []
|
|
for line in data.split("\n"):
|
|
line = line.strip()
|
|
original_line = line
|
|
if line and line[0] != '#':
|
|
key, value = line.split("=", 1)
|
|
if key == "ro.build.fingerprint":
|
|
pieces = line.split("/")
|
|
tags = set(pieces[-1].split(","))
|
|
for ch in OPTIONS.tag_changes:
|
|
if ch[0] == "-":
|
|
tags.discard(ch[1:])
|
|
elif ch[0] == "+":
|
|
tags.add(ch[1:])
|
|
line = "/".join(pieces[:-1] + [",".join(sorted(tags))])
|
|
elif key == "ro.build.description":
|
|
pieces = line.split(" ")
|
|
assert len(pieces) == 5
|
|
tags = set(pieces[-1].split(","))
|
|
for ch in OPTIONS.tag_changes:
|
|
if ch[0] == "-":
|
|
tags.discard(ch[1:])
|
|
elif ch[0] == "+":
|
|
tags.add(ch[1:])
|
|
line = " ".join(pieces[:-1] + [",".join(sorted(tags))])
|
|
if line != original_line:
|
|
print " replace: ", original_line
|
|
print " with: ", line
|
|
output.append(line)
|
|
return "\n".join(output) + "\n"
|
|
|
|
|
|
def ReplaceOtaKeys(input_tf_zip, output_tf_zip):
|
|
try:
|
|
keylist = input_tf_zip.read("META/otakeys.txt").split()
|
|
except KeyError:
|
|
raise ExternalError("can't read META/otakeys.txt from input")
|
|
|
|
mapped_keys = []
|
|
for k in keylist:
|
|
m = re.match(r"^(.*)\.x509\.pem$", k)
|
|
if not m:
|
|
raise ExternalError("can't parse \"%s\" from META/otakeys.txt" % (k,))
|
|
k = m.group(1)
|
|
mapped_keys.append(OPTIONS.key_map.get(k, k) + ".x509.pem")
|
|
|
|
if mapped_keys:
|
|
print "using:\n ", "\n ".join(mapped_keys)
|
|
print "for OTA package verification"
|
|
else:
|
|
mapped_keys.append(
|
|
OPTIONS.key_map["build/target/product/security/testkey"] + ".x509.pem")
|
|
print "META/otakeys.txt has no keys; using", mapped_keys[0]
|
|
|
|
# recovery uses a version of the key that has been slightly
|
|
# predigested (by DumpPublicKey.java) and put in res/keys.
|
|
|
|
p = common.Run(["java", "-jar",
|
|
os.path.join(OPTIONS.search_path, "framework", "dumpkey.jar")]
|
|
+ mapped_keys,
|
|
stdout=subprocess.PIPE)
|
|
data, _ = p.communicate()
|
|
if p.returncode != 0:
|
|
raise ExternalError("failed to run dumpkeys")
|
|
common.ZipWriteStr(output_tf_zip, "RECOVERY/RAMDISK/res/keys", data)
|
|
|
|
# SystemUpdateActivity uses the x509.pem version of the keys, but
|
|
# put into a zipfile system/etc/security/otacerts.zip.
|
|
|
|
tempfile = cStringIO.StringIO()
|
|
certs_zip = zipfile.ZipFile(tempfile, "w")
|
|
for k in mapped_keys:
|
|
certs_zip.write(k)
|
|
certs_zip.close()
|
|
common.ZipWriteStr(output_tf_zip, "SYSTEM/etc/security/otacerts.zip",
|
|
tempfile.getvalue())
|
|
|
|
|
|
def main(argv):
|
|
|
|
def option_handler(o, a):
|
|
if o in ("-e", "--extra_apks"):
|
|
names, key = a.split("=")
|
|
names = names.split(",")
|
|
for n in names:
|
|
OPTIONS.extra_apks[n] = key
|
|
elif o in ("-d", "--default_key_mappings"):
|
|
OPTIONS.key_map.update({
|
|
"build/target/product/security/testkey": "%s/releasekey" % (a,),
|
|
"build/target/product/security/media": "%s/media" % (a,),
|
|
"build/target/product/security/shared": "%s/shared" % (a,),
|
|
"build/target/product/security/platform": "%s/platform" % (a,),
|
|
})
|
|
elif o in ("-k", "--key_mapping"):
|
|
s, d = a.split("=")
|
|
OPTIONS.key_map[s] = d
|
|
elif o in ("-o", "--replace_ota_keys"):
|
|
OPTIONS.replace_ota_keys = True
|
|
elif o in ("-t", "--tag_changes"):
|
|
new = []
|
|
for i in a.split(","):
|
|
i = i.strip()
|
|
if not i or i[0] not in "-+":
|
|
raise ValueError("Bad tag change '%s'" % (i,))
|
|
new.append(i[0] + i[1:].strip())
|
|
OPTIONS.tag_changes = tuple(new)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
args = common.ParseOptions(argv, __doc__,
|
|
extra_opts="e:d:k:ot:",
|
|
extra_long_opts=["extra_apks=",
|
|
"default_key_mappings=",
|
|
"key_mapping=",
|
|
"replace_ota_keys",
|
|
"tag_changes="],
|
|
extra_option_handler=option_handler)
|
|
|
|
if len(args) != 2:
|
|
common.Usage(__doc__)
|
|
sys.exit(1)
|
|
|
|
input_zip = zipfile.ZipFile(args[0], "r")
|
|
output_zip = zipfile.ZipFile(args[1], "w")
|
|
|
|
apk_key_map = GetApkCerts(input_zip)
|
|
CheckAllApksSigned(input_zip, apk_key_map)
|
|
CheckSharedUserIdsConsistent(input_zip, apk_key_map)
|
|
|
|
key_passwords = common.GetKeyPasswords(set(apk_key_map.values()))
|
|
SignApks(input_zip, output_zip, apk_key_map, key_passwords)
|
|
|
|
if OPTIONS.replace_ota_keys:
|
|
ReplaceOtaKeys(input_zip, output_zip)
|
|
|
|
input_zip.close()
|
|
output_zip.close()
|
|
|
|
print "done."
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
main(sys.argv[1:])
|
|
except common.ExternalError, e:
|
|
print
|
|
print " ERROR: %s" % (e,)
|
|
print
|
|
sys.exit(1)
|