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