check_target_files_signatures revision 27bb6f5f234332d161a050b32717d94b38e63823
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      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
259            self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
260            self.max_fn_len = max(self.max_fn_len, len(apk.filename))
261    finally:
262      shutil.rmtree(d)
263
264  def CheckSharedUids(self):
265    """Look for any instances where packages signed with different
266    certs request the same sharedUserId."""
267    apks_by_uid = {}
268    for apk in self.apks.itervalues():
269      if apk.shared_uid:
270        apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
271
272    for uid in sorted(apks_by_uid.keys()):
273      apks = apks_by_uid[uid]
274      for apk in apks[1:]:
275        if apk.cert != apks[0].cert:
276          break
277      else:
278        # all the certs are the same; this uid is fine
279        continue
280
281      AddProblem("uid %s shared across multiple certs" % (uid,))
282
283      print "uid %s is shared by packages with different certs:" % (uid,)
284      x = [(i.cert, i.package, i) for i in apks]
285      x.sort()
286      lastcert = None
287      for cert, _, apk in x:
288        if cert != lastcert:
289          lastcert = cert
290          print "    %s:" % (ALL_CERTS.Get(cert),)
291        print "        %-*s  [%s]" % (self.max_pkg_len,
292                                      apk.package, apk.filename)
293      print
294
295  def PrintCerts(self):
296    """Display a table of packages grouped by cert."""
297    by_cert = {}
298    for apk in self.apks.itervalues():
299      by_cert.setdefault(apk.cert, []).append((apk.package, apk))
300
301    order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
302    order.sort()
303
304    for _, cert in order:
305      print "%s:" % (ALL_CERTS.Get(cert),)
306      apks = by_cert[cert]
307      apks.sort()
308      for _, apk in apks:
309        if apk.shared_uid:
310          print "  %-*s  %-*s  [%s]" % (self.max_fn_len, apk.filename,
311                                        self.max_pkg_len, apk.package,
312                                        apk.shared_uid)
313        else:
314          print "  %-*s  %-*s" % (self.max_fn_len, apk.filename,
315                                  self.max_pkg_len, apk.package)
316      print
317
318  def CompareWith(self, other):
319    """Look for instances where a given package that exists in both
320    self and other have different certs."""
321
322    all = set(self.apks.keys())
323    all.update(other.apks.keys())
324
325    max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
326
327    by_certpair = {}
328
329    for i in all:
330      if i in self.apks:
331        if i in other.apks:
332          # in both; should have the same cert
333          if self.apks[i].cert != other.apks[i].cert:
334            by_certpair.setdefault((other.apks[i].cert,
335                                    self.apks[i].cert), []).append(i)
336        else:
337          print "%s [%s]: new APK (not in comparison target_files)" % (
338              i, self.apks[i].filename)
339      else:
340        if i in other.apks:
341          print "%s [%s]: removed APK (only in comparison target_files)" % (
342              i, other.apks[i].filename)
343
344    if by_certpair:
345      AddProblem("some APKs changed certs")
346      Banner("APK signing differences")
347      for (old, new), packages in sorted(by_certpair.items()):
348        print "was", ALL_CERTS.Get(old)
349        print "now", ALL_CERTS.Get(new)
350        for i in sorted(packages):
351          old_fn = other.apks[i].filename
352          new_fn = self.apks[i].filename
353          if old_fn == new_fn:
354            print "  %-*s  [%s]" % (max_pkg_len, i, old_fn)
355          else:
356            print "  %-*s  [was: %s; now: %s]" % (max_pkg_len, i,
357                                                  old_fn, new_fn)
358        print
359
360
361def main(argv):
362  def option_handler(o, a):
363    if o in ("-c", "--compare_with"):
364      OPTIONS.compare_with = a
365    elif o in ("-l", "--local_cert_dirs"):
366      OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
367    elif o in ("-t", "--text"):
368      OPTIONS.text = True
369    else:
370      return False
371    return True
372
373  args = common.ParseOptions(argv, __doc__,
374                             extra_opts="c:l:t",
375                             extra_long_opts=["compare_with=",
376                                              "local_cert_dirs="],
377                             extra_option_handler=option_handler)
378
379  if len(args) != 1:
380    common.Usage(__doc__)
381    sys.exit(1)
382
383  ALL_CERTS.FindLocalCerts()
384
385  Push("input target_files:")
386  try:
387    target_files = TargetFiles()
388    target_files.LoadZipFile(args[0])
389  finally:
390    Pop()
391
392  compare_files = None
393  if OPTIONS.compare_with:
394    Push("comparison target_files:")
395    try:
396      compare_files = TargetFiles()
397      compare_files.LoadZipFile(OPTIONS.compare_with)
398    finally:
399      Pop()
400
401  if OPTIONS.text or not compare_files:
402    Banner("target files")
403    target_files.PrintCerts()
404  target_files.CheckSharedUids()
405  if compare_files:
406    if OPTIONS.text:
407      Banner("comparison files")
408      compare_files.PrintCerts()
409    target_files.CompareWith(compare_files)
410
411  if PROBLEMS:
412    print "%d problem(s) found:\n" % (len(PROBLEMS),)
413    for p in PROBLEMS:
414      print p
415    return 1
416
417  return 0
418
419
420if __name__ == '__main__':
421  try:
422    r = main(sys.argv[1:])
423    sys.exit(r)
424  except common.ExternalError, e:
425    print
426    print "   ERROR: %s" % (e,)
427    print
428    sys.exit(1)
429