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 < 0x02040000: 45 print >> sys.stderr, "Python 2.4 or newer is required." 46 sys.exit(1) 47 48 import os 49 import re 50 import sha 51 import shutil 52 import subprocess 53 import tempfile 54 import zipfile 55 56 import 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). 63 class MyZipInfo(zipfile.ZipInfo): 64 def _decodeExtra(self): 65 pass 66 zipfile.ZipInfo = MyZipInfo 67 68 OPTIONS = common.OPTIONS 69 70 OPTIONS.text = False 71 OPTIONS.compare_with = None 72 OPTIONS.local_cert_dirs = ("vendor", "build") 73 74 PROBLEMS = [] 75 PROBLEM_PREFIX = [] 76 77 def AddProblem(msg): 78 PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg) 79 def Push(msg): 80 PROBLEM_PREFIX.append(msg) 81 def Pop(): 82 PROBLEM_PREFIX.pop() 83 84 85 def Banner(msg): 86 print "-" * 70 87 print " ", msg 88 print "-" * 70 89 90 91 def 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 105 class 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 140 ALL_CERTS = CertDB() 141 142 143 def 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 158 def 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 183 class 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 242 class 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 self.apks_by_basename = {} 252 for dirpath, dirnames, filenames in os.walk(d): 253 for fn in filenames: 254 if fn.endswith(".apk"): 255 fullname = os.path.join(dirpath, fn) 256 displayname = fullname[len(d)+1:] 257 apk = APK(fullname, displayname) 258 self.apks[apk.package] = apk 259 self.apks_by_basename[os.path.basename(apk.filename)] = apk 260 261 self.max_pkg_len = max(self.max_pkg_len, len(apk.package)) 262 self.max_fn_len = max(self.max_fn_len, len(apk.filename)) 263 finally: 264 shutil.rmtree(d) 265 266 z = zipfile.ZipFile(open(filename, "rb")) 267 self.certmap = common.ReadApkCerts(z) 268 z.close() 269 270 def CheckSharedUids(self): 271 """Look for any instances where packages signed with different 272 certs request the same sharedUserId.""" 273 apks_by_uid = {} 274 for apk in self.apks.itervalues(): 275 if apk.shared_uid: 276 apks_by_uid.setdefault(apk.shared_uid, []).append(apk) 277 278 for uid in sorted(apks_by_uid.keys()): 279 apks = apks_by_uid[uid] 280 for apk in apks[1:]: 281 if apk.cert != apks[0].cert: 282 break 283 else: 284 # all the certs are the same; this uid is fine 285 continue 286 287 AddProblem("uid %s shared across multiple certs" % (uid,)) 288 289 print "uid %s is shared by packages with different certs:" % (uid,) 290 x = [(i.cert, i.package, i) for i in apks] 291 x.sort() 292 lastcert = None 293 for cert, _, apk in x: 294 if cert != lastcert: 295 lastcert = cert 296 print " %s:" % (ALL_CERTS.Get(cert),) 297 print " %-*s [%s]" % (self.max_pkg_len, 298 apk.package, apk.filename) 299 print 300 301 def CheckExternalSignatures(self): 302 for apk_filename, certname in self.certmap.iteritems(): 303 if certname == "EXTERNAL": 304 # Apps marked EXTERNAL should be signed with the test key 305 # during development, then manually re-signed after 306 # predexopting. Consider it an error if this app is now 307 # signed with any key that is present in our tree. 308 apk = self.apks_by_basename[apk_filename] 309 name = ALL_CERTS.Get(apk.cert) 310 if not name.startswith("unknown "): 311 Push(apk.filename) 312 AddProblem("hasn't been signed with EXTERNAL cert") 313 Pop() 314 315 def PrintCerts(self): 316 """Display a table of packages grouped by cert.""" 317 by_cert = {} 318 for apk in self.apks.itervalues(): 319 by_cert.setdefault(apk.cert, []).append((apk.package, apk)) 320 321 order = [(-len(v), k) for (k, v) in by_cert.iteritems()] 322 order.sort() 323 324 for _, cert in order: 325 print "%s:" % (ALL_CERTS.Get(cert),) 326 apks = by_cert[cert] 327 apks.sort() 328 for _, apk in apks: 329 if apk.shared_uid: 330 print " %-*s %-*s [%s]" % (self.max_fn_len, apk.filename, 331 self.max_pkg_len, apk.package, 332 apk.shared_uid) 333 else: 334 print " %-*s %-*s" % (self.max_fn_len, apk.filename, 335 self.max_pkg_len, apk.package) 336 print 337 338 def CompareWith(self, other): 339 """Look for instances where a given package that exists in both 340 self and other have different certs.""" 341 342 all = set(self.apks.keys()) 343 all.update(other.apks.keys()) 344 345 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len) 346 347 by_certpair = {} 348 349 for i in all: 350 if i in self.apks: 351 if i in other.apks: 352 # in both; should have the same cert 353 if self.apks[i].cert != other.apks[i].cert: 354 by_certpair.setdefault((other.apks[i].cert, 355 self.apks[i].cert), []).append(i) 356 else: 357 print "%s [%s]: new APK (not in comparison target_files)" % ( 358 i, self.apks[i].filename) 359 else: 360 if i in other.apks: 361 print "%s [%s]: removed APK (only in comparison target_files)" % ( 362 i, other.apks[i].filename) 363 364 if by_certpair: 365 AddProblem("some APKs changed certs") 366 Banner("APK signing differences") 367 for (old, new), packages in sorted(by_certpair.items()): 368 print "was", ALL_CERTS.Get(old) 369 print "now", ALL_CERTS.Get(new) 370 for i in sorted(packages): 371 old_fn = other.apks[i].filename 372 new_fn = self.apks[i].filename 373 if old_fn == new_fn: 374 print " %-*s [%s]" % (max_pkg_len, i, old_fn) 375 else: 376 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i, 377 old_fn, new_fn) 378 print 379 380 381 def main(argv): 382 def option_handler(o, a): 383 if o in ("-c", "--compare_with"): 384 OPTIONS.compare_with = a 385 elif o in ("-l", "--local_cert_dirs"): 386 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")] 387 elif o in ("-t", "--text"): 388 OPTIONS.text = True 389 else: 390 return False 391 return True 392 393 args = common.ParseOptions(argv, __doc__, 394 extra_opts="c:l:t", 395 extra_long_opts=["compare_with=", 396 "local_cert_dirs="], 397 extra_option_handler=option_handler) 398 399 if len(args) != 1: 400 common.Usage(__doc__) 401 sys.exit(1) 402 403 ALL_CERTS.FindLocalCerts() 404 405 Push("input target_files:") 406 try: 407 target_files = TargetFiles() 408 target_files.LoadZipFile(args[0]) 409 finally: 410 Pop() 411 412 compare_files = None 413 if OPTIONS.compare_with: 414 Push("comparison target_files:") 415 try: 416 compare_files = TargetFiles() 417 compare_files.LoadZipFile(OPTIONS.compare_with) 418 finally: 419 Pop() 420 421 if OPTIONS.text or not compare_files: 422 Banner("target files") 423 target_files.PrintCerts() 424 target_files.CheckSharedUids() 425 target_files.CheckExternalSignatures() 426 if compare_files: 427 if OPTIONS.text: 428 Banner("comparison files") 429 compare_files.PrintCerts() 430 target_files.CompareWith(compare_files) 431 432 if PROBLEMS: 433 print "%d problem(s) found:\n" % (len(PROBLEMS),) 434 for p in PROBLEMS: 435 print p 436 return 1 437 438 return 0 439 440 441 if __name__ == '__main__': 442 try: 443 r = main(sys.argv[1:]) 444 sys.exit(r) 445 except common.ExternalError, e: 446 print 447 print " ERROR: %s" % (e,) 448 print 449 sys.exit(1) 450