check_target_files_signatures.py revision 8b72aefb5a8ed4da28c6f83854e8babf53b9cb53
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 < 0x02070000:
45  print >> sys.stderr, "Python 2.7 or newer is required."
46  sys.exit(1)
47
48import os
49import re
50import shutil
51import subprocess
52import zipfile
53
54import 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).
61class MyZipInfo(zipfile.ZipInfo):
62  def _decodeExtra(self):
63    pass
64zipfile.ZipInfo = MyZipInfo
65
66OPTIONS = common.OPTIONS
67
68OPTIONS.text = False
69OPTIONS.compare_with = None
70OPTIONS.local_cert_dirs = ("vendor", "build")
71
72PROBLEMS = []
73PROBLEM_PREFIX = []
74
75def AddProblem(msg):
76  PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
77def Push(msg):
78  PROBLEM_PREFIX.append(msg)
79def Pop():
80  PROBLEM_PREFIX.pop()
81
82
83def Banner(msg):
84  print "-" * 70
85  print "  ", msg
86  print "-" * 70
87
88
89def 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
103class 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
138ALL_CERTS = CertDB()
139
140
141def 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
166class 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
229class 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:
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
374def 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
434if __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