1 #!/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 """ 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. 22 23 Usage: 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 42 import sys 43 44 if sys.hexversion < 0x02070000: 45 print >> sys.stderr, "Python 2.7 or newer is required." 46 sys.exit(1) 47 48 import os 49 import re 50 import shutil 51 import subprocess 52 import zipfile 53 54 import 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). 61 class MyZipInfo(zipfile.ZipInfo): 62 def _decodeExtra(self): 63 pass 64 zipfile.ZipInfo = MyZipInfo 65 66 OPTIONS = common.OPTIONS 67 68 OPTIONS.text = False 69 OPTIONS.compare_with = None 70 OPTIONS.local_cert_dirs = ("vendor", "build") 71 72 PROBLEMS = [] 73 PROBLEM_PREFIX = [] 74 75 def AddProblem(msg): 76 PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg) 77 def Push(msg): 78 PROBLEM_PREFIX.append(msg) 79 def Pop(): 80 PROBLEM_PREFIX.pop() 81 82 83 def Banner(msg): 84 print "-" * 70 85 print " ", msg 86 print "-" * 70 87 88 89 def 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 103 class 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: 113 name = "unknown cert %s (%s)" % (common.sha1(cert).hexdigest()[:12], 114 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: 124 for dirpath, _, filenames in os.walk(top): 125 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) 132 cert = common.ParseCertificate(f.read()) 133 f.close() 134 name, _ = os.path.splitext(i) 135 name, _ = os.path.splitext(name) 136 self.Add(cert, name) 137 138 ALL_CERTS = CertDB() 139 140 141 def 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 157 cert = common.ParseCertificate(out) 158 if not cert: 159 AddProblem("error parsing cert output") 160 return None 161 return cert 162 finally: 163 Pop() 164 165 166 class APK(object): 167 def __init__(self, full_filename, filename): 168 self.filename = filename 169 self.certs = None 170 self.shared_uid = None 171 self.package = None 172 173 Push(filename+":") 174 try: 175 self.RecordCerts(full_filename) 176 self.ReadManifest(full_filename) 177 finally: 178 Pop() 179 180 def RecordCerts(self, full_filename): 181 out = set() 182 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")): 189 pkcs7 = apk.read(info.filename) 190 cert = CertFromPKCS7(pkcs7, info.filename) 191 out.add(cert) 192 ALL_CERTS.Add(cert) 193 if not pkcs7: 194 AddProblem("no signature") 195 finally: 196 f.close() 197 self.certs = frozenset(out) 198 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() 213 m = re.search(r'A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line) 214 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 229 class TargetFiles(object): 230 def __init__(self): 231 self.max_pkg_len = 30 232 self.max_fn_len = 20 233 self.apks = None 234 self.apks_by_basename = None 235 self.certmap = None 236 237 def LoadZipFile(self, filename): 238 d, z = common.UnzipTemp(filename, '*.apk') 239 try: 240 self.apks = {} 241 self.apks_by_basename = {} 242 for dirpath, _, filenames in os.walk(d): 243 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) 248 self.apks[apk.package] = apk 249 self.apks_by_basename[os.path.basename(apk.filename)] = apk 250 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 256 self.certmap = common.ReadApkCerts(z) 257 z.close() 258 259 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:]: 270 if apk.certs != apks[0].certs: 271 break 272 else: 273 # all packages have the same set of certs; this uid is fine. 274 continue 275 276 AddProblem("different cert sets for packages with uid %s" % (uid,)) 277 278 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) 283 print 284 285 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 299 def PrintCerts(self): 300 """Display a table of packages grouped by cert.""" 301 by_cert = {} 302 for apk in self.apks.itervalues(): 303 for cert in apk.certs: 304 by_cert.setdefault(cert, []).append((apk.package, apk)) 305 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: 319 print " %-*s %-*s" % (self.max_fn_len, apk.filename, 320 self.max_pkg_len, apk.package) 321 print 322 323 def CompareWith(self, other): 324 """Look for instances where a given package that exists in both 325 self and other have different certs.""" 326 327 all_apks = set(self.apks.keys()) 328 all_apks.update(other.apks.keys()) 329 330 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len) 331 332 by_certpair = {} 333 334 for i in all_apks: 335 if i in self.apks: 336 if i in other.apks: 337 # in both; should have same set of certs 338 if self.apks[i].certs != other.apks[i].certs: 339 by_certpair.setdefault((other.apks[i].certs, 340 self.apks[i].certs), []).append(i) 341 else: 342 print "%s [%s]: new APK (not in comparison target_files)" % ( 343 i, self.apks[i].filename) 344 else: 345 if i in other.apks: 346 print "%s [%s]: removed APK (only in comparison target_files)" % ( 347 i, other.apks[i].filename) 348 349 if by_certpair: 350 AddProblem("some APKs changed certs") 351 Banner("APK signing differences") 352 for (old, new), packages in sorted(by_certpair.items()): 353 for i, o in enumerate(old): 354 if i == 0: 355 print "was", ALL_CERTS.Get(o) 356 else: 357 print " ", ALL_CERTS.Get(o) 358 for i, n in enumerate(new): 359 if i == 0: 360 print "now", ALL_CERTS.Get(n) 361 else: 362 print " ", ALL_CERTS.Get(n) 363 for i in sorted(packages): 364 old_fn = other.apks[i].filename 365 new_fn = self.apks[i].filename 366 if old_fn == new_fn: 367 print " %-*s [%s]" % (max_pkg_len, i, old_fn) 368 else: 369 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i, 370 old_fn, new_fn) 371 print 372 373 374 def main(argv): 375 def option_handler(o, a): 376 if o in ("-c", "--compare_with"): 377 OPTIONS.compare_with = a 378 elif o in ("-l", "--local_cert_dirs"): 379 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")] 380 elif o in ("-t", "--text"): 381 OPTIONS.text = True 382 else: 383 return False 384 return True 385 386 args = common.ParseOptions(argv, __doc__, 387 extra_opts="c:l:t", 388 extra_long_opts=["compare_with=", 389 "local_cert_dirs="], 390 extra_option_handler=option_handler) 391 392 if len(args) != 1: 393 common.Usage(__doc__) 394 sys.exit(1) 395 396 ALL_CERTS.FindLocalCerts() 397 398 Push("input target_files:") 399 try: 400 target_files = TargetFiles() 401 target_files.LoadZipFile(args[0]) 402 finally: 403 Pop() 404 405 compare_files = None 406 if OPTIONS.compare_with: 407 Push("comparison target_files:") 408 try: 409 compare_files = TargetFiles() 410 compare_files.LoadZipFile(OPTIONS.compare_with) 411 finally: 412 Pop() 413 414 if OPTIONS.text or not compare_files: 415 Banner("target files") 416 target_files.PrintCerts() 417 target_files.CheckSharedUids() 418 target_files.CheckExternalSignatures() 419 if compare_files: 420 if OPTIONS.text: 421 Banner("comparison files") 422 compare_files.PrintCerts() 423 target_files.CompareWith(compare_files) 424 425 if PROBLEMS: 426 print "%d problem(s) found:\n" % (len(PROBLEMS),) 427 for p in PROBLEMS: 428 print p 429 return 1 430 431 return 0 432 433 434 if __name__ == '__main__': 435 try: 436 r = main(sys.argv[1:]) 437 sys.exit(r) 438 except common.ExternalError as e: 439 print 440 print " ERROR: %s" % (e,) 441 print 442 sys.exit(1) 443