blob: 3048488d30c2cb98fcbbbabe570fcd321323d3db [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)
Tao Bao6a542992016-07-27 19:45:43 -0700248 self.apks[apk.filename] = 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:
Tao Bao6a542992016-07-27 19:45:43 -0700319 print " %-*s %s" % (self.max_fn_len, apk.filename, apk.package)
Doug Zongker75f17362009-12-08 13:46:44 -0800320 print
321
322 def CompareWith(self, other):
323 """Look for instances where a given package that exists in both
324 self and other have different certs."""
325
Dan Albert8b72aef2015-03-23 19:13:21 -0700326 all_apks = set(self.apks.keys())
327 all_apks.update(other.apks.keys())
Doug Zongker75f17362009-12-08 13:46:44 -0800328
329 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
330
331 by_certpair = {}
332
Tao Bao726b7f32015-06-03 17:31:34 -0700333 for i in all_apks:
Doug Zongker75f17362009-12-08 13:46:44 -0800334 if i in self.apks:
335 if i in other.apks:
Doug Zongker278c9782011-11-09 10:32:23 -0800336 # in both; should have same set of certs
337 if self.apks[i].certs != other.apks[i].certs:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700338 by_certpair.setdefault((other.apks[i].certs,
339 self.apks[i].certs), []).append(i)
Doug Zongker75f17362009-12-08 13:46:44 -0800340 else:
341 print "%s [%s]: new APK (not in comparison target_files)" % (
342 i, self.apks[i].filename)
343 else:
344 if i in other.apks:
345 print "%s [%s]: removed APK (only in comparison target_files)" % (
346 i, other.apks[i].filename)
347
348 if by_certpair:
349 AddProblem("some APKs changed certs")
350 Banner("APK signing differences")
351 for (old, new), packages in sorted(by_certpair.items()):
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700352 for i, o in enumerate(old):
353 if i == 0:
354 print "was", ALL_CERTS.Get(o)
355 else:
356 print " ", ALL_CERTS.Get(o)
357 for i, n in enumerate(new):
358 if i == 0:
359 print "now", ALL_CERTS.Get(n)
360 else:
361 print " ", ALL_CERTS.Get(n)
Doug Zongker75f17362009-12-08 13:46:44 -0800362 for i in sorted(packages):
363 old_fn = other.apks[i].filename
364 new_fn = self.apks[i].filename
365 if old_fn == new_fn:
366 print " %-*s [%s]" % (max_pkg_len, i, old_fn)
367 else:
368 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i,
369 old_fn, new_fn)
370 print
371
372
373def main(argv):
374 def option_handler(o, a):
375 if o in ("-c", "--compare_with"):
376 OPTIONS.compare_with = a
377 elif o in ("-l", "--local_cert_dirs"):
378 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
379 elif o in ("-t", "--text"):
380 OPTIONS.text = True
381 else:
382 return False
383 return True
384
385 args = common.ParseOptions(argv, __doc__,
386 extra_opts="c:l:t",
387 extra_long_opts=["compare_with=",
388 "local_cert_dirs="],
389 extra_option_handler=option_handler)
390
391 if len(args) != 1:
392 common.Usage(__doc__)
393 sys.exit(1)
394
395 ALL_CERTS.FindLocalCerts()
396
397 Push("input target_files:")
398 try:
399 target_files = TargetFiles()
400 target_files.LoadZipFile(args[0])
401 finally:
402 Pop()
403
404 compare_files = None
405 if OPTIONS.compare_with:
406 Push("comparison target_files:")
407 try:
408 compare_files = TargetFiles()
409 compare_files.LoadZipFile(OPTIONS.compare_with)
410 finally:
411 Pop()
412
413 if OPTIONS.text or not compare_files:
414 Banner("target files")
415 target_files.PrintCerts()
416 target_files.CheckSharedUids()
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800417 target_files.CheckExternalSignatures()
Doug Zongker75f17362009-12-08 13:46:44 -0800418 if compare_files:
419 if OPTIONS.text:
420 Banner("comparison files")
421 compare_files.PrintCerts()
422 target_files.CompareWith(compare_files)
423
424 if PROBLEMS:
425 print "%d problem(s) found:\n" % (len(PROBLEMS),)
426 for p in PROBLEMS:
427 print p
428 return 1
429
430 return 0
431
432
433if __name__ == '__main__':
434 try:
435 r = main(sys.argv[1:])
436 sys.exit(r)
Dan Albert8b72aef2015-03-23 19:13:21 -0700437 except common.ExternalError as e:
Doug Zongker75f17362009-12-08 13:46:44 -0800438 print
439 print " ERROR: %s" % (e,)
440 print
441 sys.exit(1)