blob: a3a5c1ce0e4534c2112e7c3819dba36c8e9123e7 [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
Tao Baod32e78f2018-01-17 10:08:48 -080056# Work around a bug in Python's zipfile module that prevents opening of zipfiles
57# if any entry has an extra field of between 1 and 3 bytes (which is common with
58# zipaligned APKs). This overrides the ZipInfo._decodeExtra() method (which
59# contains the bug) with an empty version (since we don't need to decode the
60# extra field anyway).
61# Issue #14315: https://bugs.python.org/issue14315, fixed in Python 2.7.8 and
62# Python 3.5.0 alpha 1.
Doug Zongker75f17362009-12-08 13:46:44 -080063class MyZipInfo(zipfile.ZipInfo):
64 def _decodeExtra(self):
65 pass
66zipfile.ZipInfo = MyZipInfo
67
68OPTIONS = common.OPTIONS
69
70OPTIONS.text = False
71OPTIONS.compare_with = None
72OPTIONS.local_cert_dirs = ("vendor", "build")
73
74PROBLEMS = []
75PROBLEM_PREFIX = []
76
77def AddProblem(msg):
78 PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
79def Push(msg):
80 PROBLEM_PREFIX.append(msg)
81def Pop():
82 PROBLEM_PREFIX.pop()
83
84
85def Banner(msg):
86 print "-" * 70
87 print " ", msg
88 print "-" * 70
89
90
91def GetCertSubject(cert):
92 p = common.Run(["openssl", "x509", "-inform", "DER", "-text"],
93 stdin=subprocess.PIPE,
94 stdout=subprocess.PIPE)
95 out, err = p.communicate(cert)
96 if err and not err.strip():
97 return "(error reading cert subject)"
98 for line in out.split("\n"):
99 line = line.strip()
100 if line.startswith("Subject:"):
101 return line[8:].strip()
102 return "(unknown cert subject)"
103
104
105class CertDB(object):
106 def __init__(self):
107 self.certs = {}
108
109 def Add(self, cert, name=None):
110 if cert in self.certs:
111 if name:
112 self.certs[cert] = self.certs[cert] + "," + name
113 else:
114 if name is None:
Doug Zongker6ae53812011-01-27 10:20:27 -0800115 name = "unknown cert %s (%s)" % (common.sha1(cert).hexdigest()[:12],
Doug Zongker75f17362009-12-08 13:46:44 -0800116 GetCertSubject(cert))
117 self.certs[cert] = name
118
119 def Get(self, cert):
120 """Return the name for a given cert."""
121 return self.certs.get(cert, None)
122
123 def FindLocalCerts(self):
124 to_load = []
125 for top in OPTIONS.local_cert_dirs:
Dan Albert8b72aef2015-03-23 19:13:21 -0700126 for dirpath, _, filenames in os.walk(top):
Doug Zongker75f17362009-12-08 13:46:44 -0800127 certs = [os.path.join(dirpath, i)
128 for i in filenames if i.endswith(".x509.pem")]
129 if certs:
130 to_load.extend(certs)
131
132 for i in to_load:
133 f = open(i)
Baligh Uddinbeb6afd2013-11-13 00:22:34 +0000134 cert = common.ParseCertificate(f.read())
Doug Zongker75f17362009-12-08 13:46:44 -0800135 f.close()
136 name, _ = os.path.splitext(i)
137 name, _ = os.path.splitext(name)
138 self.Add(cert, name)
139
140ALL_CERTS = CertDB()
141
142
Doug Zongker75f17362009-12-08 13:46:44 -0800143def CertFromPKCS7(data, filename):
144 """Read the cert out of a PKCS#7-format file (which is what is
145 stored in a signed .apk)."""
146 Push(filename + ":")
147 try:
148 p = common.Run(["openssl", "pkcs7",
149 "-inform", "DER",
150 "-outform", "PEM",
151 "-print_certs"],
152 stdin=subprocess.PIPE,
153 stdout=subprocess.PIPE)
154 out, err = p.communicate(data)
155 if err and not err.strip():
156 AddProblem("error reading cert:\n" + err)
157 return None
158
Baligh Uddinbeb6afd2013-11-13 00:22:34 +0000159 cert = common.ParseCertificate(out)
Doug Zongker75f17362009-12-08 13:46:44 -0800160 if not cert:
161 AddProblem("error parsing cert output")
162 return None
163 return cert
164 finally:
165 Pop()
166
167
168class APK(object):
169 def __init__(self, full_filename, filename):
170 self.filename = filename
Dan Albert8b72aef2015-03-23 19:13:21 -0700171 self.certs = None
172 self.shared_uid = None
173 self.package = None
174
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()
Dan Albert8b72aef2015-03-23 19:13:21 -0700215 m = re.search(r'A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
Doug Zongker75f17362009-12-08 13:46:44 -0800216 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
Dan Albert8b72aef2015-03-23 19:13:21 -0700235 self.apks = None
236 self.apks_by_basename = None
237 self.certmap = None
Doug Zongker75f17362009-12-08 13:46:44 -0800238
239 def LoadZipFile(self, filename):
Narayan Kamatha07bf042017-08-14 14:49:21 +0100240 # First read the APK certs file to figure out whether there are compressed
241 # APKs in the archive. If we do have compressed APKs in the archive, then we
242 # must decompress them individually before we perform any analysis.
243
244 # This is the list of wildcards of files we extract from |filename|.
245 apk_extensions = ['*.apk']
246
247 self.certmap, compressed_extension = common.ReadApkCerts(zipfile.ZipFile(filename, "r"))
248 if compressed_extension:
249 apk_extensions.append("*.apk" + compressed_extension)
250
Tao Baodba59ee2018-01-09 13:21:02 -0800251 d = common.UnzipTemp(filename, apk_extensions)
Doug Zongker75f17362009-12-08 13:46:44 -0800252 try:
253 self.apks = {}
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800254 self.apks_by_basename = {}
Dan Albert8b72aef2015-03-23 19:13:21 -0700255 for dirpath, _, filenames in os.walk(d):
Doug Zongker75f17362009-12-08 13:46:44 -0800256 for fn in filenames:
Narayan Kamatha07bf042017-08-14 14:49:21 +0100257 # Decompress compressed APKs before we begin processing them.
258 if compressed_extension and fn.endswith(compressed_extension):
259 # First strip the compressed extension from the file.
260 uncompressed_fn = fn[:-len(compressed_extension)]
261
262 # Decompress the compressed file to the output file.
263 common.Gunzip(os.path.join(dirpath, fn),
264 os.path.join(dirpath, uncompressed_fn))
265
266 # Finally, delete the compressed file and use the uncompressed file
267 # for further processing. Note that the deletion is not strictly required,
268 # but is done here to ensure that we're not using too much space in
269 # the temporary directory.
270 os.remove(os.path.join(dirpath, fn))
271 fn = uncompressed_fn
272
273
Doug Zongker75f17362009-12-08 13:46:44 -0800274 if fn.endswith(".apk"):
275 fullname = os.path.join(dirpath, fn)
276 displayname = fullname[len(d)+1:]
277 apk = APK(fullname, displayname)
Tao Bao6a542992016-07-27 19:45:43 -0700278 self.apks[apk.filename] = apk
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800279 self.apks_by_basename[os.path.basename(apk.filename)] = apk
Doug Zongker75f17362009-12-08 13:46:44 -0800280
281 self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
282 self.max_fn_len = max(self.max_fn_len, len(apk.filename))
283 finally:
284 shutil.rmtree(d)
285
286 def CheckSharedUids(self):
287 """Look for any instances where packages signed with different
288 certs request the same sharedUserId."""
289 apks_by_uid = {}
290 for apk in self.apks.itervalues():
291 if apk.shared_uid:
292 apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
293
294 for uid in sorted(apks_by_uid.keys()):
295 apks = apks_by_uid[uid]
296 for apk in apks[1:]:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700297 if apk.certs != apks[0].certs:
Doug Zongker75f17362009-12-08 13:46:44 -0800298 break
299 else:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700300 # all packages have the same set of certs; this uid is fine.
Doug Zongker75f17362009-12-08 13:46:44 -0800301 continue
302
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700303 AddProblem("different cert sets for packages with uid %s" % (uid,))
Doug Zongker75f17362009-12-08 13:46:44 -0800304
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700305 print "uid %s is shared by packages with different cert sets:" % (uid,)
306 for apk in apks:
307 print "%-*s [%s]" % (self.max_pkg_len, apk.package, apk.filename)
308 for cert in apk.certs:
309 print " ", ALL_CERTS.Get(cert)
Doug Zongker75f17362009-12-08 13:46:44 -0800310 print
311
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800312 def CheckExternalSignatures(self):
313 for apk_filename, certname in self.certmap.iteritems():
314 if certname == "EXTERNAL":
315 # Apps marked EXTERNAL should be signed with the test key
316 # during development, then manually re-signed after
317 # predexopting. Consider it an error if this app is now
318 # signed with any key that is present in our tree.
319 apk = self.apks_by_basename[apk_filename]
320 name = ALL_CERTS.Get(apk.cert)
321 if not name.startswith("unknown "):
322 Push(apk.filename)
323 AddProblem("hasn't been signed with EXTERNAL cert")
324 Pop()
325
Doug Zongker75f17362009-12-08 13:46:44 -0800326 def PrintCerts(self):
327 """Display a table of packages grouped by cert."""
328 by_cert = {}
329 for apk in self.apks.itervalues():
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700330 for cert in apk.certs:
331 by_cert.setdefault(cert, []).append((apk.package, apk))
Doug Zongker75f17362009-12-08 13:46:44 -0800332
333 order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
334 order.sort()
335
336 for _, cert in order:
337 print "%s:" % (ALL_CERTS.Get(cert),)
338 apks = by_cert[cert]
339 apks.sort()
340 for _, apk in apks:
341 if apk.shared_uid:
342 print " %-*s %-*s [%s]" % (self.max_fn_len, apk.filename,
343 self.max_pkg_len, apk.package,
344 apk.shared_uid)
345 else:
Tao Bao6a542992016-07-27 19:45:43 -0700346 print " %-*s %s" % (self.max_fn_len, apk.filename, apk.package)
Doug Zongker75f17362009-12-08 13:46:44 -0800347 print
348
349 def CompareWith(self, other):
350 """Look for instances where a given package that exists in both
351 self and other have different certs."""
352
Dan Albert8b72aef2015-03-23 19:13:21 -0700353 all_apks = set(self.apks.keys())
354 all_apks.update(other.apks.keys())
Doug Zongker75f17362009-12-08 13:46:44 -0800355
356 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
357
358 by_certpair = {}
359
Tao Bao726b7f32015-06-03 17:31:34 -0700360 for i in all_apks:
Doug Zongker75f17362009-12-08 13:46:44 -0800361 if i in self.apks:
362 if i in other.apks:
Doug Zongker278c9782011-11-09 10:32:23 -0800363 # in both; should have same set of certs
364 if self.apks[i].certs != other.apks[i].certs:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700365 by_certpair.setdefault((other.apks[i].certs,
366 self.apks[i].certs), []).append(i)
Doug Zongker75f17362009-12-08 13:46:44 -0800367 else:
368 print "%s [%s]: new APK (not in comparison target_files)" % (
369 i, self.apks[i].filename)
370 else:
371 if i in other.apks:
372 print "%s [%s]: removed APK (only in comparison target_files)" % (
373 i, other.apks[i].filename)
374
375 if by_certpair:
376 AddProblem("some APKs changed certs")
377 Banner("APK signing differences")
378 for (old, new), packages in sorted(by_certpair.items()):
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700379 for i, o in enumerate(old):
380 if i == 0:
381 print "was", ALL_CERTS.Get(o)
382 else:
383 print " ", ALL_CERTS.Get(o)
384 for i, n in enumerate(new):
385 if i == 0:
386 print "now", ALL_CERTS.Get(n)
387 else:
388 print " ", ALL_CERTS.Get(n)
Doug Zongker75f17362009-12-08 13:46:44 -0800389 for i in sorted(packages):
390 old_fn = other.apks[i].filename
391 new_fn = self.apks[i].filename
392 if old_fn == new_fn:
393 print " %-*s [%s]" % (max_pkg_len, i, old_fn)
394 else:
395 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i,
396 old_fn, new_fn)
397 print
398
399
400def main(argv):
401 def option_handler(o, a):
402 if o in ("-c", "--compare_with"):
403 OPTIONS.compare_with = a
404 elif o in ("-l", "--local_cert_dirs"):
405 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
406 elif o in ("-t", "--text"):
407 OPTIONS.text = True
408 else:
409 return False
410 return True
411
412 args = common.ParseOptions(argv, __doc__,
413 extra_opts="c:l:t",
414 extra_long_opts=["compare_with=",
415 "local_cert_dirs="],
416 extra_option_handler=option_handler)
417
418 if len(args) != 1:
419 common.Usage(__doc__)
420 sys.exit(1)
421
422 ALL_CERTS.FindLocalCerts()
423
424 Push("input target_files:")
425 try:
426 target_files = TargetFiles()
427 target_files.LoadZipFile(args[0])
428 finally:
429 Pop()
430
431 compare_files = None
432 if OPTIONS.compare_with:
433 Push("comparison target_files:")
434 try:
435 compare_files = TargetFiles()
436 compare_files.LoadZipFile(OPTIONS.compare_with)
437 finally:
438 Pop()
439
440 if OPTIONS.text or not compare_files:
441 Banner("target files")
442 target_files.PrintCerts()
443 target_files.CheckSharedUids()
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800444 target_files.CheckExternalSignatures()
Doug Zongker75f17362009-12-08 13:46:44 -0800445 if compare_files:
446 if OPTIONS.text:
447 Banner("comparison files")
448 compare_files.PrintCerts()
449 target_files.CompareWith(compare_files)
450
451 if PROBLEMS:
452 print "%d problem(s) found:\n" % (len(PROBLEMS),)
453 for p in PROBLEMS:
454 print p
455 return 1
456
457 return 0
458
459
460if __name__ == '__main__':
461 try:
462 r = main(sys.argv[1:])
463 sys.exit(r)
Dan Albert8b72aef2015-03-23 19:13:21 -0700464 except common.ExternalError as e:
Doug Zongker75f17362009-12-08 13:46:44 -0800465 print
466 print " ERROR: %s" % (e,)
467 print
468 sys.exit(1)