blob: 5c541abc6b73859b6d840b41e85f5db41bc66746 [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
Doug Zongkercf6d5a92014-02-18 10:57:07 -080044if sys.hexversion < 0x02070000:
45 print >> sys.stderr, "Python 2.7 or newer is required."
Doug Zongker75f17362009-12-08 13:46:44 -080046 sys.exit(1)
47
48import os
49import re
Doug Zongker75f17362009-12-08 13:46:44 -080050import shutil
51import subprocess
Doug Zongker75f17362009-12-08 13:46:44 -080052import zipfile
53
54import common
55
56# Work around a bug in python's zipfile module that prevents opening
57# of zipfiles if any entry has an extra field of between 1 and 3 bytes
58# (which is common with zipaligned APKs). This overrides the
59# ZipInfo._decodeExtra() method (which contains the bug) with an empty
60# version (since we don't need to decode the extra field anyway).
61class MyZipInfo(zipfile.ZipInfo):
62 def _decodeExtra(self):
63 pass
64zipfile.ZipInfo = MyZipInfo
65
66OPTIONS = common.OPTIONS
67
68OPTIONS.text = False
69OPTIONS.compare_with = None
70OPTIONS.local_cert_dirs = ("vendor", "build")
71
72PROBLEMS = []
73PROBLEM_PREFIX = []
74
75def AddProblem(msg):
76 PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
77def Push(msg):
78 PROBLEM_PREFIX.append(msg)
79def Pop():
80 PROBLEM_PREFIX.pop()
81
82
83def Banner(msg):
84 print "-" * 70
85 print " ", msg
86 print "-" * 70
87
88
89def GetCertSubject(cert):
90 p = common.Run(["openssl", "x509", "-inform", "DER", "-text"],
91 stdin=subprocess.PIPE,
92 stdout=subprocess.PIPE)
93 out, err = p.communicate(cert)
94 if err and not err.strip():
95 return "(error reading cert subject)"
96 for line in out.split("\n"):
97 line = line.strip()
98 if line.startswith("Subject:"):
99 return line[8:].strip()
100 return "(unknown cert subject)"
101
102
103class CertDB(object):
104 def __init__(self):
105 self.certs = {}
106
107 def Add(self, cert, name=None):
108 if cert in self.certs:
109 if name:
110 self.certs[cert] = self.certs[cert] + "," + name
111 else:
112 if name is None:
Doug Zongker6ae53812011-01-27 10:20:27 -0800113 name = "unknown cert %s (%s)" % (common.sha1(cert).hexdigest()[:12],
Doug Zongker75f17362009-12-08 13:46:44 -0800114 GetCertSubject(cert))
115 self.certs[cert] = name
116
117 def Get(self, cert):
118 """Return the name for a given cert."""
119 return self.certs.get(cert, None)
120
121 def FindLocalCerts(self):
122 to_load = []
123 for top in OPTIONS.local_cert_dirs:
Dan Albert8b72aef2015-03-23 19:13:21 -0700124 for dirpath, _, filenames in os.walk(top):
Doug Zongker75f17362009-12-08 13:46:44 -0800125 certs = [os.path.join(dirpath, i)
126 for i in filenames if i.endswith(".x509.pem")]
127 if certs:
128 to_load.extend(certs)
129
130 for i in to_load:
131 f = open(i)
Baligh Uddinbeb6afd2013-11-13 00:22:34 +0000132 cert = common.ParseCertificate(f.read())
Doug Zongker75f17362009-12-08 13:46:44 -0800133 f.close()
134 name, _ = os.path.splitext(i)
135 name, _ = os.path.splitext(name)
136 self.Add(cert, name)
137
138ALL_CERTS = CertDB()
139
140
Doug Zongker75f17362009-12-08 13:46:44 -0800141def CertFromPKCS7(data, filename):
142 """Read the cert out of a PKCS#7-format file (which is what is
143 stored in a signed .apk)."""
144 Push(filename + ":")
145 try:
146 p = common.Run(["openssl", "pkcs7",
147 "-inform", "DER",
148 "-outform", "PEM",
149 "-print_certs"],
150 stdin=subprocess.PIPE,
151 stdout=subprocess.PIPE)
152 out, err = p.communicate(data)
153 if err and not err.strip():
154 AddProblem("error reading cert:\n" + err)
155 return None
156
Baligh Uddinbeb6afd2013-11-13 00:22:34 +0000157 cert = common.ParseCertificate(out)
Doug Zongker75f17362009-12-08 13:46:44 -0800158 if not cert:
159 AddProblem("error parsing cert output")
160 return None
161 return cert
162 finally:
163 Pop()
164
165
166class APK(object):
167 def __init__(self, full_filename, filename):
168 self.filename = filename
Dan Albert8b72aef2015-03-23 19:13:21 -0700169 self.certs = None
170 self.shared_uid = None
171 self.package = None
172
Doug Zongker75f17362009-12-08 13:46:44 -0800173 Push(filename+":")
174 try:
Doug Zongkera5f534d2011-11-11 09:51:37 -0800175 self.RecordCerts(full_filename)
Doug Zongker75f17362009-12-08 13:46:44 -0800176 self.ReadManifest(full_filename)
177 finally:
178 Pop()
179
Doug Zongkera5f534d2011-11-11 09:51:37 -0800180 def RecordCerts(self, full_filename):
181 out = set()
Doug Zongker75f17362009-12-08 13:46:44 -0800182 try:
183 f = open(full_filename)
184 apk = zipfile.ZipFile(f, "r")
185 pkcs7 = None
186 for info in apk.infolist():
187 if info.filename.startswith("META-INF/") and \
188 (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")):
Doug Zongker75f17362009-12-08 13:46:44 -0800189 pkcs7 = apk.read(info.filename)
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700190 cert = CertFromPKCS7(pkcs7, info.filename)
Doug Zongkera5f534d2011-11-11 09:51:37 -0800191 out.add(cert)
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700192 ALL_CERTS.Add(cert)
Doug Zongker75f17362009-12-08 13:46:44 -0800193 if not pkcs7:
194 AddProblem("no signature")
195 finally:
196 f.close()
Doug Zongkera5f534d2011-11-11 09:51:37 -0800197 self.certs = frozenset(out)
Doug Zongker75f17362009-12-08 13:46:44 -0800198
199 def ReadManifest(self, full_filename):
200 p = common.Run(["aapt", "dump", "xmltree", full_filename,
201 "AndroidManifest.xml"],
202 stdout=subprocess.PIPE)
203 manifest, err = p.communicate()
204 if err:
205 AddProblem("failed to read manifest")
206 return
207
208 self.shared_uid = None
209 self.package = None
210
211 for line in manifest.split("\n"):
212 line = line.strip()
Dan Albert8b72aef2015-03-23 19:13:21 -0700213 m = re.search(r'A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
Doug Zongker75f17362009-12-08 13:46:44 -0800214 if m:
215 name = m.group(1)
216 if name == "android:sharedUserId":
217 if self.shared_uid is not None:
218 AddProblem("multiple sharedUserId declarations")
219 self.shared_uid = m.group(2)
220 elif name == "package":
221 if self.package is not None:
222 AddProblem("multiple package declarations")
223 self.package = m.group(2)
224
225 if self.package is None:
226 AddProblem("no package declaration")
227
228
229class TargetFiles(object):
230 def __init__(self):
231 self.max_pkg_len = 30
232 self.max_fn_len = 20
Dan Albert8b72aef2015-03-23 19:13:21 -0700233 self.apks = None
234 self.apks_by_basename = None
235 self.certmap = None
Doug Zongker75f17362009-12-08 13:46:44 -0800236
237 def LoadZipFile(self, filename):
Doug Zongker6ae53812011-01-27 10:20:27 -0800238 d, z = common.UnzipTemp(filename, '*.apk')
Doug Zongker75f17362009-12-08 13:46:44 -0800239 try:
240 self.apks = {}
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800241 self.apks_by_basename = {}
Dan Albert8b72aef2015-03-23 19:13:21 -0700242 for dirpath, _, filenames in os.walk(d):
Doug Zongker75f17362009-12-08 13:46:44 -0800243 for fn in filenames:
244 if fn.endswith(".apk"):
245 fullname = os.path.join(dirpath, fn)
246 displayname = fullname[len(d)+1:]
247 apk = APK(fullname, displayname)
248 self.apks[apk.package] = apk
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800249 self.apks_by_basename[os.path.basename(apk.filename)] = apk
Doug Zongker75f17362009-12-08 13:46:44 -0800250
251 self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
252 self.max_fn_len = max(self.max_fn_len, len(apk.filename))
253 finally:
254 shutil.rmtree(d)
255
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800256 self.certmap = common.ReadApkCerts(z)
257 z.close()
258
Doug Zongker75f17362009-12-08 13:46:44 -0800259 def CheckSharedUids(self):
260 """Look for any instances where packages signed with different
261 certs request the same sharedUserId."""
262 apks_by_uid = {}
263 for apk in self.apks.itervalues():
264 if apk.shared_uid:
265 apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
266
267 for uid in sorted(apks_by_uid.keys()):
268 apks = apks_by_uid[uid]
269 for apk in apks[1:]:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700270 if apk.certs != apks[0].certs:
Doug Zongker75f17362009-12-08 13:46:44 -0800271 break
272 else:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700273 # all packages have the same set of certs; this uid is fine.
Doug Zongker75f17362009-12-08 13:46:44 -0800274 continue
275
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700276 AddProblem("different cert sets for packages with uid %s" % (uid,))
Doug Zongker75f17362009-12-08 13:46:44 -0800277
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700278 print "uid %s is shared by packages with different cert sets:" % (uid,)
279 for apk in apks:
280 print "%-*s [%s]" % (self.max_pkg_len, apk.package, apk.filename)
281 for cert in apk.certs:
282 print " ", ALL_CERTS.Get(cert)
Doug Zongker75f17362009-12-08 13:46:44 -0800283 print
284
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800285 def CheckExternalSignatures(self):
286 for apk_filename, certname in self.certmap.iteritems():
287 if certname == "EXTERNAL":
288 # Apps marked EXTERNAL should be signed with the test key
289 # during development, then manually re-signed after
290 # predexopting. Consider it an error if this app is now
291 # signed with any key that is present in our tree.
292 apk = self.apks_by_basename[apk_filename]
293 name = ALL_CERTS.Get(apk.cert)
294 if not name.startswith("unknown "):
295 Push(apk.filename)
296 AddProblem("hasn't been signed with EXTERNAL cert")
297 Pop()
298
Doug Zongker75f17362009-12-08 13:46:44 -0800299 def PrintCerts(self):
300 """Display a table of packages grouped by cert."""
301 by_cert = {}
302 for apk in self.apks.itervalues():
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700303 for cert in apk.certs:
304 by_cert.setdefault(cert, []).append((apk.package, apk))
Doug Zongker75f17362009-12-08 13:46:44 -0800305
306 order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
307 order.sort()
308
309 for _, cert in order:
310 print "%s:" % (ALL_CERTS.Get(cert),)
311 apks = by_cert[cert]
312 apks.sort()
313 for _, apk in apks:
314 if apk.shared_uid:
315 print " %-*s %-*s [%s]" % (self.max_fn_len, apk.filename,
316 self.max_pkg_len, apk.package,
317 apk.shared_uid)
318 else:
319 print " %-*s %-*s" % (self.max_fn_len, apk.filename,
320 self.max_pkg_len, apk.package)
321 print
322
323 def CompareWith(self, other):
324 """Look for instances where a given package that exists in both
325 self and other have different certs."""
326
Dan Albert8b72aef2015-03-23 19:13:21 -0700327 all_apks = set(self.apks.keys())
328 all_apks.update(other.apks.keys())
Doug Zongker75f17362009-12-08 13:46:44 -0800329
330 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
331
332 by_certpair = {}
333
Tao Bao726b7f32015-06-03 17:31:34 -0700334 for i in all_apks:
Doug Zongker75f17362009-12-08 13:46:44 -0800335 if i in self.apks:
336 if i in other.apks:
Doug Zongker278c9782011-11-09 10:32:23 -0800337 # in both; should have same set of certs
338 if self.apks[i].certs != other.apks[i].certs:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700339 by_certpair.setdefault((other.apks[i].certs,
340 self.apks[i].certs), []).append(i)
Doug Zongker75f17362009-12-08 13:46:44 -0800341 else:
342 print "%s [%s]: new APK (not in comparison target_files)" % (
343 i, self.apks[i].filename)
344 else:
345 if i in other.apks:
346 print "%s [%s]: removed APK (only in comparison target_files)" % (
347 i, other.apks[i].filename)
348
349 if by_certpair:
350 AddProblem("some APKs changed certs")
351 Banner("APK signing differences")
352 for (old, new), packages in sorted(by_certpair.items()):
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700353 for i, o in enumerate(old):
354 if i == 0:
355 print "was", ALL_CERTS.Get(o)
356 else:
357 print " ", ALL_CERTS.Get(o)
358 for i, n in enumerate(new):
359 if i == 0:
360 print "now", ALL_CERTS.Get(n)
361 else:
362 print " ", ALL_CERTS.Get(n)
Doug Zongker75f17362009-12-08 13:46:44 -0800363 for i in sorted(packages):
364 old_fn = other.apks[i].filename
365 new_fn = self.apks[i].filename
366 if old_fn == new_fn:
367 print " %-*s [%s]" % (max_pkg_len, i, old_fn)
368 else:
369 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i,
370 old_fn, new_fn)
371 print
372
373
374def main(argv):
375 def option_handler(o, a):
376 if o in ("-c", "--compare_with"):
377 OPTIONS.compare_with = a
378 elif o in ("-l", "--local_cert_dirs"):
379 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
380 elif o in ("-t", "--text"):
381 OPTIONS.text = True
382 else:
383 return False
384 return True
385
386 args = common.ParseOptions(argv, __doc__,
387 extra_opts="c:l:t",
388 extra_long_opts=["compare_with=",
389 "local_cert_dirs="],
390 extra_option_handler=option_handler)
391
392 if len(args) != 1:
393 common.Usage(__doc__)
394 sys.exit(1)
395
396 ALL_CERTS.FindLocalCerts()
397
398 Push("input target_files:")
399 try:
400 target_files = TargetFiles()
401 target_files.LoadZipFile(args[0])
402 finally:
403 Pop()
404
405 compare_files = None
406 if OPTIONS.compare_with:
407 Push("comparison target_files:")
408 try:
409 compare_files = TargetFiles()
410 compare_files.LoadZipFile(OPTIONS.compare_with)
411 finally:
412 Pop()
413
414 if OPTIONS.text or not compare_files:
415 Banner("target files")
416 target_files.PrintCerts()
417 target_files.CheckSharedUids()
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800418 target_files.CheckExternalSignatures()
Doug Zongker75f17362009-12-08 13:46:44 -0800419 if compare_files:
420 if OPTIONS.text:
421 Banner("comparison files")
422 compare_files.PrintCerts()
423 target_files.CompareWith(compare_files)
424
425 if PROBLEMS:
426 print "%d problem(s) found:\n" % (len(PROBLEMS),)
427 for p in PROBLEMS:
428 print p
429 return 1
430
431 return 0
432
433
434if __name__ == '__main__':
435 try:
436 r = main(sys.argv[1:])
437 sys.exit(r)
Dan Albert8b72aef2015-03-23 19:13:21 -0700438 except common.ExternalError as e:
Doug Zongker75f17362009-12-08 13:46:44 -0800439 print
440 print " ERROR: %s" % (e,)
441 print
442 sys.exit(1)