blob: 1325ef477c7f4d63c979dc80d2f10760098739eb [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)
138 cert = 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 ParseCertificate(data):
148 """Parse a PEM-format certificate."""
149 cert = []
150 save = False
151 for line in data.split("\n"):
152 if "--END CERTIFICATE--" in line:
153 break
154 if save:
155 cert.append(line)
156 if "--BEGIN CERTIFICATE--" in line:
157 save = True
158 cert = "".join(cert).decode('base64')
159 return cert
160
161
162def CertFromPKCS7(data, filename):
163 """Read the cert out of a PKCS#7-format file (which is what is
164 stored in a signed .apk)."""
165 Push(filename + ":")
166 try:
167 p = common.Run(["openssl", "pkcs7",
168 "-inform", "DER",
169 "-outform", "PEM",
170 "-print_certs"],
171 stdin=subprocess.PIPE,
172 stdout=subprocess.PIPE)
173 out, err = p.communicate(data)
174 if err and not err.strip():
175 AddProblem("error reading cert:\n" + err)
176 return None
177
178 cert = ParseCertificate(out)
179 if not cert:
180 AddProblem("error parsing cert output")
181 return None
182 return cert
183 finally:
184 Pop()
185
186
187class APK(object):
188 def __init__(self, full_filename, filename):
189 self.filename = filename
190 self.cert = None
191 Push(filename+":")
192 try:
193 self.RecordCert(full_filename)
194 self.ReadManifest(full_filename)
195 finally:
196 Pop()
197
198 def RecordCert(self, full_filename):
199 try:
200 f = open(full_filename)
201 apk = zipfile.ZipFile(f, "r")
202 pkcs7 = None
203 for info in apk.infolist():
204 if info.filename.startswith("META-INF/") and \
205 (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")):
206 if pkcs7 is not None:
207 AddProblem("multiple certs")
208 pkcs7 = apk.read(info.filename)
209 self.cert = CertFromPKCS7(pkcs7, info.filename)
210 ALL_CERTS.Add(self.cert)
211 if not pkcs7:
212 AddProblem("no signature")
213 finally:
214 f.close()
215
216 def ReadManifest(self, full_filename):
217 p = common.Run(["aapt", "dump", "xmltree", full_filename,
218 "AndroidManifest.xml"],
219 stdout=subprocess.PIPE)
220 manifest, err = p.communicate()
221 if err:
222 AddProblem("failed to read manifest")
223 return
224
225 self.shared_uid = None
226 self.package = None
227
228 for line in manifest.split("\n"):
229 line = line.strip()
230 m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
231 if m:
232 name = m.group(1)
233 if name == "android:sharedUserId":
234 if self.shared_uid is not None:
235 AddProblem("multiple sharedUserId declarations")
236 self.shared_uid = m.group(2)
237 elif name == "package":
238 if self.package is not None:
239 AddProblem("multiple package declarations")
240 self.package = m.group(2)
241
242 if self.package is None:
243 AddProblem("no package declaration")
244
245
246class TargetFiles(object):
247 def __init__(self):
248 self.max_pkg_len = 30
249 self.max_fn_len = 20
250
251 def LoadZipFile(self, filename):
Doug Zongker6ae53812011-01-27 10:20:27 -0800252 d, z = common.UnzipTemp(filename, '*.apk')
Doug Zongker75f17362009-12-08 13:46:44 -0800253 try:
254 self.apks = {}
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800255 self.apks_by_basename = {}
Doug Zongker75f17362009-12-08 13:46:44 -0800256 for dirpath, dirnames, filenames in os.walk(d):
257 for fn in filenames:
258 if fn.endswith(".apk"):
259 fullname = os.path.join(dirpath, fn)
260 displayname = fullname[len(d)+1:]
261 apk = APK(fullname, displayname)
262 self.apks[apk.package] = apk
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800263 self.apks_by_basename[os.path.basename(apk.filename)] = apk
Doug Zongker75f17362009-12-08 13:46:44 -0800264
265 self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
266 self.max_fn_len = max(self.max_fn_len, len(apk.filename))
267 finally:
268 shutil.rmtree(d)
269
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800270 self.certmap = common.ReadApkCerts(z)
271 z.close()
272
Doug Zongker75f17362009-12-08 13:46:44 -0800273 def CheckSharedUids(self):
274 """Look for any instances where packages signed with different
275 certs request the same sharedUserId."""
276 apks_by_uid = {}
277 for apk in self.apks.itervalues():
278 if apk.shared_uid:
279 apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
280
281 for uid in sorted(apks_by_uid.keys()):
282 apks = apks_by_uid[uid]
283 for apk in apks[1:]:
284 if apk.cert != apks[0].cert:
285 break
286 else:
287 # all the certs are the same; this uid is fine
288 continue
289
290 AddProblem("uid %s shared across multiple certs" % (uid,))
291
292 print "uid %s is shared by packages with different certs:" % (uid,)
293 x = [(i.cert, i.package, i) for i in apks]
294 x.sort()
295 lastcert = None
296 for cert, _, apk in x:
297 if cert != lastcert:
298 lastcert = cert
299 print " %s:" % (ALL_CERTS.Get(cert),)
300 print " %-*s [%s]" % (self.max_pkg_len,
301 apk.package, apk.filename)
302 print
303
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800304 def CheckExternalSignatures(self):
305 for apk_filename, certname in self.certmap.iteritems():
306 if certname == "EXTERNAL":
307 # Apps marked EXTERNAL should be signed with the test key
308 # during development, then manually re-signed after
309 # predexopting. Consider it an error if this app is now
310 # signed with any key that is present in our tree.
311 apk = self.apks_by_basename[apk_filename]
312 name = ALL_CERTS.Get(apk.cert)
313 if not name.startswith("unknown "):
314 Push(apk.filename)
315 AddProblem("hasn't been signed with EXTERNAL cert")
316 Pop()
317
Doug Zongker75f17362009-12-08 13:46:44 -0800318 def PrintCerts(self):
319 """Display a table of packages grouped by cert."""
320 by_cert = {}
321 for apk in self.apks.itervalues():
322 by_cert.setdefault(apk.cert, []).append((apk.package, apk))
323
324 order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
325 order.sort()
326
327 for _, cert in order:
328 print "%s:" % (ALL_CERTS.Get(cert),)
329 apks = by_cert[cert]
330 apks.sort()
331 for _, apk in apks:
332 if apk.shared_uid:
333 print " %-*s %-*s [%s]" % (self.max_fn_len, apk.filename,
334 self.max_pkg_len, apk.package,
335 apk.shared_uid)
336 else:
337 print " %-*s %-*s" % (self.max_fn_len, apk.filename,
338 self.max_pkg_len, apk.package)
339 print
340
341 def CompareWith(self, other):
342 """Look for instances where a given package that exists in both
343 self and other have different certs."""
344
345 all = set(self.apks.keys())
346 all.update(other.apks.keys())
347
348 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
349
350 by_certpair = {}
351
352 for i in all:
353 if i in self.apks:
354 if i in other.apks:
355 # in both; should have the same cert
356 if self.apks[i].cert != other.apks[i].cert:
357 by_certpair.setdefault((other.apks[i].cert,
358 self.apks[i].cert), []).append(i)
359 else:
360 print "%s [%s]: new APK (not in comparison target_files)" % (
361 i, self.apks[i].filename)
362 else:
363 if i in other.apks:
364 print "%s [%s]: removed APK (only in comparison target_files)" % (
365 i, other.apks[i].filename)
366
367 if by_certpair:
368 AddProblem("some APKs changed certs")
369 Banner("APK signing differences")
370 for (old, new), packages in sorted(by_certpair.items()):
371 print "was", ALL_CERTS.Get(old)
372 print "now", ALL_CERTS.Get(new)
373 for i in sorted(packages):
374 old_fn = other.apks[i].filename
375 new_fn = self.apks[i].filename
376 if old_fn == new_fn:
377 print " %-*s [%s]" % (max_pkg_len, i, old_fn)
378 else:
379 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i,
380 old_fn, new_fn)
381 print
382
383
384def main(argv):
385 def option_handler(o, a):
386 if o in ("-c", "--compare_with"):
387 OPTIONS.compare_with = a
388 elif o in ("-l", "--local_cert_dirs"):
389 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
390 elif o in ("-t", "--text"):
391 OPTIONS.text = True
392 else:
393 return False
394 return True
395
396 args = common.ParseOptions(argv, __doc__,
397 extra_opts="c:l:t",
398 extra_long_opts=["compare_with=",
399 "local_cert_dirs="],
400 extra_option_handler=option_handler)
401
402 if len(args) != 1:
403 common.Usage(__doc__)
404 sys.exit(1)
405
406 ALL_CERTS.FindLocalCerts()
407
408 Push("input target_files:")
409 try:
410 target_files = TargetFiles()
411 target_files.LoadZipFile(args[0])
412 finally:
413 Pop()
414
415 compare_files = None
416 if OPTIONS.compare_with:
417 Push("comparison target_files:")
418 try:
419 compare_files = TargetFiles()
420 compare_files.LoadZipFile(OPTIONS.compare_with)
421 finally:
422 Pop()
423
424 if OPTIONS.text or not compare_files:
425 Banner("target files")
426 target_files.PrintCerts()
427 target_files.CheckSharedUids()
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800428 target_files.CheckExternalSignatures()
Doug Zongker75f17362009-12-08 13:46:44 -0800429 if compare_files:
430 if OPTIONS.text:
431 Banner("comparison files")
432 compare_files.PrintCerts()
433 target_files.CompareWith(compare_files)
434
435 if PROBLEMS:
436 print "%d problem(s) found:\n" % (len(PROBLEMS),)
437 for p in PROBLEMS:
438 print p
439 return 1
440
441 return 0
442
443
444if __name__ == '__main__':
445 try:
446 r = main(sys.argv[1:])
447 sys.exit(r)
448 except common.ExternalError, e:
449 print
450 print " ERROR: %s" % (e,)
451 print
452 sys.exit(1)