OSDN Git Service

Revert the revert. (its time to sign MR1)
[android-x86/build.git] / tools / releasetools / check_target_files_signatures
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 """
18 Check 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
20 target_files (usually a previously distributed build for the same
21 device) and flag any changes.
22
23 Usage:  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
42 import sys
43
44 if sys.hexversion < 0x02040000:
45   print >> sys.stderr, "Python 2.4 or newer is required."
46   sys.exit(1)
47
48 import os
49 import re
50 import shutil
51 import subprocess
52 import tempfile
53 import zipfile
54
55 try:
56   from hashlib import sha1 as sha1
57 except ImportError:
58   from sha import sha as sha1
59
60 import 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).
67 class MyZipInfo(zipfile.ZipInfo):
68   def _decodeExtra(self):
69     pass
70 zipfile.ZipInfo = MyZipInfo
71
72 OPTIONS = common.OPTIONS
73
74 OPTIONS.text = False
75 OPTIONS.compare_with = None
76 OPTIONS.local_cert_dirs = ("vendor", "build")
77
78 PROBLEMS = []
79 PROBLEM_PREFIX = []
80
81 def AddProblem(msg):
82   PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
83 def Push(msg):
84   PROBLEM_PREFIX.append(msg)
85 def Pop():
86   PROBLEM_PREFIX.pop()
87
88
89 def Banner(msg):
90   print "-" * 70
91   print "  ", msg
92   print "-" * 70
93
94
95 def 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
109 class 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
144 ALL_CERTS = CertDB()
145
146
147 def 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
172 class 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
231 class 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
373 def 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
433 if __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)