check_target_files_signatures revision cad0bb9f621ff1ccfb584e18249b09768c30a0c0
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)" % (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.cert = None
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          if pkcs7 is not None:
207            AddProblem("multiple certs")
208          pkcs7 = apk.read(info.filename)
209          self.cert = CertFromPKCS7(pkcs7, info.filename)
210          ALL_CERTS.Add(self.cert)
211      if not pkcs7:
212        AddProblem("no signature")
213    finally:
214      f.close()
215
216  def ReadManifest(self, full_filename):
217    p = common.Run(["aapt", "dump", "xmltree", full_filename,
218                    "AndroidManifest.xml"],
219                   stdout=subprocess.PIPE)
220    manifest, err = p.communicate()
221    if err:
222      AddProblem("failed to read manifest")
223      return
224
225    self.shared_uid = None
226    self.package = None
227
228    for line in manifest.split("\n"):
229      line = line.strip()
230      m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
231      if m:
232        name = m.group(1)
233        if name == "android:sharedUserId":
234          if self.shared_uid is not None:
235            AddProblem("multiple sharedUserId declarations")
236          self.shared_uid = m.group(2)
237        elif name == "package":
238          if self.package is not None:
239            AddProblem("multiple package declarations")
240          self.package = m.group(2)
241
242    if self.package is None:
243      AddProblem("no package declaration")
244
245
246class TargetFiles(object):
247  def __init__(self):
248    self.max_pkg_len = 30
249    self.max_fn_len = 20
250
251  def LoadZipFile(self, filename):
252    d = common.UnzipTemp(filename, '*.apk')
253    try:
254      self.apks = {}
255      self.apks_by_basename = {}
256      for dirpath, dirnames, filenames in os.walk(d):
257        for fn in filenames:
258          if fn.endswith(".apk"):
259            fullname = os.path.join(dirpath, fn)
260            displayname = fullname[len(d)+1:]
261            apk = APK(fullname, displayname)
262            self.apks[apk.package] = apk
263            self.apks_by_basename[os.path.basename(apk.filename)] = apk
264
265            self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
266            self.max_fn_len = max(self.max_fn_len, len(apk.filename))
267    finally:
268      shutil.rmtree(d)
269
270    z = zipfile.ZipFile(open(filename, "rb"))
271    self.certmap = common.ReadApkCerts(z)
272    z.close()
273
274  def CheckSharedUids(self):
275    """Look for any instances where packages signed with different
276    certs request the same sharedUserId."""
277    apks_by_uid = {}
278    for apk in self.apks.itervalues():
279      if apk.shared_uid:
280        apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
281
282    for uid in sorted(apks_by_uid.keys()):
283      apks = apks_by_uid[uid]
284      for apk in apks[1:]:
285        if apk.cert != apks[0].cert:
286          break
287      else:
288        # all the certs are the same; this uid is fine
289        continue
290
291      AddProblem("uid %s shared across multiple certs" % (uid,))
292
293      print "uid %s is shared by packages with different certs:" % (uid,)
294      x = [(i.cert, i.package, i) for i in apks]
295      x.sort()
296      lastcert = None
297      for cert, _, apk in x:
298        if cert != lastcert:
299          lastcert = cert
300          print "    %s:" % (ALL_CERTS.Get(cert),)
301        print "        %-*s  [%s]" % (self.max_pkg_len,
302                                      apk.package, apk.filename)
303      print
304
305  def CheckExternalSignatures(self):
306    for apk_filename, certname in self.certmap.iteritems():
307      if certname == "EXTERNAL":
308        # Apps marked EXTERNAL should be signed with the test key
309        # during development, then manually re-signed after
310        # predexopting.  Consider it an error if this app is now
311        # signed with any key that is present in our tree.
312        apk = self.apks_by_basename[apk_filename]
313        name = ALL_CERTS.Get(apk.cert)
314        if not name.startswith("unknown "):
315          Push(apk.filename)
316          AddProblem("hasn't been signed with EXTERNAL cert")
317          Pop()
318
319  def PrintCerts(self):
320    """Display a table of packages grouped by cert."""
321    by_cert = {}
322    for apk in self.apks.itervalues():
323      by_cert.setdefault(apk.cert, []).append((apk.package, apk))
324
325    order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
326    order.sort()
327
328    for _, cert in order:
329      print "%s:" % (ALL_CERTS.Get(cert),)
330      apks = by_cert[cert]
331      apks.sort()
332      for _, apk in apks:
333        if apk.shared_uid:
334          print "  %-*s  %-*s  [%s]" % (self.max_fn_len, apk.filename,
335                                        self.max_pkg_len, apk.package,
336                                        apk.shared_uid)
337        else:
338          print "  %-*s  %-*s" % (self.max_fn_len, apk.filename,
339                                  self.max_pkg_len, apk.package)
340      print
341
342  def CompareWith(self, other):
343    """Look for instances where a given package that exists in both
344    self and other have different certs."""
345
346    all = set(self.apks.keys())
347    all.update(other.apks.keys())
348
349    max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
350
351    by_certpair = {}
352
353    for i in all:
354      if i in self.apks:
355        if i in other.apks:
356          # in both; should have the same cert
357          if self.apks[i].cert != other.apks[i].cert:
358            by_certpair.setdefault((other.apks[i].cert,
359                                    self.apks[i].cert), []).append(i)
360        else:
361          print "%s [%s]: new APK (not in comparison target_files)" % (
362              i, self.apks[i].filename)
363      else:
364        if i in other.apks:
365          print "%s [%s]: removed APK (only in comparison target_files)" % (
366              i, other.apks[i].filename)
367
368    if by_certpair:
369      AddProblem("some APKs changed certs")
370      Banner("APK signing differences")
371      for (old, new), packages in sorted(by_certpair.items()):
372        print "was", ALL_CERTS.Get(old)
373        print "now", ALL_CERTS.Get(new)
374        for i in sorted(packages):
375          old_fn = other.apks[i].filename
376          new_fn = self.apks[i].filename
377          if old_fn == new_fn:
378            print "  %-*s  [%s]" % (max_pkg_len, i, old_fn)
379          else:
380            print "  %-*s  [was: %s; now: %s]" % (max_pkg_len, i,
381                                                  old_fn, new_fn)
382        print
383
384
385def main(argv):
386  def option_handler(o, a):
387    if o in ("-c", "--compare_with"):
388      OPTIONS.compare_with = a
389    elif o in ("-l", "--local_cert_dirs"):
390      OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
391    elif o in ("-t", "--text"):
392      OPTIONS.text = True
393    else:
394      return False
395    return True
396
397  args = common.ParseOptions(argv, __doc__,
398                             extra_opts="c:l:t",
399                             extra_long_opts=["compare_with=",
400                                              "local_cert_dirs="],
401                             extra_option_handler=option_handler)
402
403  if len(args) != 1:
404    common.Usage(__doc__)
405    sys.exit(1)
406
407  ALL_CERTS.FindLocalCerts()
408
409  Push("input target_files:")
410  try:
411    target_files = TargetFiles()
412    target_files.LoadZipFile(args[0])
413  finally:
414    Pop()
415
416  compare_files = None
417  if OPTIONS.compare_with:
418    Push("comparison target_files:")
419    try:
420      compare_files = TargetFiles()
421      compare_files.LoadZipFile(OPTIONS.compare_with)
422    finally:
423      Pop()
424
425  if OPTIONS.text or not compare_files:
426    Banner("target files")
427    target_files.PrintCerts()
428  target_files.CheckSharedUids()
429  target_files.CheckExternalSignatures()
430  if compare_files:
431    if OPTIONS.text:
432      Banner("comparison files")
433      compare_files.PrintCerts()
434    target_files.CompareWith(compare_files)
435
436  if PROBLEMS:
437    print "%d problem(s) found:\n" % (len(PROBLEMS),)
438    for p in PROBLEMS:
439      print p
440    return 1
441
442  return 0
443
444
445if __name__ == '__main__':
446  try:
447    r = main(sys.argv[1:])
448    sys.exit(r)
449  except common.ExternalError, e:
450    print
451    print "   ERROR: %s" % (e,)
452    print
453    sys.exit(1)
454