blob: 9b7695479bf3d03de703f9e583dc417d1fb03fa6 [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
Tao Baobadceb22019-03-15 09:33:43 -070042import logging
Tao Bao767543a2018-03-01 10:09:07 -080043import os
44import re
45import subprocess
Doug Zongker75f17362009-12-08 13:46:44 -080046import sys
Tao Bao767543a2018-03-01 10:09:07 -080047import zipfile
48
49import common
Doug Zongker75f17362009-12-08 13:46:44 -080050
Doug Zongkercf6d5a92014-02-18 10:57:07 -080051if sys.hexversion < 0x02070000:
52 print >> sys.stderr, "Python 2.7 or newer is required."
Doug Zongker75f17362009-12-08 13:46:44 -080053 sys.exit(1)
54
Doug Zongker75f17362009-12-08 13:46:44 -080055
Tao Baobadceb22019-03-15 09:33:43 -070056logger = logging.getLogger(__name__)
57
Tao Baod32e78f2018-01-17 10:08:48 -080058# Work around a bug in Python's zipfile module that prevents opening of zipfiles
59# if any entry has an extra field of between 1 and 3 bytes (which is common with
60# zipaligned APKs). This overrides the ZipInfo._decodeExtra() method (which
61# contains the bug) with an empty version (since we don't need to decode the
62# extra field anyway).
63# Issue #14315: https://bugs.python.org/issue14315, fixed in Python 2.7.8 and
64# Python 3.5.0 alpha 1.
Doug Zongker75f17362009-12-08 13:46:44 -080065class MyZipInfo(zipfile.ZipInfo):
66 def _decodeExtra(self):
67 pass
68zipfile.ZipInfo = MyZipInfo
69
70OPTIONS = common.OPTIONS
71
72OPTIONS.text = False
73OPTIONS.compare_with = None
74OPTIONS.local_cert_dirs = ("vendor", "build")
75
76PROBLEMS = []
77PROBLEM_PREFIX = []
78
79def AddProblem(msg):
80 PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
81def Push(msg):
82 PROBLEM_PREFIX.append(msg)
83def Pop():
84 PROBLEM_PREFIX.pop()
85
86
87def Banner(msg):
88 print "-" * 70
89 print " ", msg
90 print "-" * 70
91
92
93def GetCertSubject(cert):
94 p = common.Run(["openssl", "x509", "-inform", "DER", "-text"],
95 stdin=subprocess.PIPE,
96 stdout=subprocess.PIPE)
97 out, err = p.communicate(cert)
98 if err and not err.strip():
99 return "(error reading cert subject)"
100 for line in out.split("\n"):
101 line = line.strip()
102 if line.startswith("Subject:"):
103 return line[8:].strip()
104 return "(unknown cert subject)"
105
106
107class CertDB(object):
108 def __init__(self):
109 self.certs = {}
110
111 def Add(self, cert, name=None):
112 if cert in self.certs:
113 if name:
114 self.certs[cert] = self.certs[cert] + "," + name
115 else:
116 if name is None:
Doug Zongker6ae53812011-01-27 10:20:27 -0800117 name = "unknown cert %s (%s)" % (common.sha1(cert).hexdigest()[:12],
Doug Zongker75f17362009-12-08 13:46:44 -0800118 GetCertSubject(cert))
119 self.certs[cert] = name
120
121 def Get(self, cert):
122 """Return the name for a given cert."""
123 return self.certs.get(cert, None)
124
125 def FindLocalCerts(self):
126 to_load = []
127 for top in OPTIONS.local_cert_dirs:
Dan Albert8b72aef2015-03-23 19:13:21 -0700128 for dirpath, _, filenames in os.walk(top):
Doug Zongker75f17362009-12-08 13:46:44 -0800129 certs = [os.path.join(dirpath, i)
130 for i in filenames if i.endswith(".x509.pem")]
131 if certs:
132 to_load.extend(certs)
133
134 for i in to_load:
135 f = open(i)
Baligh Uddinbeb6afd2013-11-13 00:22:34 +0000136 cert = common.ParseCertificate(f.read())
Doug Zongker75f17362009-12-08 13:46:44 -0800137 f.close()
138 name, _ = os.path.splitext(i)
139 name, _ = os.path.splitext(name)
140 self.Add(cert, name)
141
142ALL_CERTS = CertDB()
143
144
Doug Zongker75f17362009-12-08 13:46:44 -0800145def CertFromPKCS7(data, filename):
146 """Read the cert out of a PKCS#7-format file (which is what is
147 stored in a signed .apk)."""
148 Push(filename + ":")
149 try:
150 p = common.Run(["openssl", "pkcs7",
151 "-inform", "DER",
152 "-outform", "PEM",
153 "-print_certs"],
154 stdin=subprocess.PIPE,
155 stdout=subprocess.PIPE)
156 out, err = p.communicate(data)
157 if err and not err.strip():
158 AddProblem("error reading cert:\n" + err)
159 return None
160
Baligh Uddinbeb6afd2013-11-13 00:22:34 +0000161 cert = common.ParseCertificate(out)
Doug Zongker75f17362009-12-08 13:46:44 -0800162 if not cert:
163 AddProblem("error parsing cert output")
164 return None
165 return cert
166 finally:
167 Pop()
168
169
170class APK(object):
171 def __init__(self, full_filename, filename):
172 self.filename = filename
Dan Albert8b72aef2015-03-23 19:13:21 -0700173 self.certs = None
174 self.shared_uid = None
175 self.package = None
176
Doug Zongker75f17362009-12-08 13:46:44 -0800177 Push(filename+":")
178 try:
Doug Zongkera5f534d2011-11-11 09:51:37 -0800179 self.RecordCerts(full_filename)
Doug Zongker75f17362009-12-08 13:46:44 -0800180 self.ReadManifest(full_filename)
181 finally:
182 Pop()
183
Doug Zongkera5f534d2011-11-11 09:51:37 -0800184 def RecordCerts(self, full_filename):
185 out = set()
Doug Zongker75f17362009-12-08 13:46:44 -0800186 try:
187 f = open(full_filename)
188 apk = zipfile.ZipFile(f, "r")
189 pkcs7 = None
190 for info in apk.infolist():
191 if info.filename.startswith("META-INF/") and \
192 (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")):
Doug Zongker75f17362009-12-08 13:46:44 -0800193 pkcs7 = apk.read(info.filename)
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700194 cert = CertFromPKCS7(pkcs7, info.filename)
Doug Zongkera5f534d2011-11-11 09:51:37 -0800195 out.add(cert)
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700196 ALL_CERTS.Add(cert)
Doug Zongker75f17362009-12-08 13:46:44 -0800197 if not pkcs7:
198 AddProblem("no signature")
199 finally:
200 f.close()
Doug Zongkera5f534d2011-11-11 09:51:37 -0800201 self.certs = frozenset(out)
Doug Zongker75f17362009-12-08 13:46:44 -0800202
203 def ReadManifest(self, full_filename):
204 p = common.Run(["aapt", "dump", "xmltree", full_filename,
205 "AndroidManifest.xml"],
206 stdout=subprocess.PIPE)
207 manifest, err = p.communicate()
208 if err:
209 AddProblem("failed to read manifest")
210 return
211
212 self.shared_uid = None
213 self.package = None
214
215 for line in manifest.split("\n"):
216 line = line.strip()
Dan Albert8b72aef2015-03-23 19:13:21 -0700217 m = re.search(r'A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
Doug Zongker75f17362009-12-08 13:46:44 -0800218 if m:
219 name = m.group(1)
220 if name == "android:sharedUserId":
221 if self.shared_uid is not None:
222 AddProblem("multiple sharedUserId declarations")
223 self.shared_uid = m.group(2)
224 elif name == "package":
225 if self.package is not None:
226 AddProblem("multiple package declarations")
227 self.package = m.group(2)
228
229 if self.package is None:
230 AddProblem("no package declaration")
231
232
233class TargetFiles(object):
234 def __init__(self):
235 self.max_pkg_len = 30
236 self.max_fn_len = 20
Dan Albert8b72aef2015-03-23 19:13:21 -0700237 self.apks = None
238 self.apks_by_basename = None
239 self.certmap = None
Doug Zongker75f17362009-12-08 13:46:44 -0800240
241 def LoadZipFile(self, filename):
Narayan Kamatha07bf042017-08-14 14:49:21 +0100242 # First read the APK certs file to figure out whether there are compressed
243 # APKs in the archive. If we do have compressed APKs in the archive, then we
244 # must decompress them individually before we perform any analysis.
245
246 # This is the list of wildcards of files we extract from |filename|.
247 apk_extensions = ['*.apk']
248
Tao Bao767543a2018-03-01 10:09:07 -0800249 self.certmap, compressed_extension = common.ReadApkCerts(
250 zipfile.ZipFile(filename, "r"))
Narayan Kamatha07bf042017-08-14 14:49:21 +0100251 if compressed_extension:
252 apk_extensions.append("*.apk" + compressed_extension)
253
Tao Baodba59ee2018-01-09 13:21:02 -0800254 d = common.UnzipTemp(filename, apk_extensions)
Tao Bao767543a2018-03-01 10:09:07 -0800255 self.apks = {}
256 self.apks_by_basename = {}
257 for dirpath, _, filenames in os.walk(d):
258 for fn in filenames:
259 # Decompress compressed APKs before we begin processing them.
260 if compressed_extension and fn.endswith(compressed_extension):
261 # First strip the compressed extension from the file.
262 uncompressed_fn = fn[:-len(compressed_extension)]
Narayan Kamatha07bf042017-08-14 14:49:21 +0100263
Tao Bao767543a2018-03-01 10:09:07 -0800264 # Decompress the compressed file to the output file.
265 common.Gunzip(os.path.join(dirpath, fn),
266 os.path.join(dirpath, uncompressed_fn))
Narayan Kamatha07bf042017-08-14 14:49:21 +0100267
Tao Bao767543a2018-03-01 10:09:07 -0800268 # Finally, delete the compressed file and use the uncompressed file
269 # for further processing. Note that the deletion is not strictly
270 # required, but is done here to ensure that we're not using too much
271 # space in the temporary directory.
272 os.remove(os.path.join(dirpath, fn))
273 fn = uncompressed_fn
Narayan Kamatha07bf042017-08-14 14:49:21 +0100274
Tao Bao767543a2018-03-01 10:09:07 -0800275 if fn.endswith(".apk"):
276 fullname = os.path.join(dirpath, fn)
277 displayname = fullname[len(d)+1:]
278 apk = APK(fullname, displayname)
279 self.apks[apk.filename] = apk
280 self.apks_by_basename[os.path.basename(apk.filename)] = apk
Narayan Kamatha07bf042017-08-14 14:49:21 +0100281
Tao Bao767543a2018-03-01 10:09:07 -0800282 self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
283 self.max_fn_len = max(self.max_fn_len, len(apk.filename))
Doug Zongker75f17362009-12-08 13:46:44 -0800284
285 def CheckSharedUids(self):
286 """Look for any instances where packages signed with different
287 certs request the same sharedUserId."""
288 apks_by_uid = {}
289 for apk in self.apks.itervalues():
290 if apk.shared_uid:
291 apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
292
Tao Bao767543a2018-03-01 10:09:07 -0800293 for uid in sorted(apks_by_uid):
Doug Zongker75f17362009-12-08 13:46:44 -0800294 apks = apks_by_uid[uid]
295 for apk in apks[1:]:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700296 if apk.certs != apks[0].certs:
Doug Zongker75f17362009-12-08 13:46:44 -0800297 break
298 else:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700299 # all packages have the same set of certs; this uid is fine.
Doug Zongker75f17362009-12-08 13:46:44 -0800300 continue
301
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700302 AddProblem("different cert sets for packages with uid %s" % (uid,))
Doug Zongker75f17362009-12-08 13:46:44 -0800303
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700304 print "uid %s is shared by packages with different cert sets:" % (uid,)
305 for apk in apks:
306 print "%-*s [%s]" % (self.max_pkg_len, apk.package, apk.filename)
307 for cert in apk.certs:
308 print " ", ALL_CERTS.Get(cert)
Doug Zongker75f17362009-12-08 13:46:44 -0800309 print
310
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800311 def CheckExternalSignatures(self):
312 for apk_filename, certname in self.certmap.iteritems():
313 if certname == "EXTERNAL":
314 # Apps marked EXTERNAL should be signed with the test key
315 # during development, then manually re-signed after
316 # predexopting. Consider it an error if this app is now
317 # signed with any key that is present in our tree.
318 apk = self.apks_by_basename[apk_filename]
319 name = ALL_CERTS.Get(apk.cert)
320 if not name.startswith("unknown "):
321 Push(apk.filename)
322 AddProblem("hasn't been signed with EXTERNAL cert")
323 Pop()
324
Doug Zongker75f17362009-12-08 13:46:44 -0800325 def PrintCerts(self):
326 """Display a table of packages grouped by cert."""
327 by_cert = {}
328 for apk in self.apks.itervalues():
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700329 for cert in apk.certs:
330 by_cert.setdefault(cert, []).append((apk.package, apk))
Doug Zongker75f17362009-12-08 13:46:44 -0800331
332 order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
333 order.sort()
334
335 for _, cert in order:
336 print "%s:" % (ALL_CERTS.Get(cert),)
337 apks = by_cert[cert]
338 apks.sort()
339 for _, apk in apks:
340 if apk.shared_uid:
341 print " %-*s %-*s [%s]" % (self.max_fn_len, apk.filename,
342 self.max_pkg_len, apk.package,
343 apk.shared_uid)
344 else:
Tao Bao6a542992016-07-27 19:45:43 -0700345 print " %-*s %s" % (self.max_fn_len, apk.filename, apk.package)
Doug Zongker75f17362009-12-08 13:46:44 -0800346 print
347
348 def CompareWith(self, other):
349 """Look for instances where a given package that exists in both
350 self and other have different certs."""
351
Dan Albert8b72aef2015-03-23 19:13:21 -0700352 all_apks = set(self.apks.keys())
353 all_apks.update(other.apks.keys())
Doug Zongker75f17362009-12-08 13:46:44 -0800354
355 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
356
357 by_certpair = {}
358
Tao Bao726b7f32015-06-03 17:31:34 -0700359 for i in all_apks:
Doug Zongker75f17362009-12-08 13:46:44 -0800360 if i in self.apks:
361 if i in other.apks:
Doug Zongker278c9782011-11-09 10:32:23 -0800362 # in both; should have same set of certs
363 if self.apks[i].certs != other.apks[i].certs:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700364 by_certpair.setdefault((other.apks[i].certs,
365 self.apks[i].certs), []).append(i)
Doug Zongker75f17362009-12-08 13:46:44 -0800366 else:
367 print "%s [%s]: new APK (not in comparison target_files)" % (
368 i, self.apks[i].filename)
369 else:
370 if i in other.apks:
371 print "%s [%s]: removed APK (only in comparison target_files)" % (
372 i, other.apks[i].filename)
373
374 if by_certpair:
375 AddProblem("some APKs changed certs")
376 Banner("APK signing differences")
377 for (old, new), packages in sorted(by_certpair.items()):
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700378 for i, o in enumerate(old):
379 if i == 0:
380 print "was", ALL_CERTS.Get(o)
381 else:
382 print " ", ALL_CERTS.Get(o)
383 for i, n in enumerate(new):
384 if i == 0:
385 print "now", ALL_CERTS.Get(n)
386 else:
387 print " ", ALL_CERTS.Get(n)
Doug Zongker75f17362009-12-08 13:46:44 -0800388 for i in sorted(packages):
389 old_fn = other.apks[i].filename
390 new_fn = self.apks[i].filename
391 if old_fn == new_fn:
392 print " %-*s [%s]" % (max_pkg_len, i, old_fn)
393 else:
394 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i,
395 old_fn, new_fn)
396 print
397
398
399def main(argv):
400 def option_handler(o, a):
401 if o in ("-c", "--compare_with"):
402 OPTIONS.compare_with = a
403 elif o in ("-l", "--local_cert_dirs"):
404 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
405 elif o in ("-t", "--text"):
406 OPTIONS.text = True
407 else:
408 return False
409 return True
410
411 args = common.ParseOptions(argv, __doc__,
412 extra_opts="c:l:t",
413 extra_long_opts=["compare_with=",
414 "local_cert_dirs="],
415 extra_option_handler=option_handler)
416
417 if len(args) != 1:
418 common.Usage(__doc__)
419 sys.exit(1)
420
Tao Baobadceb22019-03-15 09:33:43 -0700421 common.InitLogging()
422
Doug Zongker75f17362009-12-08 13:46:44 -0800423 ALL_CERTS.FindLocalCerts()
424
425 Push("input target_files:")
426 try:
427 target_files = TargetFiles()
428 target_files.LoadZipFile(args[0])
429 finally:
430 Pop()
431
432 compare_files = None
433 if OPTIONS.compare_with:
434 Push("comparison target_files:")
435 try:
436 compare_files = TargetFiles()
437 compare_files.LoadZipFile(OPTIONS.compare_with)
438 finally:
439 Pop()
440
441 if OPTIONS.text or not compare_files:
442 Banner("target files")
443 target_files.PrintCerts()
444 target_files.CheckSharedUids()
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800445 target_files.CheckExternalSignatures()
Doug Zongker75f17362009-12-08 13:46:44 -0800446 if compare_files:
447 if OPTIONS.text:
448 Banner("comparison files")
449 compare_files.PrintCerts()
450 target_files.CompareWith(compare_files)
451
452 if PROBLEMS:
453 print "%d problem(s) found:\n" % (len(PROBLEMS),)
454 for p in PROBLEMS:
455 print p
456 return 1
457
458 return 0
459
460
461if __name__ == '__main__':
462 try:
463 r = main(sys.argv[1:])
464 sys.exit(r)
Dan Albert8b72aef2015-03-23 19:13:21 -0700465 except common.ExternalError as e:
Doug Zongker75f17362009-12-08 13:46:44 -0800466 print
467 print " ERROR: %s" % (e,)
468 print
469 sys.exit(1)
Tao Bao767543a2018-03-01 10:09:07 -0800470 finally:
471 common.Cleanup()