blob: b91f3d4c08c718216b9e45dcc1868fb8a7f5d6af [file] [log] [blame]
Doug Zongker27bb6f52009-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
50import sha
51import shutil
52import subprocess
53import tempfile
54import zipfile
55
56import common
57
58# Work around a bug in python's zipfile module that prevents opening
59# of zipfiles if any entry has an extra field of between 1 and 3 bytes
60# (which is common with zipaligned APKs). This overrides the
61# ZipInfo._decodeExtra() method (which contains the bug) with an empty
62# version (since we don't need to decode the extra field anyway).
63class 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:
115 name = "unknown cert %s (%s)" % (sha.sha(cert).hexdigest()[:12],
116 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:
126 for dirpath, dirnames, filenames in os.walk(top):
127 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)
134 cert = ParseCertificate(f.read())
135 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
143def ParseCertificate(data):
144 """Parse a PEM-format certificate."""
145 cert = []
146 save = False
147 for line in data.split("\n"):
148 if "--END CERTIFICATE--" in line:
149 break
150 if save:
151 cert.append(line)
152 if "--BEGIN CERTIFICATE--" in line:
153 save = True
154 cert = "".join(cert).decode('base64')
155 return cert
156
157
158def CertFromPKCS7(data, filename):
159 """Read the cert out of a PKCS#7-format file (which is what is
160 stored in a signed .apk)."""
161 Push(filename + ":")
162 try:
163 p = common.Run(["openssl", "pkcs7",
164 "-inform", "DER",
165 "-outform", "PEM",
166 "-print_certs"],
167 stdin=subprocess.PIPE,
168 stdout=subprocess.PIPE)
169 out, err = p.communicate(data)
170 if err and not err.strip():
171 AddProblem("error reading cert:\n" + err)
172 return None
173
174 cert = ParseCertificate(out)
175 if not cert:
176 AddProblem("error parsing cert output")
177 return None
178 return cert
179 finally:
180 Pop()
181
182
183class APK(object):
184 def __init__(self, full_filename, filename):
185 self.filename = filename
186 self.cert = None
187 Push(filename+":")
188 try:
189 self.RecordCert(full_filename)
190 self.ReadManifest(full_filename)
191 finally:
192 Pop()
193
194 def RecordCert(self, full_filename):
195 try:
196 f = open(full_filename)
197 apk = zipfile.ZipFile(f, "r")
198 pkcs7 = None
199 for info in apk.infolist():
200 if info.filename.startswith("META-INF/") and \
201 (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")):
202 if pkcs7 is not None:
203 AddProblem("multiple certs")
204 pkcs7 = apk.read(info.filename)
205 self.cert = CertFromPKCS7(pkcs7, info.filename)
206 ALL_CERTS.Add(self.cert)
207 if not pkcs7:
208 AddProblem("no signature")
209 finally:
210 f.close()
211
212 def ReadManifest(self, full_filename):
213 p = common.Run(["aapt", "dump", "xmltree", full_filename,
214 "AndroidManifest.xml"],
215 stdout=subprocess.PIPE)
216 manifest, err = p.communicate()
217 if err:
218 AddProblem("failed to read manifest")
219 return
220
221 self.shared_uid = None
222 self.package = None
223
224 for line in manifest.split("\n"):
225 line = line.strip()
226 m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
227 if m:
228 name = m.group(1)
229 if name == "android:sharedUserId":
230 if self.shared_uid is not None:
231 AddProblem("multiple sharedUserId declarations")
232 self.shared_uid = m.group(2)
233 elif name == "package":
234 if self.package is not None:
235 AddProblem("multiple package declarations")
236 self.package = m.group(2)
237
238 if self.package is None:
239 AddProblem("no package declaration")
240
241
242class TargetFiles(object):
243 def __init__(self):
244 self.max_pkg_len = 30
245 self.max_fn_len = 20
246
247 def LoadZipFile(self, filename):
248 d = common.UnzipTemp(filename, '*.apk')
249 try:
250 self.apks = {}
251 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
258
259 self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
260 self.max_fn_len = max(self.max_fn_len, len(apk.filename))
261 finally:
262 shutil.rmtree(d)
263
264 def CheckSharedUids(self):
265 """Look for any instances where packages signed with different
266 certs request the same sharedUserId."""
267 apks_by_uid = {}
268 for apk in self.apks.itervalues():
269 if apk.shared_uid:
270 apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
271
272 for uid in sorted(apks_by_uid.keys()):
273 apks = apks_by_uid[uid]
274 for apk in apks[1:]:
275 if apk.cert != apks[0].cert:
276 break
277 else:
278 # all the certs are the same; this uid is fine
279 continue
280
281 AddProblem("uid %s shared across multiple certs" % (uid,))
282
283 print "uid %s is shared by packages with different certs:" % (uid,)
284 x = [(i.cert, i.package, i) for i in apks]
285 x.sort()
286 lastcert = None
287 for cert, _, apk in x:
288 if cert != lastcert:
289 lastcert = cert
290 print " %s:" % (ALL_CERTS.Get(cert),)
291 print " %-*s [%s]" % (self.max_pkg_len,
292 apk.package, apk.filename)
293 print
294
295 def PrintCerts(self):
296 """Display a table of packages grouped by cert."""
297 by_cert = {}
298 for apk in self.apks.itervalues():
299 by_cert.setdefault(apk.cert, []).append((apk.package, apk))
300
301 order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
302 order.sort()
303
304 for _, cert in order:
305 print "%s:" % (ALL_CERTS.Get(cert),)
306 apks = by_cert[cert]
307 apks.sort()
308 for _, apk in apks:
309 if apk.shared_uid:
310 print " %-*s %-*s [%s]" % (self.max_fn_len, apk.filename,
311 self.max_pkg_len, apk.package,
312 apk.shared_uid)
313 else:
314 print " %-*s %-*s" % (self.max_fn_len, apk.filename,
315 self.max_pkg_len, apk.package)
316 print
317
318 def CompareWith(self, other):
319 """Look for instances where a given package that exists in both
320 self and other have different certs."""
321
322 all = set(self.apks.keys())
323 all.update(other.apks.keys())
324
325 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
326
327 by_certpair = {}
328
329 for i in all:
330 if i in self.apks:
331 if i in other.apks:
332 # in both; should have the same cert
333 if self.apks[i].cert != other.apks[i].cert:
334 by_certpair.setdefault((other.apks[i].cert,
335 self.apks[i].cert), []).append(i)
336 else:
337 print "%s [%s]: new APK (not in comparison target_files)" % (
338 i, self.apks[i].filename)
339 else:
340 if i in other.apks:
341 print "%s [%s]: removed APK (only in comparison target_files)" % (
342 i, other.apks[i].filename)
343
344 if by_certpair:
345 AddProblem("some APKs changed certs")
346 Banner("APK signing differences")
347 for (old, new), packages in sorted(by_certpair.items()):
348 print "was", ALL_CERTS.Get(old)
349 print "now", ALL_CERTS.Get(new)
350 for i in sorted(packages):
351 old_fn = other.apks[i].filename
352 new_fn = self.apks[i].filename
353 if old_fn == new_fn:
354 print " %-*s [%s]" % (max_pkg_len, i, old_fn)
355 else:
356 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i,
357 old_fn, new_fn)
358 print
359
360
361def main(argv):
362 def option_handler(o, a):
363 if o in ("-c", "--compare_with"):
364 OPTIONS.compare_with = a
365 elif o in ("-l", "--local_cert_dirs"):
366 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
367 elif o in ("-t", "--text"):
368 OPTIONS.text = True
369 else:
370 return False
371 return True
372
373 args = common.ParseOptions(argv, __doc__,
374 extra_opts="c:l:t",
375 extra_long_opts=["compare_with=",
376 "local_cert_dirs="],
377 extra_option_handler=option_handler)
378
379 if len(args) != 1:
380 common.Usage(__doc__)
381 sys.exit(1)
382
383 ALL_CERTS.FindLocalCerts()
384
385 Push("input target_files:")
386 try:
387 target_files = TargetFiles()
388 target_files.LoadZipFile(args[0])
389 finally:
390 Pop()
391
392 compare_files = None
393 if OPTIONS.compare_with:
394 Push("comparison target_files:")
395 try:
396 compare_files = TargetFiles()
397 compare_files.LoadZipFile(OPTIONS.compare_with)
398 finally:
399 Pop()
400
401 if OPTIONS.text or not compare_files:
402 Banner("target files")
403 target_files.PrintCerts()
404 target_files.CheckSharedUids()
405 if compare_files:
406 if OPTIONS.text:
407 Banner("comparison files")
408 compare_files.PrintCerts()
409 target_files.CompareWith(compare_files)
410
411 if PROBLEMS:
412 print "%d problem(s) found:\n" % (len(PROBLEMS),)
413 for p in PROBLEMS:
414 print p
415 return 1
416
417 return 0
418
419
420if __name__ == '__main__':
421 try:
422 r = main(sys.argv[1:])
423 sys.exit(r)
424 except common.ExternalError, e:
425 print
426 print " ERROR: %s" % (e,)
427 print
428 sys.exit(1)