• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.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
373def 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
433if __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