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 zipfile
     53 
     54 import common
     55 
     56 # Work around a bug in python's zipfile module that prevents opening
     57 # of zipfiles if any entry has an extra field of between 1 and 3 bytes
     58 # (which is common with zipaligned APKs).  This overrides the
     59 # ZipInfo._decodeExtra() method (which contains the bug) with an empty
     60 # version (since we don't need to decode the extra field anyway).
     61 class MyZipInfo(zipfile.ZipInfo):
     62   def _decodeExtra(self):
     63     pass
     64 zipfile.ZipInfo = MyZipInfo
     65 
     66 OPTIONS = common.OPTIONS
     67 
     68 OPTIONS.text = False
     69 OPTIONS.compare_with = None
     70 OPTIONS.local_cert_dirs = ("vendor", "build")
     71 
     72 PROBLEMS = []
     73 PROBLEM_PREFIX = []
     74 
     75 def AddProblem(msg):
     76   PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
     77 def Push(msg):
     78   PROBLEM_PREFIX.append(msg)
     79 def Pop():
     80   PROBLEM_PREFIX.pop()
     81 
     82 
     83 def Banner(msg):
     84   print "-" * 70
     85   print "  ", msg
     86   print "-" * 70
     87 
     88 
     89 def GetCertSubject(cert):
     90   p = common.Run(["openssl", "x509", "-inform", "DER", "-text"],
     91                  stdin=subprocess.PIPE,
     92                  stdout=subprocess.PIPE)
     93   out, err = p.communicate(cert)
     94   if err and not err.strip():
     95     return "(error reading cert subject)"
     96   for line in out.split("\n"):
     97     line = line.strip()
     98     if line.startswith("Subject:"):
     99       return line[8:].strip()
    100   return "(unknown cert subject)"
    101 
    102 
    103 class CertDB(object):
    104   def __init__(self):
    105     self.certs = {}
    106 
    107   def Add(self, cert, name=None):
    108     if cert in self.certs:
    109       if name:
    110         self.certs[cert] = self.certs[cert] + "," + name
    111     else:
    112       if name is None:
    113         name = "unknown cert %s (%s)" % (common.sha1(cert).hexdigest()[:12],
    114                                          GetCertSubject(cert))
    115       self.certs[cert] = name
    116 
    117   def Get(self, cert):
    118     """Return the name for a given cert."""
    119     return self.certs.get(cert, None)
    120 
    121   def FindLocalCerts(self):
    122     to_load = []
    123     for top in OPTIONS.local_cert_dirs:
    124       for dirpath, _, filenames in os.walk(top):
    125         certs = [os.path.join(dirpath, i)
    126                  for i in filenames if i.endswith(".x509.pem")]
    127         if certs:
    128           to_load.extend(certs)
    129 
    130     for i in to_load:
    131       f = open(i)
    132       cert = common.ParseCertificate(f.read())
    133       f.close()
    134       name, _ = os.path.splitext(i)
    135       name, _ = os.path.splitext(name)
    136       self.Add(cert, name)
    137 
    138 ALL_CERTS = CertDB()
    139 
    140 
    141 def CertFromPKCS7(data, filename):
    142   """Read the cert out of a PKCS#7-format file (which is what is
    143   stored in a signed .apk)."""
    144   Push(filename + ":")
    145   try:
    146     p = common.Run(["openssl", "pkcs7",
    147                     "-inform", "DER",
    148                     "-outform", "PEM",
    149                     "-print_certs"],
    150                    stdin=subprocess.PIPE,
    151                    stdout=subprocess.PIPE)
    152     out, err = p.communicate(data)
    153     if err and not err.strip():
    154       AddProblem("error reading cert:\n" + err)
    155       return None
    156 
    157     cert = common.ParseCertificate(out)
    158     if not cert:
    159       AddProblem("error parsing cert output")
    160       return None
    161     return cert
    162   finally:
    163     Pop()
    164 
    165 
    166 class APK(object):
    167   def __init__(self, full_filename, filename):
    168     self.filename = filename
    169     self.certs = None
    170     self.shared_uid = None
    171     self.package = None
    172 
    173     Push(filename+":")
    174     try:
    175       self.RecordCerts(full_filename)
    176       self.ReadManifest(full_filename)
    177     finally:
    178       Pop()
    179 
    180   def RecordCerts(self, full_filename):
    181     out = set()
    182     try:
    183       f = open(full_filename)
    184       apk = zipfile.ZipFile(f, "r")
    185       pkcs7 = None
    186       for info in apk.infolist():
    187         if info.filename.startswith("META-INF/") and \
    188            (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")):
    189           pkcs7 = apk.read(info.filename)
    190           cert = CertFromPKCS7(pkcs7, info.filename)
    191           out.add(cert)
    192           ALL_CERTS.Add(cert)
    193       if not pkcs7:
    194         AddProblem("no signature")
    195     finally:
    196       f.close()
    197       self.certs = frozenset(out)
    198 
    199   def ReadManifest(self, full_filename):
    200     p = common.Run(["aapt", "dump", "xmltree", full_filename,
    201                     "AndroidManifest.xml"],
    202                    stdout=subprocess.PIPE)
    203     manifest, err = p.communicate()
    204     if err:
    205       AddProblem("failed to read manifest")
    206       return
    207 
    208     self.shared_uid = None
    209     self.package = None
    210 
    211     for line in manifest.split("\n"):
    212       line = line.strip()
    213       m = re.search(r'A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
    214       if m:
    215         name = m.group(1)
    216         if name == "android:sharedUserId":
    217           if self.shared_uid is not None:
    218             AddProblem("multiple sharedUserId declarations")
    219           self.shared_uid = m.group(2)
    220         elif name == "package":
    221           if self.package is not None:
    222             AddProblem("multiple package declarations")
    223           self.package = m.group(2)
    224 
    225     if self.package is None:
    226       AddProblem("no package declaration")
    227 
    228 
    229 class TargetFiles(object):
    230   def __init__(self):
    231     self.max_pkg_len = 30
    232     self.max_fn_len = 20
    233     self.apks = None
    234     self.apks_by_basename = None
    235     self.certmap = None
    236 
    237   def LoadZipFile(self, filename):
    238     d, z = common.UnzipTemp(filename, '*.apk')
    239     try:
    240       self.apks = {}
    241       self.apks_by_basename = {}
    242       for dirpath, _, filenames in os.walk(d):
    243         for fn in filenames:
    244           if fn.endswith(".apk"):
    245             fullname = os.path.join(dirpath, fn)
    246             displayname = fullname[len(d)+1:]
    247             apk = APK(fullname, displayname)
    248             self.apks[apk.filename] = apk
    249             self.apks_by_basename[os.path.basename(apk.filename)] = apk
    250 
    251             self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
    252             self.max_fn_len = max(self.max_fn_len, len(apk.filename))
    253     finally:
    254       shutil.rmtree(d)
    255 
    256     self.certmap = common.ReadApkCerts(z)
    257     z.close()
    258 
    259   def CheckSharedUids(self):
    260     """Look for any instances where packages signed with different
    261     certs request the same sharedUserId."""
    262     apks_by_uid = {}
    263     for apk in self.apks.itervalues():
    264       if apk.shared_uid:
    265         apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
    266 
    267     for uid in sorted(apks_by_uid.keys()):
    268       apks = apks_by_uid[uid]
    269       for apk in apks[1:]:
    270         if apk.certs != apks[0].certs:
    271           break
    272       else:
    273         # all packages have the same set of certs; this uid is fine.
    274         continue
    275 
    276       AddProblem("different cert sets for packages with uid %s" % (uid,))
    277 
    278       print "uid %s is shared by packages with different cert sets:" % (uid,)
    279       for apk in apks:
    280         print "%-*s  [%s]" % (self.max_pkg_len, apk.package, apk.filename)
    281         for cert in apk.certs:
    282           print "   ", ALL_CERTS.Get(cert)
    283       print
    284 
    285   def CheckExternalSignatures(self):
    286     for apk_filename, certname in self.certmap.iteritems():
    287       if certname == "EXTERNAL":
    288         # Apps marked EXTERNAL should be signed with the test key
    289         # during development, then manually re-signed after
    290         # predexopting.  Consider it an error if this app is now
    291         # signed with any key that is present in our tree.
    292         apk = self.apks_by_basename[apk_filename]
    293         name = ALL_CERTS.Get(apk.cert)
    294         if not name.startswith("unknown "):
    295           Push(apk.filename)
    296           AddProblem("hasn't been signed with EXTERNAL cert")
    297           Pop()
    298 
    299   def PrintCerts(self):
    300     """Display a table of packages grouped by cert."""
    301     by_cert = {}
    302     for apk in self.apks.itervalues():
    303       for cert in apk.certs:
    304         by_cert.setdefault(cert, []).append((apk.package, apk))
    305 
    306     order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
    307     order.sort()
    308 
    309     for _, cert in order:
    310       print "%s:" % (ALL_CERTS.Get(cert),)
    311       apks = by_cert[cert]
    312       apks.sort()
    313       for _, apk in apks:
    314         if apk.shared_uid:
    315           print "  %-*s  %-*s  [%s]" % (self.max_fn_len, apk.filename,
    316                                         self.max_pkg_len, apk.package,
    317                                         apk.shared_uid)
    318         else:
    319           print "  %-*s  %s" % (self.max_fn_len, apk.filename, 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_apks = set(self.apks.keys())
    327     all_apks.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_apks:
    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 as e:
    438     print
    439     print "   ERROR: %s" % (e,)
    440     print
    441     sys.exit(1)
    442