check_target_files_signatures revision 278c9781ec5d2ef289fee635dfcfd8befcfe531b
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"""
18Check 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
20target_files (usually a previously distributed build for the same
21device) and flag any changes.
22
23Usage:  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
42import sys
43
44if sys.hexversion < 0x02040000:
45  print >> sys.stderr, "Python 2.4 or newer is required."
46  sys.exit(1)
47
48import os
49import re
50import shutil
51import subprocess
52import tempfile
53import zipfile
54
55try:
56  from hashlib import sha1 as sha1
57except ImportError:
58  from sha import sha as sha1
59
60import 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).
67class MyZipInfo(zipfile.ZipInfo):
68  def _decodeExtra(self):
69    pass
70zipfile.ZipInfo = MyZipInfo
71
72OPTIONS = common.OPTIONS
73
74OPTIONS.text = False
75OPTIONS.compare_with = None
76OPTIONS.local_cert_dirs = ("vendor", "build")
77
78PROBLEMS = []
79PROBLEM_PREFIX = []
80
81def AddProblem(msg):
82  PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
83def Push(msg):
84  PROBLEM_PREFIX.append(msg)
85def Pop():
86  PROBLEM_PREFIX.pop()
87
88
89def Banner(msg):
90  print "-" * 70
91  print "  ", msg
92  print "-" * 70
93
94
95def 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
109class 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
144ALL_CERTS = CertDB()
145
146
147def 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
162def 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
187class 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
245class 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 same set of certs
351          if self.apks[i].certs != other.apks[i].certs:
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
387def 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
447if __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