blob: 45d30a651c3764577100ee7e231395b398ef52f7 [file] [log] [blame]
Doug Zongker75f17362009-12-08 13:46:44 -08001#!/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
Doug Zongker75f17362009-12-08 13:46:44 -080050import shutil
51import subprocess
52import tempfile
53import zipfile
54
davidcad0bb92011-03-15 14:21:38 +000055try:
56 from hashlib import sha1 as sha1
57except ImportError:
58 from sha import sha as sha1
59
Doug Zongker75f17362009-12-08 13:46:44 -080060import 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:
Doug Zongker6ae53812011-01-27 10:20:27 -0800119 name = "unknown cert %s (%s)" % (common.sha1(cert).hexdigest()[:12],
Doug Zongker75f17362009-12-08 13:46:44 -0800120 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)
Baligh Uddinbeb6afd2013-11-13 00:22:34 +0000138 cert = common.ParseCertificate(f.read())
Doug Zongker75f17362009-12-08 13:46:44 -0800139 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
Doug Zongker75f17362009-12-08 13:46:44 -0800147def 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
Baligh Uddinbeb6afd2013-11-13 00:22:34 +0000163 cert = common.ParseCertificate(out)
Doug Zongker75f17362009-12-08 13:46:44 -0800164 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
Doug Zongker75f17362009-12-08 13:46:44 -0800175 Push(filename+":")
176 try:
Doug Zongkera5f534d2011-11-11 09:51:37 -0800177 self.RecordCerts(full_filename)
Doug Zongker75f17362009-12-08 13:46:44 -0800178 self.ReadManifest(full_filename)
179 finally:
180 Pop()
181
Doug Zongkera5f534d2011-11-11 09:51:37 -0800182 def RecordCerts(self, full_filename):
183 out = set()
Doug Zongker75f17362009-12-08 13:46:44 -0800184 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")):
Doug Zongker75f17362009-12-08 13:46:44 -0800191 pkcs7 = apk.read(info.filename)
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700192 cert = CertFromPKCS7(pkcs7, info.filename)
Doug Zongkera5f534d2011-11-11 09:51:37 -0800193 out.add(cert)
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700194 ALL_CERTS.Add(cert)
Doug Zongker75f17362009-12-08 13:46:44 -0800195 if not pkcs7:
196 AddProblem("no signature")
197 finally:
198 f.close()
Doug Zongkera5f534d2011-11-11 09:51:37 -0800199 self.certs = frozenset(out)
Doug Zongker75f17362009-12-08 13:46:44 -0800200
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):
Doug Zongker6ae53812011-01-27 10:20:27 -0800237 d, z = common.UnzipTemp(filename, '*.apk')
Doug Zongker75f17362009-12-08 13:46:44 -0800238 try:
239 self.apks = {}
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800240 self.apks_by_basename = {}
Doug Zongker75f17362009-12-08 13:46:44 -0800241 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
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800248 self.apks_by_basename[os.path.basename(apk.filename)] = apk
Doug Zongker75f17362009-12-08 13:46:44 -0800249
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
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800255 self.certmap = common.ReadApkCerts(z)
256 z.close()
257
Doug Zongker75f17362009-12-08 13:46:44 -0800258 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:]:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700269 if apk.certs != apks[0].certs:
Doug Zongker75f17362009-12-08 13:46:44 -0800270 break
271 else:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700272 # all packages have the same set of certs; this uid is fine.
Doug Zongker75f17362009-12-08 13:46:44 -0800273 continue
274
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700275 AddProblem("different cert sets for packages with uid %s" % (uid,))
Doug Zongker75f17362009-12-08 13:46:44 -0800276
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700277 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)
Doug Zongker75f17362009-12-08 13:46:44 -0800282 print
283
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800284 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
Doug Zongker75f17362009-12-08 13:46:44 -0800298 def PrintCerts(self):
299 """Display a table of packages grouped by cert."""
300 by_cert = {}
301 for apk in self.apks.itervalues():
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700302 for cert in apk.certs:
303 by_cert.setdefault(cert, []).append((apk.package, apk))
Doug Zongker75f17362009-12-08 13:46:44 -0800304
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:
Doug Zongker278c9782011-11-09 10:32:23 -0800336 # in both; should have same set of certs
337 if self.apks[i].certs != other.apks[i].certs:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700338 by_certpair.setdefault((other.apks[i].certs,
339 self.apks[i].certs), []).append(i)
Doug Zongker75f17362009-12-08 13:46:44 -0800340 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()):
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700352 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)
Doug Zongker75f17362009-12-08 13:46:44 -0800362 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()
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800417 target_files.CheckExternalSignatures()
Doug Zongker75f17362009-12-08 13:46:44 -0800418 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)