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