Home | History | Annotate | Download | only in releasetools
      1 #!/usr/bin/env python
      2 #
      3 # Copyright (C) 2009 The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 
     17 """
     18 Check the signatures of all APKs in a target_files .zip file.  With
     19 -c, compare the signatures of each package to the ones in a separate
     20 target_files (usually a previously distributed build for the same
     21 device) and flag any changes.
     22 
     23 Usage:  check_target_file_signatures [flags] target_files
     24 
     25   -c  (--compare_with)  <other_target_files>
     26       Look for compatibility problems between the two sets of target
     27       files (eg., packages whose keys have changed).
     28 
     29   -l  (--local_cert_dirs)  <dir,dir,...>
     30       Comma-separated list of top-level directories to scan for
     31       .x509.pem files.  Defaults to "vendor,build".  Where cert files
     32       can be found that match APK signatures, the filename will be
     33       printed as the cert name, otherwise a hash of the cert plus its
     34       subject string will be printed instead.
     35 
     36   -t  (--text)
     37       Dump the certificate information for both packages in comparison
     38       mode (this output is normally suppressed).
     39 
     40 """
     41 
     42 import sys
     43 
     44 if sys.hexversion < 0x02040000:
     45   print >> sys.stderr, "Python 2.4 or newer is required."
     46   sys.exit(1)
     47 
     48 import os
     49 import re
     50 import sha
     51 import shutil
     52 import subprocess
     53 import tempfile
     54 import zipfile
     55 
     56 import common
     57 
     58 # Work around a bug in python's zipfile module that prevents opening
     59 # of zipfiles if any entry has an extra field of between 1 and 3 bytes
     60 # (which is common with zipaligned APKs).  This overrides the
     61 # ZipInfo._decodeExtra() method (which contains the bug) with an empty
     62 # version (since we don't need to decode the extra field anyway).
     63 class MyZipInfo(zipfile.ZipInfo):
     64   def _decodeExtra(self):
     65     pass
     66 zipfile.ZipInfo = MyZipInfo
     67 
     68 OPTIONS = common.OPTIONS
     69 
     70 OPTIONS.text = False
     71 OPTIONS.compare_with = None
     72 OPTIONS.local_cert_dirs = ("vendor", "build")
     73 
     74 PROBLEMS = []
     75 PROBLEM_PREFIX = []
     76 
     77 def AddProblem(msg):
     78   PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
     79 def Push(msg):
     80   PROBLEM_PREFIX.append(msg)
     81 def Pop():
     82   PROBLEM_PREFIX.pop()
     83 
     84 
     85 def Banner(msg):
     86   print "-" * 70
     87   print "  ", msg
     88   print "-" * 70
     89 
     90 
     91 def GetCertSubject(cert):
     92   p = common.Run(["openssl", "x509", "-inform", "DER", "-text"],
     93                  stdin=subprocess.PIPE,
     94                  stdout=subprocess.PIPE)
     95   out, err = p.communicate(cert)
     96   if err and not err.strip():
     97     return "(error reading cert subject)"
     98   for line in out.split("\n"):
     99     line = line.strip()
    100     if line.startswith("Subject:"):
    101       return line[8:].strip()
    102   return "(unknown cert subject)"
    103 
    104 
    105 class CertDB(object):
    106   def __init__(self):
    107     self.certs = {}
    108 
    109   def Add(self, cert, name=None):
    110     if cert in self.certs:
    111       if name:
    112         self.certs[cert] = self.certs[cert] + "," + name
    113     else:
    114       if name is None:
    115         name = "unknown cert %s (%s)" % (sha.sha(cert).hexdigest()[:12],
    116                                          GetCertSubject(cert))
    117       self.certs[cert] = name
    118 
    119   def Get(self, cert):
    120     """Return the name for a given cert."""
    121     return self.certs.get(cert, None)
    122 
    123   def FindLocalCerts(self):
    124     to_load = []
    125     for top in OPTIONS.local_cert_dirs:
    126       for dirpath, dirnames, filenames in os.walk(top):
    127         certs = [os.path.join(dirpath, i)
    128                  for i in filenames if i.endswith(".x509.pem")]
    129         if certs:
    130           to_load.extend(certs)
    131 
    132     for i in to_load:
    133       f = open(i)
    134       cert = ParseCertificate(f.read())
    135       f.close()
    136       name, _ = os.path.splitext(i)
    137       name, _ = os.path.splitext(name)
    138       self.Add(cert, name)
    139 
    140 ALL_CERTS = CertDB()
    141 
    142 
    143 def ParseCertificate(data):
    144   """Parse a PEM-format certificate."""
    145   cert = []
    146   save = False
    147   for line in data.split("\n"):
    148     if "--END CERTIFICATE--" in line:
    149       break
    150     if save:
    151       cert.append(line)
    152     if "--BEGIN CERTIFICATE--" in line:
    153       save = True
    154   cert = "".join(cert).decode('base64')
    155   return cert
    156 
    157 
    158 def CertFromPKCS7(data, filename):
    159   """Read the cert out of a PKCS#7-format file (which is what is
    160   stored in a signed .apk)."""
    161   Push(filename + ":")
    162   try:
    163     p = common.Run(["openssl", "pkcs7",
    164                     "-inform", "DER",
    165                     "-outform", "PEM",
    166                     "-print_certs"],
    167                    stdin=subprocess.PIPE,
    168                    stdout=subprocess.PIPE)
    169     out, err = p.communicate(data)
    170     if err and not err.strip():
    171       AddProblem("error reading cert:\n" + err)
    172       return None
    173 
    174     cert = ParseCertificate(out)
    175     if not cert:
    176       AddProblem("error parsing cert output")
    177       return None
    178     return cert
    179   finally:
    180     Pop()
    181 
    182 
    183 class APK(object):
    184   def __init__(self, full_filename, filename):
    185     self.filename = filename
    186     self.cert = None
    187     Push(filename+":")
    188     try:
    189       self.RecordCert(full_filename)
    190       self.ReadManifest(full_filename)
    191     finally:
    192       Pop()
    193 
    194   def RecordCert(self, full_filename):
    195     try:
    196       f = open(full_filename)
    197       apk = zipfile.ZipFile(f, "r")
    198       pkcs7 = None
    199       for info in apk.infolist():
    200         if info.filename.startswith("META-INF/") and \
    201            (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")):
    202           if pkcs7 is not None:
    203             AddProblem("multiple certs")
    204           pkcs7 = apk.read(info.filename)
    205           self.cert = CertFromPKCS7(pkcs7, info.filename)
    206           ALL_CERTS.Add(self.cert)
    207       if not pkcs7:
    208         AddProblem("no signature")
    209     finally:
    210       f.close()
    211 
    212   def ReadManifest(self, full_filename):
    213     p = common.Run(["aapt", "dump", "xmltree", full_filename,
    214                     "AndroidManifest.xml"],
    215                    stdout=subprocess.PIPE)
    216     manifest, err = p.communicate()
    217     if err:
    218       AddProblem("failed to read manifest")
    219       return
    220 
    221     self.shared_uid = None
    222     self.package = None
    223 
    224     for line in manifest.split("\n"):
    225       line = line.strip()
    226       m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
    227       if m:
    228         name = m.group(1)
    229         if name == "android:sharedUserId":
    230           if self.shared_uid is not None:
    231             AddProblem("multiple sharedUserId declarations")
    232           self.shared_uid = m.group(2)
    233         elif name == "package":
    234           if self.package is not None:
    235             AddProblem("multiple package declarations")
    236           self.package = m.group(2)
    237 
    238     if self.package is None:
    239       AddProblem("no package declaration")
    240 
    241 
    242 class TargetFiles(object):
    243   def __init__(self):
    244     self.max_pkg_len = 30
    245     self.max_fn_len = 20
    246 
    247   def LoadZipFile(self, filename):
    248     d = common.UnzipTemp(filename, '*.apk')
    249     try:
    250       self.apks = {}
    251       self.apks_by_basename = {}
    252       for dirpath, dirnames, filenames in os.walk(d):
    253         for fn in filenames:
    254           if fn.endswith(".apk"):
    255             fullname = os.path.join(dirpath, fn)
    256             displayname = fullname[len(d)+1:]
    257             apk = APK(fullname, displayname)
    258             self.apks[apk.package] = apk
    259             self.apks_by_basename[os.path.basename(apk.filename)] = apk
    260 
    261             self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
    262             self.max_fn_len = max(self.max_fn_len, len(apk.filename))
    263     finally:
    264       shutil.rmtree(d)
    265 
    266     z = zipfile.ZipFile(open(filename, "rb"))
    267     self.certmap = common.ReadApkCerts(z)
    268     z.close()
    269 
    270   def CheckSharedUids(self):
    271     """Look for any instances where packages signed with different
    272     certs request the same sharedUserId."""
    273     apks_by_uid = {}
    274     for apk in self.apks.itervalues():
    275       if apk.shared_uid:
    276         apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
    277 
    278     for uid in sorted(apks_by_uid.keys()):
    279       apks = apks_by_uid[uid]
    280       for apk in apks[1:]:
    281         if apk.cert != apks[0].cert:
    282           break
    283       else:
    284         # all the certs are the same; this uid is fine
    285         continue
    286 
    287       AddProblem("uid %s shared across multiple certs" % (uid,))
    288 
    289       print "uid %s is shared by packages with different certs:" % (uid,)
    290       x = [(i.cert, i.package, i) for i in apks]
    291       x.sort()
    292       lastcert = None
    293       for cert, _, apk in x:
    294         if cert != lastcert:
    295           lastcert = cert
    296           print "    %s:" % (ALL_CERTS.Get(cert),)
    297         print "        %-*s  [%s]" % (self.max_pkg_len,
    298                                       apk.package, apk.filename)
    299       print
    300 
    301   def CheckExternalSignatures(self):
    302     for apk_filename, certname in self.certmap.iteritems():
    303       if certname == "EXTERNAL":
    304         # Apps marked EXTERNAL should be signed with the test key
    305         # during development, then manually re-signed after
    306         # predexopting.  Consider it an error if this app is now
    307         # signed with any key that is present in our tree.
    308         apk = self.apks_by_basename[apk_filename]
    309         name = ALL_CERTS.Get(apk.cert)
    310         if not name.startswith("unknown "):
    311           Push(apk.filename)
    312           AddProblem("hasn't been signed with EXTERNAL cert")
    313           Pop()
    314 
    315   def PrintCerts(self):
    316     """Display a table of packages grouped by cert."""
    317     by_cert = {}
    318     for apk in self.apks.itervalues():
    319       by_cert.setdefault(apk.cert, []).append((apk.package, apk))
    320 
    321     order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
    322     order.sort()
    323 
    324     for _, cert in order:
    325       print "%s:" % (ALL_CERTS.Get(cert),)
    326       apks = by_cert[cert]
    327       apks.sort()
    328       for _, apk in apks:
    329         if apk.shared_uid:
    330           print "  %-*s  %-*s  [%s]" % (self.max_fn_len, apk.filename,
    331                                         self.max_pkg_len, apk.package,
    332                                         apk.shared_uid)
    333         else:
    334           print "  %-*s  %-*s" % (self.max_fn_len, apk.filename,
    335                                   self.max_pkg_len, apk.package)
    336       print
    337 
    338   def CompareWith(self, other):
    339     """Look for instances where a given package that exists in both
    340     self and other have different certs."""
    341 
    342     all = set(self.apks.keys())
    343     all.update(other.apks.keys())
    344 
    345     max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
    346 
    347     by_certpair = {}
    348 
    349     for i in all:
    350       if i in self.apks:
    351         if i in other.apks:
    352           # in both; should have the same cert
    353           if self.apks[i].cert != other.apks[i].cert:
    354             by_certpair.setdefault((other.apks[i].cert,
    355                                     self.apks[i].cert), []).append(i)
    356         else:
    357           print "%s [%s]: new APK (not in comparison target_files)" % (
    358               i, self.apks[i].filename)
    359       else:
    360         if i in other.apks:
    361           print "%s [%s]: removed APK (only in comparison target_files)" % (
    362               i, other.apks[i].filename)
    363 
    364     if by_certpair:
    365       AddProblem("some APKs changed certs")
    366       Banner("APK signing differences")
    367       for (old, new), packages in sorted(by_certpair.items()):
    368         print "was", ALL_CERTS.Get(old)
    369         print "now", ALL_CERTS.Get(new)
    370         for i in sorted(packages):
    371           old_fn = other.apks[i].filename
    372           new_fn = self.apks[i].filename
    373           if old_fn == new_fn:
    374             print "  %-*s  [%s]" % (max_pkg_len, i, old_fn)
    375           else:
    376             print "  %-*s  [was: %s; now: %s]" % (max_pkg_len, i,
    377                                                   old_fn, new_fn)
    378         print
    379 
    380 
    381 def main(argv):
    382   def option_handler(o, a):
    383     if o in ("-c", "--compare_with"):
    384       OPTIONS.compare_with = a
    385     elif o in ("-l", "--local_cert_dirs"):
    386       OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
    387     elif o in ("-t", "--text"):
    388       OPTIONS.text = True
    389     else:
    390       return False
    391     return True
    392 
    393   args = common.ParseOptions(argv, __doc__,
    394                              extra_opts="c:l:t",
    395                              extra_long_opts=["compare_with=",
    396                                               "local_cert_dirs="],
    397                              extra_option_handler=option_handler)
    398 
    399   if len(args) != 1:
    400     common.Usage(__doc__)
    401     sys.exit(1)
    402 
    403   ALL_CERTS.FindLocalCerts()
    404 
    405   Push("input target_files:")
    406   try:
    407     target_files = TargetFiles()
    408     target_files.LoadZipFile(args[0])
    409   finally:
    410     Pop()
    411 
    412   compare_files = None
    413   if OPTIONS.compare_with:
    414     Push("comparison target_files:")
    415     try:
    416       compare_files = TargetFiles()
    417       compare_files.LoadZipFile(OPTIONS.compare_with)
    418     finally:
    419       Pop()
    420 
    421   if OPTIONS.text or not compare_files:
    422     Banner("target files")
    423     target_files.PrintCerts()
    424   target_files.CheckSharedUids()
    425   target_files.CheckExternalSignatures()
    426   if compare_files:
    427     if OPTIONS.text:
    428       Banner("comparison files")
    429       compare_files.PrintCerts()
    430     target_files.CompareWith(compare_files)
    431 
    432   if PROBLEMS:
    433     print "%d problem(s) found:\n" % (len(PROBLEMS),)
    434     for p in PROBLEMS:
    435       print p
    436     return 1
    437 
    438   return 0
    439 
    440 
    441 if __name__ == '__main__':
    442   try:
    443     r = main(sys.argv[1:])
    444     sys.exit(r)
    445   except common.ExternalError, e:
    446     print
    447     print "   ERROR: %s" % (e,)
    448     print
    449     sys.exit(1)
    450