3 # Copyright (C) 2009 The Android Open Source Project
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
18 Check 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
20 target_files (usually a previously distributed build for the same
21 device) and flag any changes.
23 Usage: check_target_file_signatures [flags] target_files
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).
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.
37 Dump the certificate information for both packages in comparison
38 mode (this output is normally suppressed).
44 if sys.hexversion < 0x02040000:
45 print >> sys.stderr, "Python 2.4 or newer is required."
56 from hashlib import sha1 as sha1
58 from sha import sha as sha1
62 # Work around a bug in python's zipfile module that prevents opening
63 # of zipfiles if any entry has an extra field of between 1 and 3 bytes
64 # (which is common with zipaligned APKs). This overrides the
65 # ZipInfo._decodeExtra() method (which contains the bug) with an empty
66 # version (since we don't need to decode the extra field anyway).
67 class MyZipInfo(zipfile.ZipInfo):
68 def _decodeExtra(self):
70 zipfile.ZipInfo = MyZipInfo
72 OPTIONS = common.OPTIONS
75 OPTIONS.compare_with = None
76 OPTIONS.local_cert_dirs = ("vendor", "build")
82 PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
84 PROBLEM_PREFIX.append(msg)
95 def GetCertSubject(cert):
96 p = common.Run(["openssl", "x509", "-inform", "DER", "-text"],
97 stdin=subprocess.PIPE,
98 stdout=subprocess.PIPE)
99 out, err = p.communicate(cert)
100 if err and not err.strip():
101 return "(error reading cert subject)"
102 for line in out.split("\n"):
104 if line.startswith("Subject:"):
105 return line[8:].strip()
106 return "(unknown cert subject)"
109 class CertDB(object):
113 def Add(self, cert, name=None):
114 if cert in self.certs:
116 self.certs[cert] = self.certs[cert] + "," + name
119 name = "unknown cert %s (%s)" % (common.sha1(cert).hexdigest()[:12],
120 GetCertSubject(cert))
121 self.certs[cert] = name
124 """Return the name for a given cert."""
125 return self.certs.get(cert, None)
127 def FindLocalCerts(self):
129 for top in OPTIONS.local_cert_dirs:
130 for dirpath, dirnames, filenames in os.walk(top):
131 certs = [os.path.join(dirpath, i)
132 for i in filenames if i.endswith(".x509.pem")]
134 to_load.extend(certs)
138 cert = common.ParseCertificate(f.read())
140 name, _ = os.path.splitext(i)
141 name, _ = os.path.splitext(name)
147 def CertFromPKCS7(data, filename):
148 """Read the cert out of a PKCS#7-format file (which is what is
149 stored in a signed .apk)."""
152 p = common.Run(["openssl", "pkcs7",
156 stdin=subprocess.PIPE,
157 stdout=subprocess.PIPE)
158 out, err = p.communicate(data)
159 if err and not err.strip():
160 AddProblem("error reading cert:\n" + err)
163 cert = common.ParseCertificate(out)
165 AddProblem("error parsing cert output")
173 def __init__(self, full_filename, filename):
174 self.filename = filename
177 self.RecordCerts(full_filename)
178 self.ReadManifest(full_filename)
182 def RecordCerts(self, full_filename):
185 f = open(full_filename)
186 apk = zipfile.ZipFile(f, "r")
188 for info in apk.infolist():
189 if info.filename.startswith("META-INF/") and \
190 (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")):
191 pkcs7 = apk.read(info.filename)
192 cert = CertFromPKCS7(pkcs7, info.filename)
196 AddProblem("no signature")
199 self.certs = frozenset(out)
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()
207 AddProblem("failed to read manifest")
210 self.shared_uid = None
213 for line in manifest.split("\n"):
215 m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
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)
227 if self.package is None:
228 AddProblem("no package declaration")
231 class TargetFiles(object):
233 self.max_pkg_len = 30
236 def LoadZipFile(self, filename):
237 d, z = common.UnzipTemp(filename, '*.apk')
240 self.apks_by_basename = {}
241 for dirpath, dirnames, filenames in os.walk(d):
243 if fn.endswith(".apk"):
244 fullname = os.path.join(dirpath, fn)
245 displayname = fullname[len(d)+1:]
246 apk = APK(fullname, displayname)
247 self.apks[apk.package] = apk
248 self.apks_by_basename[os.path.basename(apk.filename)] = apk
250 self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
251 self.max_fn_len = max(self.max_fn_len, len(apk.filename))
255 self.certmap = common.ReadApkCerts(z)
258 def CheckSharedUids(self):
259 """Look for any instances where packages signed with different
260 certs request the same sharedUserId."""
262 for apk in self.apks.itervalues():
264 apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
266 for uid in sorted(apks_by_uid.keys()):
267 apks = apks_by_uid[uid]
269 if apk.certs != apks[0].certs:
272 # all packages have the same set of certs; this uid is fine.
275 AddProblem("different cert sets for packages with uid %s" % (uid,))
277 print "uid %s is shared by packages with different cert sets:" % (uid,)
279 print "%-*s [%s]" % (self.max_pkg_len, apk.package, apk.filename)
280 for cert in apk.certs:
281 print " ", ALL_CERTS.Get(cert)
284 def CheckExternalSignatures(self):
285 for apk_filename, certname in self.certmap.iteritems():
286 if certname == "EXTERNAL":
287 # Apps marked EXTERNAL should be signed with the test key
288 # during development, then manually re-signed after
289 # predexopting. Consider it an error if this app is now
290 # signed with any key that is present in our tree.
291 apk = self.apks_by_basename[apk_filename]
292 name = ALL_CERTS.Get(apk.cert)
293 if not name.startswith("unknown "):
295 AddProblem("hasn't been signed with EXTERNAL cert")
298 def PrintCerts(self):
299 """Display a table of packages grouped by cert."""
301 for apk in self.apks.itervalues():
302 for cert in apk.certs:
303 by_cert.setdefault(cert, []).append((apk.package, apk))
305 order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
308 for _, cert in order:
309 print "%s:" % (ALL_CERTS.Get(cert),)
314 print " %-*s %-*s [%s]" % (self.max_fn_len, apk.filename,
315 self.max_pkg_len, apk.package,
318 print " %-*s %-*s" % (self.max_fn_len, apk.filename,
319 self.max_pkg_len, apk.package)
322 def CompareWith(self, other):
323 """Look for instances where a given package that exists in both
324 self and other have different certs."""
326 all = set(self.apks.keys())
327 all.update(other.apks.keys())
329 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
336 # in both; should have same set of certs
337 if self.apks[i].certs != other.apks[i].certs:
338 by_certpair.setdefault((other.apks[i].certs,
339 self.apks[i].certs), []).append(i)
341 print "%s [%s]: new APK (not in comparison target_files)" % (
342 i, self.apks[i].filename)
345 print "%s [%s]: removed APK (only in comparison target_files)" % (
346 i, other.apks[i].filename)
349 AddProblem("some APKs changed certs")
350 Banner("APK signing differences")
351 for (old, new), packages in sorted(by_certpair.items()):
352 for i, o in enumerate(old):
354 print "was", ALL_CERTS.Get(o)
356 print " ", ALL_CERTS.Get(o)
357 for i, n in enumerate(new):
359 print "now", ALL_CERTS.Get(n)
361 print " ", ALL_CERTS.Get(n)
362 for i in sorted(packages):
363 old_fn = other.apks[i].filename
364 new_fn = self.apks[i].filename
366 print " %-*s [%s]" % (max_pkg_len, i, old_fn)
368 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i,
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"):
385 args = common.ParseOptions(argv, __doc__,
387 extra_long_opts=["compare_with=",
389 extra_option_handler=option_handler)
392 common.Usage(__doc__)
395 ALL_CERTS.FindLocalCerts()
397 Push("input target_files:")
399 target_files = TargetFiles()
400 target_files.LoadZipFile(args[0])
405 if OPTIONS.compare_with:
406 Push("comparison target_files:")
408 compare_files = TargetFiles()
409 compare_files.LoadZipFile(OPTIONS.compare_with)
413 if OPTIONS.text or not compare_files:
414 Banner("target files")
415 target_files.PrintCerts()
416 target_files.CheckSharedUids()
417 target_files.CheckExternalSignatures()
420 Banner("comparison files")
421 compare_files.PrintCerts()
422 target_files.CompareWith(compare_files)
425 print "%d problem(s) found:\n" % (len(PROBLEMS),)
433 if __name__ == '__main__':
435 r = main(sys.argv[1:])
437 except common.ExternalError, e:
439 print " ERROR: %s" % (e,)