blob: 7cb3e8a2d24f3b464069a47d222caab428a03217 [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
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700190 self.certs = set()
Doug Zongker75f17362009-12-08 13:46:44 -0800191 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")):
Doug Zongker75f17362009-12-08 13:46:44 -0800206 pkcs7 = apk.read(info.filename)
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700207 cert = CertFromPKCS7(pkcs7, info.filename)
208 self.certs.add(cert)
209 ALL_CERTS.Add(cert)
Doug Zongker75f17362009-12-08 13:46:44 -0800210 if not pkcs7:
211 AddProblem("no signature")
212 finally:
213 f.close()
214
215 def ReadManifest(self, full_filename):
216 p = common.Run(["aapt", "dump", "xmltree", full_filename,
217 "AndroidManifest.xml"],
218 stdout=subprocess.PIPE)
219 manifest, err = p.communicate()
220 if err:
221 AddProblem("failed to read manifest")
222 return
223
224 self.shared_uid = None
225 self.package = None
226
227 for line in manifest.split("\n"):
228 line = line.strip()
229 m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
230 if m:
231 name = m.group(1)
232 if name == "android:sharedUserId":
233 if self.shared_uid is not None:
234 AddProblem("multiple sharedUserId declarations")
235 self.shared_uid = m.group(2)
236 elif name == "package":
237 if self.package is not None:
238 AddProblem("multiple package declarations")
239 self.package = m.group(2)
240
241 if self.package is None:
242 AddProblem("no package declaration")
243
244
245class TargetFiles(object):
246 def __init__(self):
247 self.max_pkg_len = 30
248 self.max_fn_len = 20
249
250 def LoadZipFile(self, filename):
Doug Zongker6ae53812011-01-27 10:20:27 -0800251 d, z = common.UnzipTemp(filename, '*.apk')
Doug Zongker75f17362009-12-08 13:46:44 -0800252 try:
253 self.apks = {}
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800254 self.apks_by_basename = {}
Doug Zongker75f17362009-12-08 13:46:44 -0800255 for dirpath, dirnames, filenames in os.walk(d):
256 for fn in filenames:
257 if fn.endswith(".apk"):
258 fullname = os.path.join(dirpath, fn)
259 displayname = fullname[len(d)+1:]
260 apk = APK(fullname, displayname)
261 self.apks[apk.package] = apk
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800262 self.apks_by_basename[os.path.basename(apk.filename)] = apk
Doug Zongker75f17362009-12-08 13:46:44 -0800263
264 self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
265 self.max_fn_len = max(self.max_fn_len, len(apk.filename))
266 finally:
267 shutil.rmtree(d)
268
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800269 self.certmap = common.ReadApkCerts(z)
270 z.close()
271
Doug Zongker75f17362009-12-08 13:46:44 -0800272 def CheckSharedUids(self):
273 """Look for any instances where packages signed with different
274 certs request the same sharedUserId."""
275 apks_by_uid = {}
276 for apk in self.apks.itervalues():
277 if apk.shared_uid:
278 apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
279
280 for uid in sorted(apks_by_uid.keys()):
281 apks = apks_by_uid[uid]
282 for apk in apks[1:]:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700283 if apk.certs != apks[0].certs:
Doug Zongker75f17362009-12-08 13:46:44 -0800284 break
285 else:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700286 # all packages have the same set of certs; this uid is fine.
Doug Zongker75f17362009-12-08 13:46:44 -0800287 continue
288
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700289 AddProblem("different cert sets for packages with uid %s" % (uid,))
Doug Zongker75f17362009-12-08 13:46:44 -0800290
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700291 print "uid %s is shared by packages with different cert sets:" % (uid,)
292 for apk in apks:
293 print "%-*s [%s]" % (self.max_pkg_len, apk.package, apk.filename)
294 for cert in apk.certs:
295 print " ", ALL_CERTS.Get(cert)
Doug Zongker75f17362009-12-08 13:46:44 -0800296 print
297
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800298 def CheckExternalSignatures(self):
299 for apk_filename, certname in self.certmap.iteritems():
300 if certname == "EXTERNAL":
301 # Apps marked EXTERNAL should be signed with the test key
302 # during development, then manually re-signed after
303 # predexopting. Consider it an error if this app is now
304 # signed with any key that is present in our tree.
305 apk = self.apks_by_basename[apk_filename]
306 name = ALL_CERTS.Get(apk.cert)
307 if not name.startswith("unknown "):
308 Push(apk.filename)
309 AddProblem("hasn't been signed with EXTERNAL cert")
310 Pop()
311
Doug Zongker75f17362009-12-08 13:46:44 -0800312 def PrintCerts(self):
313 """Display a table of packages grouped by cert."""
314 by_cert = {}
315 for apk in self.apks.itervalues():
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700316 for cert in apk.certs:
317 by_cert.setdefault(cert, []).append((apk.package, apk))
Doug Zongker75f17362009-12-08 13:46:44 -0800318
319 order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
320 order.sort()
321
322 for _, cert in order:
323 print "%s:" % (ALL_CERTS.Get(cert),)
324 apks = by_cert[cert]
325 apks.sort()
326 for _, apk in apks:
327 if apk.shared_uid:
328 print " %-*s %-*s [%s]" % (self.max_fn_len, apk.filename,
329 self.max_pkg_len, apk.package,
330 apk.shared_uid)
331 else:
332 print " %-*s %-*s" % (self.max_fn_len, apk.filename,
333 self.max_pkg_len, apk.package)
334 print
335
336 def CompareWith(self, other):
337 """Look for instances where a given package that exists in both
338 self and other have different certs."""
339
340 all = set(self.apks.keys())
341 all.update(other.apks.keys())
342
343 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
344
345 by_certpair = {}
346
347 for i in all:
348 if i in self.apks:
349 if i in other.apks:
Doug Zongker278c9782011-11-09 10:32:23 -0800350 # in both; should have same set of certs
351 if self.apks[i].certs != other.apks[i].certs:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700352 by_certpair.setdefault((other.apks[i].certs,
353 self.apks[i].certs), []).append(i)
Doug Zongker75f17362009-12-08 13:46:44 -0800354 else:
355 print "%s [%s]: new APK (not in comparison target_files)" % (
356 i, self.apks[i].filename)
357 else:
358 if i in other.apks:
359 print "%s [%s]: removed APK (only in comparison target_files)" % (
360 i, other.apks[i].filename)
361
362 if by_certpair:
363 AddProblem("some APKs changed certs")
364 Banner("APK signing differences")
365 for (old, new), packages in sorted(by_certpair.items()):
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700366 for i, o in enumerate(old):
367 if i == 0:
368 print "was", ALL_CERTS.Get(o)
369 else:
370 print " ", ALL_CERTS.Get(o)
371 for i, n in enumerate(new):
372 if i == 0:
373 print "now", ALL_CERTS.Get(n)
374 else:
375 print " ", ALL_CERTS.Get(n)
Doug Zongker75f17362009-12-08 13:46:44 -0800376 for i in sorted(packages):
377 old_fn = other.apks[i].filename
378 new_fn = self.apks[i].filename
379 if old_fn == new_fn:
380 print " %-*s [%s]" % (max_pkg_len, i, old_fn)
381 else:
382 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i,
383 old_fn, new_fn)
384 print
385
386
387def main(argv):
388 def option_handler(o, a):
389 if o in ("-c", "--compare_with"):
390 OPTIONS.compare_with = a
391 elif o in ("-l", "--local_cert_dirs"):
392 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
393 elif o in ("-t", "--text"):
394 OPTIONS.text = True
395 else:
396 return False
397 return True
398
399 args = common.ParseOptions(argv, __doc__,
400 extra_opts="c:l:t",
401 extra_long_opts=["compare_with=",
402 "local_cert_dirs="],
403 extra_option_handler=option_handler)
404
405 if len(args) != 1:
406 common.Usage(__doc__)
407 sys.exit(1)
408
409 ALL_CERTS.FindLocalCerts()
410
411 Push("input target_files:")
412 try:
413 target_files = TargetFiles()
414 target_files.LoadZipFile(args[0])
415 finally:
416 Pop()
417
418 compare_files = None
419 if OPTIONS.compare_with:
420 Push("comparison target_files:")
421 try:
422 compare_files = TargetFiles()
423 compare_files.LoadZipFile(OPTIONS.compare_with)
424 finally:
425 Pop()
426
427 if OPTIONS.text or not compare_files:
428 Banner("target files")
429 target_files.PrintCerts()
430 target_files.CheckSharedUids()
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800431 target_files.CheckExternalSignatures()
Doug Zongker75f17362009-12-08 13:46:44 -0800432 if compare_files:
433 if OPTIONS.text:
434 Banner("comparison files")
435 compare_files.PrintCerts()
436 target_files.CompareWith(compare_files)
437
438 if PROBLEMS:
439 print "%d problem(s) found:\n" % (len(PROBLEMS),)
440 for p in PROBLEMS:
441 print p
442 return 1
443
444 return 0
445
446
447if __name__ == '__main__':
448 try:
449 r = main(sys.argv[1:])
450 sys.exit(r)
451 except common.ExternalError, e:
452 print
453 print " ERROR: %s" % (e,)
454 print
455 sys.exit(1)