check_target_files_signatures.py revision 6ae5381670a12f63b4f7a18166e0886898e2083b
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
55import common
56
57# Work around a bug in python's zipfile module that prevents opening
58# of zipfiles if any entry has an extra field of between 1 and 3 bytes
59# (which is common with zipaligned APKs).  This overrides the
60# ZipInfo._decodeExtra() method (which contains the bug) with an empty
61# version (since we don't need to decode the extra field anyway).
62class MyZipInfo(zipfile.ZipInfo):
63  def _decodeExtra(self):
64    pass
65zipfile.ZipInfo = MyZipInfo
66
67OPTIONS = common.OPTIONS
68
69OPTIONS.text = False
70OPTIONS.compare_with = None
71OPTIONS.local_cert_dirs = ("vendor", "build")
72
73PROBLEMS = []
74PROBLEM_PREFIX = []
75
76def AddProblem(msg):
77  PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
78def Push(msg):
79  PROBLEM_PREFIX.append(msg)
80def Pop():
81  PROBLEM_PREFIX.pop()
82
83
84def Banner(msg):
85  print "-" * 70
86  print "  ", msg
87  print "-" * 70
88
89
90def GetCertSubject(cert):
91  p = common.Run(["openssl", "x509", "-inform", "DER", "-text"],
92                 stdin=subprocess.PIPE,
93                 stdout=subprocess.PIPE)
94  out, err = p.communicate(cert)
95  if err and not err.strip():
96    return "(error reading cert subject)"
97  for line in out.split("\n"):
98    line = line.strip()
99    if line.startswith("Subject:"):
100      return line[8:].strip()
101  return "(unknown cert subject)"
102
103
104class CertDB(object):
105  def __init__(self):
106    self.certs = {}
107
108  def Add(self, cert, name=None):
109    if cert in self.certs:
110      if name:
111        self.certs[cert] = self.certs[cert] + "," + name
112    else:
113      if name is None:
114        name = "unknown cert %s (%s)" % (common.sha1(cert).hexdigest()[:12],
115                                         GetCertSubject(cert))
116      self.certs[cert] = name
117
118  def Get(self, cert):
119    """Return the name for a given cert."""
120    return self.certs.get(cert, None)
121
122  def FindLocalCerts(self):
123    to_load = []
124    for top in OPTIONS.local_cert_dirs:
125      for dirpath, dirnames, filenames in os.walk(top):
126        certs = [os.path.join(dirpath, i)
127                 for i in filenames if i.endswith(".x509.pem")]
128        if certs:
129          to_load.extend(certs)
130
131    for i in to_load:
132      f = open(i)
133      cert = ParseCertificate(f.read())
134      f.close()
135      name, _ = os.path.splitext(i)
136      name, _ = os.path.splitext(name)
137      self.Add(cert, name)
138
139ALL_CERTS = CertDB()
140
141
142def ParseCertificate(data):
143  """Parse a PEM-format certificate."""
144  cert = []
145  save = False
146  for line in data.split("\n"):
147    if "--END CERTIFICATE--" in line:
148      break
149    if save:
150      cert.append(line)
151    if "--BEGIN CERTIFICATE--" in line:
152      save = True
153  cert = "".join(cert).decode('base64')
154  return cert
155
156
157def CertFromPKCS7(data, filename):
158  """Read the cert out of a PKCS#7-format file (which is what is
159  stored in a signed .apk)."""
160  Push(filename + ":")
161  try:
162    p = common.Run(["openssl", "pkcs7",
163                    "-inform", "DER",
164                    "-outform", "PEM",
165                    "-print_certs"],
166                   stdin=subprocess.PIPE,
167                   stdout=subprocess.PIPE)
168    out, err = p.communicate(data)
169    if err and not err.strip():
170      AddProblem("error reading cert:\n" + err)
171      return None
172
173    cert = ParseCertificate(out)
174    if not cert:
175      AddProblem("error parsing cert output")
176      return None
177    return cert
178  finally:
179    Pop()
180
181
182class APK(object):
183  def __init__(self, full_filename, filename):
184    self.filename = filename
185    self.cert = None
186    Push(filename+":")
187    try:
188      self.RecordCert(full_filename)
189      self.ReadManifest(full_filename)
190    finally:
191      Pop()
192
193  def RecordCert(self, full_filename):
194    try:
195      f = open(full_filename)
196      apk = zipfile.ZipFile(f, "r")
197      pkcs7 = None
198      for info in apk.infolist():
199        if info.filename.startswith("META-INF/") and \
200           (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")):
201          if pkcs7 is not None:
202            AddProblem("multiple certs")
203          pkcs7 = apk.read(info.filename)
204          self.cert = CertFromPKCS7(pkcs7, info.filename)
205          ALL_CERTS.Add(self.cert)
206      if not pkcs7:
207        AddProblem("no signature")
208    finally:
209      f.close()
210
211  def ReadManifest(self, full_filename):
212    p = common.Run(["aapt", "dump", "xmltree", full_filename,
213                    "AndroidManifest.xml"],
214                   stdout=subprocess.PIPE)
215    manifest, err = p.communicate()
216    if err:
217      AddProblem("failed to read manifest")
218      return
219
220    self.shared_uid = None
221    self.package = None
222
223    for line in manifest.split("\n"):
224      line = line.strip()
225      m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
226      if m:
227        name = m.group(1)
228        if name == "android:sharedUserId":
229          if self.shared_uid is not None:
230            AddProblem("multiple sharedUserId declarations")
231          self.shared_uid = m.group(2)
232        elif name == "package":
233          if self.package is not None:
234            AddProblem("multiple package declarations")
235          self.package = m.group(2)
236
237    if self.package is None:
238      AddProblem("no package declaration")
239
240
241class TargetFiles(object):
242  def __init__(self):
243    self.max_pkg_len = 30
244    self.max_fn_len = 20
245
246  def LoadZipFile(self, filename):
247    d, z = common.UnzipTemp(filename, '*.apk')
248    try:
249      self.apks = {}
250      self.apks_by_basename = {}
251      for dirpath, dirnames, filenames in os.walk(d):
252        for fn in filenames:
253          if fn.endswith(".apk"):
254            fullname = os.path.join(dirpath, fn)
255            displayname = fullname[len(d)+1:]
256            apk = APK(fullname, displayname)
257            self.apks[apk.package] = apk
258            self.apks_by_basename[os.path.basename(apk.filename)] = apk
259
260            self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
261            self.max_fn_len = max(self.max_fn_len, len(apk.filename))
262    finally:
263      shutil.rmtree(d)
264
265    self.certmap = common.ReadApkCerts(z)
266    z.close()
267
268  def CheckSharedUids(self):
269    """Look for any instances where packages signed with different
270    certs request the same sharedUserId."""
271    apks_by_uid = {}
272    for apk in self.apks.itervalues():
273      if apk.shared_uid:
274        apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
275
276    for uid in sorted(apks_by_uid.keys()):
277      apks = apks_by_uid[uid]
278      for apk in apks[1:]:
279        if apk.cert != apks[0].cert:
280          break
281      else:
282        # all the certs are the same; this uid is fine
283        continue
284
285      AddProblem("uid %s shared across multiple certs" % (uid,))
286
287      print "uid %s is shared by packages with different certs:" % (uid,)
288      x = [(i.cert, i.package, i) for i in apks]
289      x.sort()
290      lastcert = None
291      for cert, _, apk in x:
292        if cert != lastcert:
293          lastcert = cert
294          print "    %s:" % (ALL_CERTS.Get(cert),)
295        print "        %-*s  [%s]" % (self.max_pkg_len,
296                                      apk.package, apk.filename)
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      by_cert.setdefault(apk.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 the same cert
351          if self.apks[i].cert != other.apks[i].cert:
352            by_certpair.setdefault((other.apks[i].cert,
353                                    self.apks[i].cert), []).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        print "was", ALL_CERTS.Get(old)
367        print "now", ALL_CERTS.Get(new)
368        for i in sorted(packages):
369          old_fn = other.apks[i].filename
370          new_fn = self.apks[i].filename
371          if old_fn == new_fn:
372            print "  %-*s  [%s]" % (max_pkg_len, i, old_fn)
373          else:
374            print "  %-*s  [was: %s; now: %s]" % (max_pkg_len, i,
375                                                  old_fn, new_fn)
376        print
377
378
379def main(argv):
380  def option_handler(o, a):
381    if o in ("-c", "--compare_with"):
382      OPTIONS.compare_with = a
383    elif o in ("-l", "--local_cert_dirs"):
384      OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
385    elif o in ("-t", "--text"):
386      OPTIONS.text = True
387    else:
388      return False
389    return True
390
391  args = common.ParseOptions(argv, __doc__,
392                             extra_opts="c:l:t",
393                             extra_long_opts=["compare_with=",
394                                              "local_cert_dirs="],
395                             extra_option_handler=option_handler)
396
397  if len(args) != 1:
398    common.Usage(__doc__)
399    sys.exit(1)
400
401  ALL_CERTS.FindLocalCerts()
402
403  Push("input target_files:")
404  try:
405    target_files = TargetFiles()
406    target_files.LoadZipFile(args[0])
407  finally:
408    Pop()
409
410  compare_files = None
411  if OPTIONS.compare_with:
412    Push("comparison target_files:")
413    try:
414      compare_files = TargetFiles()
415      compare_files.LoadZipFile(OPTIONS.compare_with)
416    finally:
417      Pop()
418
419  if OPTIONS.text or not compare_files:
420    Banner("target files")
421    target_files.PrintCerts()
422  target_files.CheckSharedUids()
423  target_files.CheckExternalSignatures()
424  if compare_files:
425    if OPTIONS.text:
426      Banner("comparison files")
427      compare_files.PrintCerts()
428    target_files.CompareWith(compare_files)
429
430  if PROBLEMS:
431    print "%d problem(s) found:\n" % (len(PROBLEMS),)
432    for p in PROBLEMS:
433      print p
434    return 1
435
436  return 0
437
438
439if __name__ == '__main__':
440  try:
441    r = main(sys.argv[1:])
442    sys.exit(r)
443  except common.ExternalError, e:
444    print
445    print "   ERROR: %s" % (e,)
446    print
447    sys.exit(1)
448