check_target_files_signatures revision 817c574d753191c52acd5787da02bb853d4ac090
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 = common.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 CertFromPKCS7(data, filename):
148  """Read the cert out of a PKCS#7-format file (which is what is
149  stored in a signed .apk)."""
150  Push(filename + ":")
151  try:
152    p = common.Run(["openssl", "pkcs7",
153                    "-inform", "DER",
154                    "-outform", "PEM",
155                    "-print_certs"],
156                   stdin=subprocess.PIPE,
157                   stdout=subprocess.PIPE)
158    out, err = p.communicate(data)
159    if err and not err.strip():
160      AddProblem("error reading cert:\n" + err)
161      return None
162
163    cert = common.ParseCertificate(out)
164    if not cert:
165      AddProblem("error parsing cert output")
166      return None
167    return cert
168  finally:
169    Pop()
170
171
172class APK(object):
173  def __init__(self, full_filename, filename):
174    self.filename = filename
175    Push(filename+":")
176    try:
177      self.RecordCerts(full_filename)
178      self.ReadManifest(full_filename)
179    finally:
180      Pop()
181
182  def RecordCerts(self, full_filename):
183    out = set()
184    try:
185      f = open(full_filename)
186      apk = zipfile.ZipFile(f, "r")
187      pkcs7 = None
188      for info in apk.infolist():
189        if info.filename.startswith("META-INF/") and \
190           (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")):
191          pkcs7 = apk.read(info.filename)
192          cert = CertFromPKCS7(pkcs7, info.filename)
193          out.add(cert)
194          ALL_CERTS.Add(cert)
195      if not pkcs7:
196        AddProblem("no signature")
197    finally:
198      f.close()
199      self.certs = frozenset(out)
200
201  def ReadManifest(self, full_filename):
202    p = common.Run(["aapt", "dump", "xmltree", full_filename,
203                    "AndroidManifest.xml"],
204                   stdout=subprocess.PIPE)
205    manifest, err = p.communicate()
206    if err:
207      AddProblem("failed to read manifest")
208      return
209
210    self.shared_uid = None
211    self.package = None
212
213    for line in manifest.split("\n"):
214      line = line.strip()
215      m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
216      if m:
217        name = m.group(1)
218        if name == "android:sharedUserId":
219          if self.shared_uid is not None:
220            AddProblem("multiple sharedUserId declarations")
221          self.shared_uid = m.group(2)
222        elif name == "package":
223          if self.package is not None:
224            AddProblem("multiple package declarations")
225          self.package = m.group(2)
226
227    if self.package is None:
228      AddProblem("no package declaration")
229
230
231class TargetFiles(object):
232  def __init__(self):
233    self.max_pkg_len = 30
234    self.max_fn_len = 20
235
236  def LoadZipFile(self, filename):
237    d, z = common.UnzipTemp(filename, '*.apk')
238    try:
239      self.apks = {}
240      self.apks_by_basename = {}
241      for dirpath, dirnames, filenames in os.walk(d):
242        for fn in filenames:
243          if fn.endswith(".apk"):
244            fullname = os.path.join(dirpath, fn)
245            displayname = fullname[len(d)+1:]
246            apk = APK(fullname, displayname)
247            self.apks[apk.package] = apk
248            self.apks_by_basename[os.path.basename(apk.filename)] = apk
249
250            self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
251            self.max_fn_len = max(self.max_fn_len, len(apk.filename))
252    finally:
253      shutil.rmtree(d)
254
255    self.certmap = common.ReadApkCerts(z)
256    z.close()
257
258  def CheckSharedUids(self):
259    """Look for any instances where packages signed with different
260    certs request the same sharedUserId."""
261    apks_by_uid = {}
262    for apk in self.apks.itervalues():
263      if apk.shared_uid:
264        apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
265
266    for uid in sorted(apks_by_uid.keys()):
267      apks = apks_by_uid[uid]
268      for apk in apks[1:]:
269        if apk.certs != apks[0].certs:
270          break
271      else:
272        # all packages have the same set of certs; this uid is fine.
273        continue
274
275      AddProblem("different cert sets for packages with uid %s" % (uid,))
276
277      print "uid %s is shared by packages with different cert sets:" % (uid,)
278      for apk in apks:
279        print "%-*s  [%s]" % (self.max_pkg_len, apk.package, apk.filename)
280        for cert in apk.certs:
281          print "   ", ALL_CERTS.Get(cert)
282      print
283
284  def CheckExternalSignatures(self):
285    for apk_filename, certname in self.certmap.iteritems():
286      if certname == "EXTERNAL":
287        # Apps marked EXTERNAL should be signed with the test key
288        # during development, then manually re-signed after
289        # predexopting.  Consider it an error if this app is now
290        # signed with any key that is present in our tree.
291        apk = self.apks_by_basename[apk_filename]
292        name = ALL_CERTS.Get(apk.cert)
293        if not name.startswith("unknown "):
294          Push(apk.filename)
295          AddProblem("hasn't been signed with EXTERNAL cert")
296          Pop()
297
298  def PrintCerts(self):
299    """Display a table of packages grouped by cert."""
300    by_cert = {}
301    for apk in self.apks.itervalues():
302      for cert in apk.certs:
303        by_cert.setdefault(cert, []).append((apk.package, apk))
304
305    order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
306    order.sort()
307
308    for _, cert in order:
309      print "%s:" % (ALL_CERTS.Get(cert),)
310      apks = by_cert[cert]
311      apks.sort()
312      for _, apk in apks:
313        if apk.shared_uid:
314          print "  %-*s  %-*s  [%s]" % (self.max_fn_len, apk.filename,
315                                        self.max_pkg_len, apk.package,
316                                        apk.shared_uid)
317        else:
318          print "  %-*s  %-*s" % (self.max_fn_len, apk.filename,
319                                  self.max_pkg_len, 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 = set(self.apks.keys())
327    all.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:
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, e:
438    print
439    print "   ERROR: %s" % (e,)
440    print
441    sys.exit(1)
442