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     self.certs = set()
    191     Push(filename+":")
    192     try:
    193       self.RecordCert(full_filename)
    194       self.ReadManifest(full_filename)
    195     finally:
    196       Pop()
    197 
    198   def RecordCert(self, full_filename):
    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           self.certs.add(cert)
    209           ALL_CERTS.Add(cert)
    210       if not pkcs7:
    211         AddProblem("no signature")
    212     finally:
    213       f.close()
    214 
    215   def ReadManifest(self, full_filename):
    216     p = common.Run(["aapt", "dump", "xmltree", full_filename,
    217                     "AndroidManifest.xml"],
    218                    stdout=subprocess.PIPE)
    219     manifest, err = p.communicate()
    220     if err:
    221       AddProblem("failed to read manifest")
    222       return
    223 
    224     self.shared_uid = None
    225     self.package = None
    226 
    227     for line in manifest.split("\n"):
    228       line = line.strip()
    229       m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
    230       if m:
    231         name = m.group(1)
    232         if name == "android:sharedUserId":
    233           if self.shared_uid is not None:
    234             AddProblem("multiple sharedUserId declarations")
    235           self.shared_uid = m.group(2)
    236         elif name == "package":
    237           if self.package is not None:
    238             AddProblem("multiple package declarations")
    239           self.package = m.group(2)
    240 
    241     if self.package is None:
    242       AddProblem("no package declaration")
    243 
    244 
    245 class TargetFiles(object):
    246   def __init__(self):
    247     self.max_pkg_len = 30
    248     self.max_fn_len = 20
    249 
    250   def LoadZipFile(self, filename):
    251     d, z = common.UnzipTemp(filename, '*.apk')
    252     try:
    253       self.apks = {}
    254       self.apks_by_basename = {}
    255       for dirpath, dirnames, filenames in os.walk(d):
    256         for fn in filenames:
    257           if fn.endswith(".apk"):
    258             fullname = os.path.join(dirpath, fn)
    259             displayname = fullname[len(d)+1:]
    260             apk = APK(fullname, displayname)
    261             self.apks[apk.package] = apk
    262             self.apks_by_basename[os.path.basename(apk.filename)] = apk
    263 
    264             self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
    265             self.max_fn_len = max(self.max_fn_len, len(apk.filename))
    266     finally:
    267       shutil.rmtree(d)
    268 
    269     self.certmap = common.ReadApkCerts(z)
    270     z.close()
    271 
    272   def CheckSharedUids(self):
    273     """Look for any instances where packages signed with different
    274     certs request the same sharedUserId."""
    275     apks_by_uid = {}
    276     for apk in self.apks.itervalues():
    277       if apk.shared_uid:
    278         apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
    279 
    280     for uid in sorted(apks_by_uid.keys()):
    281       apks = apks_by_uid[uid]
    282       for apk in apks[1:]:
    283         if apk.certs != apks[0].certs:
    284           break
    285       else:
    286         # all packages have the same set of certs; this uid is fine.
    287         continue
    288 
    289       AddProblem("different cert sets for packages with uid %s" % (uid,))
    290 
    291       print "uid %s is shared by packages with different cert sets:" % (uid,)
    292       for apk in apks:
    293         print "%-*s  [%s]" % (self.max_pkg_len, apk.package, apk.filename)
    294         for cert in apk.certs:
    295           print "   ", ALL_CERTS.Get(cert)
    296       print
    297 
    298   def CheckExternalSignatures(self):
    299     for apk_filename, certname in self.certmap.iteritems():
    300       if certname == "EXTERNAL":
    301         # Apps marked EXTERNAL should be signed with the test key
    302         # during development, then manually re-signed after
    303         # predexopting.  Consider it an error if this app is now
    304         # signed with any key that is present in our tree.
    305         apk = self.apks_by_basename[apk_filename]
    306         name = ALL_CERTS.Get(apk.cert)
    307         if not name.startswith("unknown "):
    308           Push(apk.filename)
    309           AddProblem("hasn't been signed with EXTERNAL cert")
    310           Pop()
    311 
    312   def PrintCerts(self):
    313     """Display a table of packages grouped by cert."""
    314     by_cert = {}
    315     for apk in self.apks.itervalues():
    316       for cert in apk.certs:
    317         by_cert.setdefault(cert, []).append((apk.package, apk))
    318 
    319     order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
    320     order.sort()
    321 
    322     for _, cert in order:
    323       print "%s:" % (ALL_CERTS.Get(cert),)
    324       apks = by_cert[cert]
    325       apks.sort()
    326       for _, apk in apks:
    327         if apk.shared_uid:
    328           print "  %-*s  %-*s  [%s]" % (self.max_fn_len, apk.filename,
    329                                         self.max_pkg_len, apk.package,
    330                                         apk.shared_uid)
    331         else:
    332           print "  %-*s  %-*s" % (self.max_fn_len, apk.filename,
    333                                   self.max_pkg_len, apk.package)
    334       print
    335 
    336   def CompareWith(self, other):
    337     """Look for instances where a given package that exists in both
    338     self and other have different certs."""
    339 
    340     all = set(self.apks.keys())
    341     all.update(other.apks.keys())
    342 
    343     max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
    344 
    345     by_certpair = {}
    346 
    347     for i in all:
    348       if i in self.apks:
    349         if i in other.apks:
    350           # in both; should have at least one cert in common
    351           if not (self.apks[i].cert & other.apks[i].cert):
    352             by_certpair.setdefault((other.apks[i].certs,
    353                                     self.apks[i].certs), []).append(i)
    354         else:
    355           print "%s [%s]: new APK (not in comparison target_files)" % (
    356               i, self.apks[i].filename)
    357       else:
    358         if i in other.apks:
    359           print "%s [%s]: removed APK (only in comparison target_files)" % (
    360               i, other.apks[i].filename)
    361 
    362     if by_certpair:
    363       AddProblem("some APKs changed certs")
    364       Banner("APK signing differences")
    365       for (old, new), packages in sorted(by_certpair.items()):
    366         for i, o in enumerate(old):
    367           if i == 0:
    368             print "was", ALL_CERTS.Get(o)
    369           else:
    370             print "   ", ALL_CERTS.Get(o)
    371         for i, n in enumerate(new):
    372           if i == 0:
    373             print "now", ALL_CERTS.Get(n)
    374           else:
    375             print "   ", ALL_CERTS.Get(n)
    376         for i in sorted(packages):
    377           old_fn = other.apks[i].filename
    378           new_fn = self.apks[i].filename
    379           if old_fn == new_fn:
    380             print "  %-*s  [%s]" % (max_pkg_len, i, old_fn)
    381           else:
    382             print "  %-*s  [was: %s; now: %s]" % (max_pkg_len, i,
    383                                                   old_fn, new_fn)
    384         print
    385 
    386 
    387 def main(argv):
    388   def option_handler(o, a):
    389     if o in ("-c", "--compare_with"):
    390       OPTIONS.compare_with = a
    391     elif o in ("-l", "--local_cert_dirs"):
    392       OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
    393     elif o in ("-t", "--text"):
    394       OPTIONS.text = True
    395     else:
    396       return False
    397     return True
    398 
    399   args = common.ParseOptions(argv, __doc__,
    400                              extra_opts="c:l:t",
    401                              extra_long_opts=["compare_with=",
    402                                               "local_cert_dirs="],
    403                              extra_option_handler=option_handler)
    404 
    405   if len(args) != 1:
    406     common.Usage(__doc__)
    407     sys.exit(1)
    408 
    409   ALL_CERTS.FindLocalCerts()
    410 
    411   Push("input target_files:")
    412   try:
    413     target_files = TargetFiles()
    414     target_files.LoadZipFile(args[0])
    415   finally:
    416     Pop()
    417 
    418   compare_files = None
    419   if OPTIONS.compare_with:
    420     Push("comparison target_files:")
    421     try:
    422       compare_files = TargetFiles()
    423       compare_files.LoadZipFile(OPTIONS.compare_with)
    424     finally:
    425       Pop()
    426 
    427   if OPTIONS.text or not compare_files:
    428     Banner("target files")
    429     target_files.PrintCerts()
    430   target_files.CheckSharedUids()
    431   target_files.CheckExternalSignatures()
    432   if compare_files:
    433     if OPTIONS.text:
    434       Banner("comparison files")
    435       compare_files.PrintCerts()
    436     target_files.CompareWith(compare_files)
    437 
    438   if PROBLEMS:
    439     print "%d problem(s) found:\n" % (len(PROBLEMS),)
    440     for p in PROBLEMS:
    441       print p
    442     return 1
    443 
    444   return 0
    445 
    446 
    447 if __name__ == '__main__':
    448   try:
    449     r = main(sys.argv[1:])
    450     sys.exit(r)
    451   except common.ExternalError, e:
    452     print
    453     print "   ERROR: %s" % (e,)
    454     print
    455     sys.exit(1)
    456