check_target_files_signatures.py revision a5f534df07a598f3fc9d353a1a9a0ff6c09cfbc0
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    Push(filename+":")
191    try:
192      self.RecordCerts(full_filename)
193      self.ReadManifest(full_filename)
194    finally:
195      Pop()
196
197  def RecordCerts(self, full_filename):
198    out = set()
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          out.add(cert)
209          ALL_CERTS.Add(cert)
210      if not pkcs7:
211        AddProblem("no signature")
212    finally:
213      f.close()
214      self.certs = frozenset(out)
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, z = 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    self.certmap = common.ReadApkCerts(z)
271    z.close()
272
273  def CheckSharedUids(self):
274    """Look for any instances where packages signed with different
275    certs request the same sharedUserId."""
276    apks_by_uid = {}
277    for apk in self.apks.itervalues():
278      if apk.shared_uid:
279        apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
280
281    for uid in sorted(apks_by_uid.keys()):
282      apks = apks_by_uid[uid]
283      for apk in apks[1:]:
284        if apk.certs != apks[0].certs:
285          break
286      else:
287        # all packages have the same set of certs; this uid is fine.
288        continue
289
290      AddProblem("different cert sets for packages with uid %s" % (uid,))
291
292      print "uid %s is shared by packages with different cert sets:" % (uid,)
293      for apk in apks:
294        print "%-*s  [%s]" % (self.max_pkg_len, apk.package, apk.filename)
295        for cert in apk.certs:
296          print "   ", ALL_CERTS.Get(cert)
297      print
298
299  def CheckExternalSignatures(self):
300    for apk_filename, certname in self.certmap.iteritems():
301      if certname == "EXTERNAL":
302        # Apps marked EXTERNAL should be signed with the test key
303        # during development, then manually re-signed after
304        # predexopting.  Consider it an error if this app is now
305        # signed with any key that is present in our tree.
306        apk = self.apks_by_basename[apk_filename]
307        name = ALL_CERTS.Get(apk.cert)
308        if not name.startswith("unknown "):
309          Push(apk.filename)
310          AddProblem("hasn't been signed with EXTERNAL cert")
311          Pop()
312
313  def PrintCerts(self):
314    """Display a table of packages grouped by cert."""
315    by_cert = {}
316    for apk in self.apks.itervalues():
317      for cert in apk.certs:
318        by_cert.setdefault(cert, []).append((apk.package, apk))
319
320    order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
321    order.sort()
322
323    for _, cert in order:
324      print "%s:" % (ALL_CERTS.Get(cert),)
325      apks = by_cert[cert]
326      apks.sort()
327      for _, apk in apks:
328        if apk.shared_uid:
329          print "  %-*s  %-*s  [%s]" % (self.max_fn_len, apk.filename,
330                                        self.max_pkg_len, apk.package,
331                                        apk.shared_uid)
332        else:
333          print "  %-*s  %-*s" % (self.max_fn_len, apk.filename,
334                                  self.max_pkg_len, apk.package)
335      print
336
337  def CompareWith(self, other):
338    """Look for instances where a given package that exists in both
339    self and other have different certs."""
340
341    all = set(self.apks.keys())
342    all.update(other.apks.keys())
343
344    max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
345
346    by_certpair = {}
347
348    for i in all:
349      if i in self.apks:
350        if i in other.apks:
351          # in both; should have same set of certs
352          if self.apks[i].certs != other.apks[i].certs:
353            by_certpair.setdefault((other.apks[i].certs,
354                                    self.apks[i].certs), []).append(i)
355        else:
356          print "%s [%s]: new APK (not in comparison target_files)" % (
357              i, self.apks[i].filename)
358      else:
359        if i in other.apks:
360          print "%s [%s]: removed APK (only in comparison target_files)" % (
361              i, other.apks[i].filename)
362
363    if by_certpair:
364      AddProblem("some APKs changed certs")
365      Banner("APK signing differences")
366      for (old, new), packages in sorted(by_certpair.items()):
367        for i, o in enumerate(old):
368          if i == 0:
369            print "was", ALL_CERTS.Get(o)
370          else:
371            print "   ", ALL_CERTS.Get(o)
372        for i, n in enumerate(new):
373          if i == 0:
374            print "now", ALL_CERTS.Get(n)
375          else:
376            print "   ", ALL_CERTS.Get(n)
377        for i in sorted(packages):
378          old_fn = other.apks[i].filename
379          new_fn = self.apks[i].filename
380          if old_fn == new_fn:
381            print "  %-*s  [%s]" % (max_pkg_len, i, old_fn)
382          else:
383            print "  %-*s  [was: %s; now: %s]" % (max_pkg_len, i,
384                                                  old_fn, new_fn)
385        print
386
387
388def main(argv):
389  def option_handler(o, a):
390    if o in ("-c", "--compare_with"):
391      OPTIONS.compare_with = a
392    elif o in ("-l", "--local_cert_dirs"):
393      OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
394    elif o in ("-t", "--text"):
395      OPTIONS.text = True
396    else:
397      return False
398    return True
399
400  args = common.ParseOptions(argv, __doc__,
401                             extra_opts="c:l:t",
402                             extra_long_opts=["compare_with=",
403                                              "local_cert_dirs="],
404                             extra_option_handler=option_handler)
405
406  if len(args) != 1:
407    common.Usage(__doc__)
408    sys.exit(1)
409
410  ALL_CERTS.FindLocalCerts()
411
412  Push("input target_files:")
413  try:
414    target_files = TargetFiles()
415    target_files.LoadZipFile(args[0])
416  finally:
417    Pop()
418
419  compare_files = None
420  if OPTIONS.compare_with:
421    Push("comparison target_files:")
422    try:
423      compare_files = TargetFiles()
424      compare_files.LoadZipFile(OPTIONS.compare_with)
425    finally:
426      Pop()
427
428  if OPTIONS.text or not compare_files:
429    Banner("target files")
430    target_files.PrintCerts()
431  target_files.CheckSharedUids()
432  target_files.CheckExternalSignatures()
433  if compare_files:
434    if OPTIONS.text:
435      Banner("comparison files")
436      compare_files.PrintCerts()
437    target_files.CompareWith(compare_files)
438
439  if PROBLEMS:
440    print "%d problem(s) found:\n" % (len(PROBLEMS),)
441    for p in PROBLEMS:
442      print p
443    return 1
444
445  return 0
446
447
448if __name__ == '__main__':
449  try:
450    r = main(sys.argv[1:])
451    sys.exit(r)
452  except common.ExternalError, e:
453    print
454    print "   ERROR: %s" % (e,)
455    print
456    sys.exit(1)
457