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