diff --git a/tools/releasetools/check_target_files_signatures b/tools/releasetools/check_target_files_signatures new file mode 100755 index 000000000..b91f3d4c0 --- /dev/null +++ b/tools/releasetools/check_target_files_signatures @@ -0,0 +1,428 @@ +#!/usr/bin/env python +# +# Copyright (C) 2009 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. + +""" +Check the signatures of all APKs in a target_files .zip file. With +-c, compare the signatures of each package to the ones in a separate +target_files (usually a previously distributed build for the same +device) and flag any changes. + +Usage: check_target_file_signatures [flags] target_files + + -c (--compare_with) + Look for compatibility problems between the two sets of target + files (eg., packages whose keys have changed). + + -l (--local_cert_dirs) + Comma-separated list of top-level directories to scan for + .x509.pem files. Defaults to "vendor,build". Where cert files + can be found that match APK signatures, the filename will be + printed as the cert name, otherwise a hash of the cert plus its + subject string will be printed instead. + + -t (--text) + Dump the certificate information for both packages in comparison + mode (this output is normally suppressed). + +""" + +import sys + +if sys.hexversion < 0x02040000: + print >> sys.stderr, "Python 2.4 or newer is required." + sys.exit(1) + +import os +import re +import sha +import shutil +import subprocess +import tempfile +import zipfile + +import common + +# Work around a bug in python's zipfile module that prevents opening +# of zipfiles if any entry has an extra field of between 1 and 3 bytes +# (which is common with zipaligned APKs). This overrides the +# ZipInfo._decodeExtra() method (which contains the bug) with an empty +# version (since we don't need to decode the extra field anyway). +class MyZipInfo(zipfile.ZipInfo): + def _decodeExtra(self): + pass +zipfile.ZipInfo = MyZipInfo + +OPTIONS = common.OPTIONS + +OPTIONS.text = False +OPTIONS.compare_with = None +OPTIONS.local_cert_dirs = ("vendor", "build") + +PROBLEMS = [] +PROBLEM_PREFIX = [] + +def AddProblem(msg): + PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg) +def Push(msg): + PROBLEM_PREFIX.append(msg) +def Pop(): + PROBLEM_PREFIX.pop() + + +def Banner(msg): + print "-" * 70 + print " ", msg + print "-" * 70 + + +def GetCertSubject(cert): + p = common.Run(["openssl", "x509", "-inform", "DER", "-text"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + out, err = p.communicate(cert) + if err and not err.strip(): + return "(error reading cert subject)" + for line in out.split("\n"): + line = line.strip() + if line.startswith("Subject:"): + return line[8:].strip() + return "(unknown cert subject)" + + +class CertDB(object): + def __init__(self): + self.certs = {} + + def Add(self, cert, name=None): + if cert in self.certs: + if name: + self.certs[cert] = self.certs[cert] + "," + name + else: + if name is None: + name = "unknown cert %s (%s)" % (sha.sha(cert).hexdigest()[:12], + GetCertSubject(cert)) + self.certs[cert] = name + + def Get(self, cert): + """Return the name for a given cert.""" + return self.certs.get(cert, None) + + def FindLocalCerts(self): + to_load = [] + for top in OPTIONS.local_cert_dirs: + for dirpath, dirnames, filenames in os.walk(top): + certs = [os.path.join(dirpath, i) + for i in filenames if i.endswith(".x509.pem")] + if certs: + to_load.extend(certs) + + for i in to_load: + f = open(i) + cert = ParseCertificate(f.read()) + f.close() + name, _ = os.path.splitext(i) + name, _ = os.path.splitext(name) + self.Add(cert, name) + +ALL_CERTS = CertDB() + + +def ParseCertificate(data): + """Parse a PEM-format certificate.""" + cert = [] + save = False + for line in data.split("\n"): + if "--END CERTIFICATE--" in line: + break + if save: + cert.append(line) + if "--BEGIN CERTIFICATE--" in line: + save = True + cert = "".join(cert).decode('base64') + return cert + + +def CertFromPKCS7(data, filename): + """Read the cert out of a PKCS#7-format file (which is what is + stored in a signed .apk).""" + Push(filename + ":") + try: + p = common.Run(["openssl", "pkcs7", + "-inform", "DER", + "-outform", "PEM", + "-print_certs"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + out, err = p.communicate(data) + if err and not err.strip(): + AddProblem("error reading cert:\n" + err) + return None + + cert = ParseCertificate(out) + if not cert: + AddProblem("error parsing cert output") + return None + return cert + finally: + Pop() + + +class APK(object): + def __init__(self, full_filename, filename): + self.filename = filename + self.cert = None + Push(filename+":") + try: + self.RecordCert(full_filename) + self.ReadManifest(full_filename) + finally: + Pop() + + def RecordCert(self, full_filename): + try: + f = open(full_filename) + apk = zipfile.ZipFile(f, "r") + pkcs7 = None + for info in apk.infolist(): + if info.filename.startswith("META-INF/") and \ + (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")): + if pkcs7 is not None: + AddProblem("multiple certs") + pkcs7 = apk.read(info.filename) + self.cert = CertFromPKCS7(pkcs7, info.filename) + ALL_CERTS.Add(self.cert) + if not pkcs7: + AddProblem("no signature") + finally: + f.close() + + def ReadManifest(self, full_filename): + p = common.Run(["aapt", "dump", "xmltree", full_filename, + "AndroidManifest.xml"], + stdout=subprocess.PIPE) + manifest, err = p.communicate() + if err: + AddProblem("failed to read manifest") + return + + self.shared_uid = None + self.package = None + + for line in manifest.split("\n"): + line = line.strip() + m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line) + if m: + name = m.group(1) + if name == "android:sharedUserId": + if self.shared_uid is not None: + AddProblem("multiple sharedUserId declarations") + self.shared_uid = m.group(2) + elif name == "package": + if self.package is not None: + AddProblem("multiple package declarations") + self.package = m.group(2) + + if self.package is None: + AddProblem("no package declaration") + + +class TargetFiles(object): + def __init__(self): + self.max_pkg_len = 30 + self.max_fn_len = 20 + + def LoadZipFile(self, filename): + d = common.UnzipTemp(filename, '*.apk') + try: + self.apks = {} + for dirpath, dirnames, filenames in os.walk(d): + for fn in filenames: + if fn.endswith(".apk"): + fullname = os.path.join(dirpath, fn) + displayname = fullname[len(d)+1:] + apk = APK(fullname, displayname) + self.apks[apk.package] = apk + + self.max_pkg_len = max(self.max_pkg_len, len(apk.package)) + self.max_fn_len = max(self.max_fn_len, len(apk.filename)) + finally: + shutil.rmtree(d) + + def CheckSharedUids(self): + """Look for any instances where packages signed with different + certs request the same sharedUserId.""" + apks_by_uid = {} + for apk in self.apks.itervalues(): + if apk.shared_uid: + apks_by_uid.setdefault(apk.shared_uid, []).append(apk) + + for uid in sorted(apks_by_uid.keys()): + apks = apks_by_uid[uid] + for apk in apks[1:]: + if apk.cert != apks[0].cert: + break + else: + # all the certs are the same; this uid is fine + continue + + AddProblem("uid %s shared across multiple certs" % (uid,)) + + print "uid %s is shared by packages with different certs:" % (uid,) + x = [(i.cert, i.package, i) for i in apks] + x.sort() + lastcert = None + for cert, _, apk in x: + if cert != lastcert: + lastcert = cert + print " %s:" % (ALL_CERTS.Get(cert),) + print " %-*s [%s]" % (self.max_pkg_len, + apk.package, apk.filename) + print + + def PrintCerts(self): + """Display a table of packages grouped by cert.""" + by_cert = {} + for apk in self.apks.itervalues(): + by_cert.setdefault(apk.cert, []).append((apk.package, apk)) + + order = [(-len(v), k) for (k, v) in by_cert.iteritems()] + order.sort() + + for _, cert in order: + print "%s:" % (ALL_CERTS.Get(cert),) + apks = by_cert[cert] + apks.sort() + for _, apk in apks: + if apk.shared_uid: + print " %-*s %-*s [%s]" % (self.max_fn_len, apk.filename, + self.max_pkg_len, apk.package, + apk.shared_uid) + else: + print " %-*s %-*s" % (self.max_fn_len, apk.filename, + self.max_pkg_len, apk.package) + print + + def CompareWith(self, other): + """Look for instances where a given package that exists in both + self and other have different certs.""" + + all = set(self.apks.keys()) + all.update(other.apks.keys()) + + max_pkg_len = max(self.max_pkg_len, other.max_pkg_len) + + by_certpair = {} + + for i in all: + if i in self.apks: + if i in other.apks: + # in both; should have the same cert + if self.apks[i].cert != other.apks[i].cert: + by_certpair.setdefault((other.apks[i].cert, + self.apks[i].cert), []).append(i) + else: + print "%s [%s]: new APK (not in comparison target_files)" % ( + i, self.apks[i].filename) + else: + if i in other.apks: + print "%s [%s]: removed APK (only in comparison target_files)" % ( + i, other.apks[i].filename) + + if by_certpair: + AddProblem("some APKs changed certs") + Banner("APK signing differences") + for (old, new), packages in sorted(by_certpair.items()): + print "was", ALL_CERTS.Get(old) + print "now", ALL_CERTS.Get(new) + for i in sorted(packages): + old_fn = other.apks[i].filename + new_fn = self.apks[i].filename + if old_fn == new_fn: + print " %-*s [%s]" % (max_pkg_len, i, old_fn) + else: + print " %-*s [was: %s; now: %s]" % (max_pkg_len, i, + old_fn, new_fn) + print + + +def main(argv): + def option_handler(o, a): + if o in ("-c", "--compare_with"): + OPTIONS.compare_with = a + elif o in ("-l", "--local_cert_dirs"): + OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")] + elif o in ("-t", "--text"): + OPTIONS.text = True + else: + return False + return True + + args = common.ParseOptions(argv, __doc__, + extra_opts="c:l:t", + extra_long_opts=["compare_with=", + "local_cert_dirs="], + extra_option_handler=option_handler) + + if len(args) != 1: + common.Usage(__doc__) + sys.exit(1) + + ALL_CERTS.FindLocalCerts() + + Push("input target_files:") + try: + target_files = TargetFiles() + target_files.LoadZipFile(args[0]) + finally: + Pop() + + compare_files = None + if OPTIONS.compare_with: + Push("comparison target_files:") + try: + compare_files = TargetFiles() + compare_files.LoadZipFile(OPTIONS.compare_with) + finally: + Pop() + + if OPTIONS.text or not compare_files: + Banner("target files") + target_files.PrintCerts() + target_files.CheckSharedUids() + if compare_files: + if OPTIONS.text: + Banner("comparison files") + compare_files.PrintCerts() + target_files.CompareWith(compare_files) + + if PROBLEMS: + print "%d problem(s) found:\n" % (len(PROBLEMS),) + for p in PROBLEMS: + print p + return 1 + + return 0 + + +if __name__ == '__main__': + try: + r = main(sys.argv[1:]) + sys.exit(r) + except common.ExternalError, e: + print + print " ERROR: %s" % (e,) + print + sys.exit(1) diff --git a/tools/releasetools/common.py b/tools/releasetools/common.py index 26f216db1..0e17a5fa7 100644 --- a/tools/releasetools/common.py +++ b/tools/releasetools/common.py @@ -141,12 +141,15 @@ def AddBoot(output_zip): BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"), "boot.img", output_zip) -def UnzipTemp(filename): +def UnzipTemp(filename, pattern=None): """Unzip the given archive into a temporary directory and return the name.""" tmp = tempfile.mkdtemp(prefix="targetfiles-") OPTIONS.tempfiles.append(tmp) - p = Run(["unzip", "-o", "-q", filename, "-d", tmp], stdout=subprocess.PIPE) + cmd = ["unzip", "-o", "-q", filename, "-d", tmp] + if pattern is not None: + cmd.append(pattern) + p = Run(cmd, stdout=subprocess.PIPE) p.communicate() if p.returncode != 0: raise ExternalError("failed to unzip input target-files \"%s\"" %