blob: 2c97e2e57dd283c9c3babf1a09bc8c8f56505f28 [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
55import common
56
57# Work around a bug in python's zipfile module that prevents opening
58# of zipfiles if any entry has an extra field of between 1 and 3 bytes
59# (which is common with zipaligned APKs). This overrides the
60# ZipInfo._decodeExtra() method (which contains the bug) with an empty
61# version (since we don't need to decode the extra field anyway).
62class MyZipInfo(zipfile.ZipInfo):
63 def _decodeExtra(self):
64 pass
65zipfile.ZipInfo = MyZipInfo
66
67OPTIONS = common.OPTIONS
68
69OPTIONS.text = False
70OPTIONS.compare_with = None
71OPTIONS.local_cert_dirs = ("vendor", "build")
72
73PROBLEMS = []
74PROBLEM_PREFIX = []
75
76def AddProblem(msg):
77 PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
78def Push(msg):
79 PROBLEM_PREFIX.append(msg)
80def Pop():
81 PROBLEM_PREFIX.pop()
82
83
84def Banner(msg):
85 print "-" * 70
86 print " ", msg
87 print "-" * 70
88
89
90def GetCertSubject(cert):
91 p = common.Run(["openssl", "x509", "-inform", "DER", "-text"],
92 stdin=subprocess.PIPE,
93 stdout=subprocess.PIPE)
94 out, err = p.communicate(cert)
95 if err and not err.strip():
96 return "(error reading cert subject)"
97 for line in out.split("\n"):
98 line = line.strip()
99 if line.startswith("Subject:"):
100 return line[8:].strip()
101 return "(unknown cert subject)"
102
103
104class CertDB(object):
105 def __init__(self):
106 self.certs = {}
107
108 def Add(self, cert, name=None):
109 if cert in self.certs:
110 if name:
111 self.certs[cert] = self.certs[cert] + "," + name
112 else:
113 if name is None:
Doug Zongker6ae53812011-01-27 10:20:27 -0800114 name = "unknown cert %s (%s)" % (common.sha1(cert).hexdigest()[:12],
Doug Zongker75f17362009-12-08 13:46:44 -0800115 GetCertSubject(cert))
116 self.certs[cert] = name
117
118 def Get(self, cert):
119 """Return the name for a given cert."""
120 return self.certs.get(cert, None)
121
122 def FindLocalCerts(self):
123 to_load = []
124 for top in OPTIONS.local_cert_dirs:
125 for dirpath, dirnames, filenames in os.walk(top):
126 certs = [os.path.join(dirpath, i)
127 for i in filenames if i.endswith(".x509.pem")]
128 if certs:
129 to_load.extend(certs)
130
131 for i in to_load:
132 f = open(i)
133 cert = ParseCertificate(f.read())
134 f.close()
135 name, _ = os.path.splitext(i)
136 name, _ = os.path.splitext(name)
137 self.Add(cert, name)
138
139ALL_CERTS = CertDB()
140
141
142def ParseCertificate(data):
143 """Parse a PEM-format certificate."""
144 cert = []
145 save = False
146 for line in data.split("\n"):
147 if "--END CERTIFICATE--" in line:
148 break
149 if save:
150 cert.append(line)
151 if "--BEGIN CERTIFICATE--" in line:
152 save = True
153 cert = "".join(cert).decode('base64')
154 return cert
155
156
157def CertFromPKCS7(data, filename):
158 """Read the cert out of a PKCS#7-format file (which is what is
159 stored in a signed .apk)."""
160 Push(filename + ":")
161 try:
162 p = common.Run(["openssl", "pkcs7",
163 "-inform", "DER",
164 "-outform", "PEM",
165 "-print_certs"],
166 stdin=subprocess.PIPE,
167 stdout=subprocess.PIPE)
168 out, err = p.communicate(data)
169 if err and not err.strip():
170 AddProblem("error reading cert:\n" + err)
171 return None
172
173 cert = ParseCertificate(out)
174 if not cert:
175 AddProblem("error parsing cert output")
176 return None
177 return cert
178 finally:
179 Pop()
180
181
182class APK(object):
183 def __init__(self, full_filename, filename):
184 self.filename = filename
185 self.cert = None
186 Push(filename+":")
187 try:
188 self.RecordCert(full_filename)
189 self.ReadManifest(full_filename)
190 finally:
191 Pop()
192
193 def RecordCert(self, full_filename):
194 try:
195 f = open(full_filename)
196 apk = zipfile.ZipFile(f, "r")
197 pkcs7 = None
198 for info in apk.infolist():
199 if info.filename.startswith("META-INF/") and \
200 (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")):
201 if pkcs7 is not None:
202 AddProblem("multiple certs")
203 pkcs7 = apk.read(info.filename)
204 self.cert = CertFromPKCS7(pkcs7, info.filename)
205 ALL_CERTS.Add(self.cert)
206 if not pkcs7:
207 AddProblem("no signature")
208 finally:
209 f.close()
210
211 def ReadManifest(self, full_filename):
212 p = common.Run(["aapt", "dump", "xmltree", full_filename,
213 "AndroidManifest.xml"],
214 stdout=subprocess.PIPE)
215 manifest, err = p.communicate()
216 if err:
217 AddProblem("failed to read manifest")
218 return
219
220 self.shared_uid = None
221 self.package = None
222
223 for line in manifest.split("\n"):
224 line = line.strip()
225 m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
226 if m:
227 name = m.group(1)
228 if name == "android:sharedUserId":
229 if self.shared_uid is not None:
230 AddProblem("multiple sharedUserId declarations")
231 self.shared_uid = m.group(2)
232 elif name == "package":
233 if self.package is not None:
234 AddProblem("multiple package declarations")
235 self.package = m.group(2)
236
237 if self.package is None:
238 AddProblem("no package declaration")
239
240
241class TargetFiles(object):
242 def __init__(self):
243 self.max_pkg_len = 30
244 self.max_fn_len = 20
245
246 def LoadZipFile(self, filename):
Doug Zongker6ae53812011-01-27 10:20:27 -0800247 d, z = common.UnzipTemp(filename, '*.apk')
Doug Zongker75f17362009-12-08 13:46:44 -0800248 try:
249 self.apks = {}
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800250 self.apks_by_basename = {}
Doug Zongker75f17362009-12-08 13:46:44 -0800251 for dirpath, dirnames, filenames in os.walk(d):
252 for fn in filenames:
253 if fn.endswith(".apk"):
254 fullname = os.path.join(dirpath, fn)
255 displayname = fullname[len(d)+1:]
256 apk = APK(fullname, displayname)
257 self.apks[apk.package] = apk
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800258 self.apks_by_basename[os.path.basename(apk.filename)] = apk
Doug Zongker75f17362009-12-08 13:46:44 -0800259
260 self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
261 self.max_fn_len = max(self.max_fn_len, len(apk.filename))
262 finally:
263 shutil.rmtree(d)
264
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800265 self.certmap = common.ReadApkCerts(z)
266 z.close()
267
Doug Zongker75f17362009-12-08 13:46:44 -0800268 def CheckSharedUids(self):
269 """Look for any instances where packages signed with different
270 certs request the same sharedUserId."""
271 apks_by_uid = {}
272 for apk in self.apks.itervalues():
273 if apk.shared_uid:
274 apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
275
276 for uid in sorted(apks_by_uid.keys()):
277 apks = apks_by_uid[uid]
278 for apk in apks[1:]:
279 if apk.cert != apks[0].cert:
280 break
281 else:
282 # all the certs are the same; this uid is fine
283 continue
284
285 AddProblem("uid %s shared across multiple certs" % (uid,))
286
287 print "uid %s is shared by packages with different certs:" % (uid,)
288 x = [(i.cert, i.package, i) for i in apks]
289 x.sort()
290 lastcert = None
291 for cert, _, apk in x:
292 if cert != lastcert:
293 lastcert = cert
294 print " %s:" % (ALL_CERTS.Get(cert),)
295 print " %-*s [%s]" % (self.max_pkg_len,
296 apk.package, apk.filename)
297 print
298
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800299 def CheckExternalSignatures(self):
300 for apk_filename, certname in self.certmap.iteritems():
301 if certname == "EXTERNAL":
302 # Apps marked EXTERNAL should be signed with the test key
303 # during development, then manually re-signed after
304 # predexopting. Consider it an error if this app is now
305 # signed with any key that is present in our tree.
306 apk = self.apks_by_basename[apk_filename]
307 name = ALL_CERTS.Get(apk.cert)
308 if not name.startswith("unknown "):
309 Push(apk.filename)
310 AddProblem("hasn't been signed with EXTERNAL cert")
311 Pop()
312
Doug Zongker75f17362009-12-08 13:46:44 -0800313 def PrintCerts(self):
314 """Display a table of packages grouped by cert."""
315 by_cert = {}
316 for apk in self.apks.itervalues():
317 by_cert.setdefault(apk.cert, []).append((apk.package, apk))
318
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:
350 # in both; should have the same cert
351 if self.apks[i].cert != other.apks[i].cert:
352 by_certpair.setdefault((other.apks[i].cert,
353 self.apks[i].cert), []).append(i)
354 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()):
366 print "was", ALL_CERTS.Get(old)
367 print "now", ALL_CERTS.Get(new)
368 for i in sorted(packages):
369 old_fn = other.apks[i].filename
370 new_fn = self.apks[i].filename
371 if old_fn == new_fn:
372 print " %-*s [%s]" % (max_pkg_len, i, old_fn)
373 else:
374 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i,
375 old_fn, new_fn)
376 print
377
378
379def main(argv):
380 def option_handler(o, a):
381 if o in ("-c", "--compare_with"):
382 OPTIONS.compare_with = a
383 elif o in ("-l", "--local_cert_dirs"):
384 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
385 elif o in ("-t", "--text"):
386 OPTIONS.text = True
387 else:
388 return False
389 return True
390
391 args = common.ParseOptions(argv, __doc__,
392 extra_opts="c:l:t",
393 extra_long_opts=["compare_with=",
394 "local_cert_dirs="],
395 extra_option_handler=option_handler)
396
397 if len(args) != 1:
398 common.Usage(__doc__)
399 sys.exit(1)
400
401 ALL_CERTS.FindLocalCerts()
402
403 Push("input target_files:")
404 try:
405 target_files = TargetFiles()
406 target_files.LoadZipFile(args[0])
407 finally:
408 Pop()
409
410 compare_files = None
411 if OPTIONS.compare_with:
412 Push("comparison target_files:")
413 try:
414 compare_files = TargetFiles()
415 compare_files.LoadZipFile(OPTIONS.compare_with)
416 finally:
417 Pop()
418
419 if OPTIONS.text or not compare_files:
420 Banner("target files")
421 target_files.PrintCerts()
422 target_files.CheckSharedUids()
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800423 target_files.CheckExternalSignatures()
Doug Zongker75f17362009-12-08 13:46:44 -0800424 if compare_files:
425 if OPTIONS.text:
426 Banner("comparison files")
427 compare_files.PrintCerts()
428 target_files.CompareWith(compare_files)
429
430 if PROBLEMS:
431 print "%d problem(s) found:\n" % (len(PROBLEMS),)
432 for p in PROBLEMS:
433 print p
434 return 1
435
436 return 0
437
438
439if __name__ == '__main__':
440 try:
441 r = main(sys.argv[1:])
442 sys.exit(r)
443 except common.ExternalError, e:
444 print
445 print " ERROR: %s" % (e,)
446 print
447 sys.exit(1)