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.package] = 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,
    320                                   self.max_pkg_len, apk.package)
    321       print
    322 
    323   def CompareWith(self, other):
    324     """Look for instances where a given package that exists in both
    325     self and other have different certs."""
    326 
    327     all_apks = set(self.apks.keys())
    328     all_apks.update(other.apks.keys())
    329 
    330     max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
    331 
    332     by_certpair = {}
    333 
    334     for i in all_apks:
    335       if i in self.apks:
    336         if i in other.apks:
    337           # in both; should have same set of certs
    338           if self.apks[i].certs != other.apks[i].certs:
    339             by_certpair.setdefault((other.apks[i].certs,
    340                                     self.apks[i].certs), []).append(i)
    341         else:
    342           print "%s [%s]: new APK (not in comparison target_files)" % (
    343               i, self.apks[i].filename)
    344       else:
    345         if i in other.apks:
    346           print "%s [%s]: removed APK (only in comparison target_files)" % (
    347               i, other.apks[i].filename)
    348 
    349     if by_certpair:
    350       AddProblem("some APKs changed certs")
    351       Banner("APK signing differences")
    352       for (old, new), packages in sorted(by_certpair.items()):
    353         for i, o in enumerate(old):
    354           if i == 0:
    355             print "was", ALL_CERTS.Get(o)
    356           else:
    357             print "   ", ALL_CERTS.Get(o)
    358         for i, n in enumerate(new):
    359           if i == 0:
    360             print "now", ALL_CERTS.Get(n)
    361           else:
    362             print "   ", ALL_CERTS.Get(n)
    363         for i in sorted(packages):
    364           old_fn = other.apks[i].filename
    365           new_fn = self.apks[i].filename
    366           if old_fn == new_fn:
    367             print "  %-*s  [%s]" % (max_pkg_len, i, old_fn)
    368           else:
    369             print "  %-*s  [was: %s; now: %s]" % (max_pkg_len, i,
    370                                                   old_fn, new_fn)
    371         print
    372 
    373 
    374 def main(argv):
    375   def option_handler(o, a):
    376     if o in ("-c", "--compare_with"):
    377       OPTIONS.compare_with = a
    378     elif o in ("-l", "--local_cert_dirs"):
    379       OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
    380     elif o in ("-t", "--text"):
    381       OPTIONS.text = True
    382     else:
    383       return False
    384     return True
    385 
    386   args = common.ParseOptions(argv, __doc__,
    387                              extra_opts="c:l:t",
    388                              extra_long_opts=["compare_with=",
    389                                               "local_cert_dirs="],
    390                              extra_option_handler=option_handler)
    391 
    392   if len(args) != 1:
    393     common.Usage(__doc__)
    394     sys.exit(1)
    395 
    396   ALL_CERTS.FindLocalCerts()
    397 
    398   Push("input target_files:")
    399   try:
    400     target_files = TargetFiles()
    401     target_files.LoadZipFile(args[0])
    402   finally:
    403     Pop()
    404 
    405   compare_files = None
    406   if OPTIONS.compare_with:
    407     Push("comparison target_files:")
    408     try:
    409       compare_files = TargetFiles()
    410       compare_files.LoadZipFile(OPTIONS.compare_with)
    411     finally:
    412       Pop()
    413 
    414   if OPTIONS.text or not compare_files:
    415     Banner("target files")
    416     target_files.PrintCerts()
    417   target_files.CheckSharedUids()
    418   target_files.CheckExternalSignatures()
    419   if compare_files:
    420     if OPTIONS.text:
    421       Banner("comparison files")
    422       compare_files.PrintCerts()
    423     target_files.CompareWith(compare_files)
    424 
    425   if PROBLEMS:
    426     print "%d problem(s) found:\n" % (len(PROBLEMS),)
    427     for p in PROBLEMS:
    428       print p
    429     return 1
    430 
    431   return 0
    432 
    433 
    434 if __name__ == '__main__':
    435   try:
    436     r = main(sys.argv[1:])
    437     sys.exit(r)
    438   except common.ExternalError as e:
    439     print
    440     print "   ERROR: %s" % (e,)
    441     print
    442     sys.exit(1)
    443