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 shutil 51 import subprocess 52 import tempfile 53 import zipfile 54 55 try: 56 from hashlib import sha1 as sha1 57 except ImportError: 58 from sha import sha as sha1 59 60 import common 61 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): 69 pass 70 zipfile.ZipInfo = MyZipInfo 71 72 OPTIONS = common.OPTIONS 73 74 OPTIONS.text = False 75 OPTIONS.compare_with = None 76 OPTIONS.local_cert_dirs = ("vendor", "build") 77 78 PROBLEMS = [] 79 PROBLEM_PREFIX = [] 80 81 def AddProblem(msg): 82 PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg) 83 def Push(msg): 84 PROBLEM_PREFIX.append(msg) 85 def Pop(): 86 PROBLEM_PREFIX.pop() 87 88 89 def Banner(msg): 90 print "-" * 70 91 print " ", msg 92 print "-" * 70 93 94 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"): 103 line = line.strip() 104 if line.startswith("Subject:"): 105 return line[8:].strip() 106 return "(unknown cert subject)" 107 108 109 class CertDB(object): 110 def __init__(self): 111 self.certs = {} 112 113 def Add(self, cert, name=None): 114 if cert in self.certs: 115 if name: 116 self.certs[cert] = self.certs[cert] + "," + name 117 else: 118 if name is None: 119 name = "unknown cert %s (%s)" % (common.sha1(cert).hexdigest()[:12], 120 GetCertSubject(cert)) 121 self.certs[cert] = name 122 123 def Get(self, cert): 124 """Return the name for a given cert.""" 125 return self.certs.get(cert, None) 126 127 def FindLocalCerts(self): 128 to_load = [] 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")] 133 if certs: 134 to_load.extend(certs) 135 136 for i in to_load: 137 f = open(i) 138 cert = ParseCertificate(f.read()) 139 f.close() 140 name, _ = os.path.splitext(i) 141 name, _ = os.path.splitext(name) 142 self.Add(cert, name) 143 144 ALL_CERTS = CertDB() 145 146 147 def ParseCertificate(data): 148 """Parse a PEM-format certificate.""" 149 cert = [] 150 save = False 151 for line in data.split("\n"): 152 if "--END CERTIFICATE--" in line: 153 break 154 if save: 155 cert.append(line) 156 if "--BEGIN CERTIFICATE--" in line: 157 save = True 158 cert = "".join(cert).decode('base64') 159 return cert 160 161 162 def CertFromPKCS7(data, filename): 163 """Read the cert out of a PKCS#7-format file (which is what is 164 stored in a signed .apk).""" 165 Push(filename + ":") 166 try: 167 p = common.Run(["openssl", "pkcs7", 168 "-inform", "DER", 169 "-outform", "PEM", 170 "-print_certs"], 171 stdin=subprocess.PIPE, 172 stdout=subprocess.PIPE) 173 out, err = p.communicate(data) 174 if err and not err.strip(): 175 AddProblem("error reading cert:\n" + err) 176 return None 177 178 cert = ParseCertificate(out) 179 if not cert: 180 AddProblem("error parsing cert output") 181 return None 182 return cert 183 finally: 184 Pop() 185 186 187 class APK(object): 188 def __init__(self, full_filename, filename): 189 self.filename = filename 190 Push(filename+":") 191 try: 192 self.RecordCerts(full_filename) 193 self.ReadManifest(full_filename) 194 finally: 195 Pop() 196 197 def RecordCerts(self, full_filename): 198 out = set() 199 try: 200 f = open(full_filename) 201 apk = zipfile.ZipFile(f, "r") 202 pkcs7 = None 203 for info in apk.infolist(): 204 if info.filename.startswith("META-INF/") and \ 205 (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")): 206 pkcs7 = apk.read(info.filename) 207 cert = CertFromPKCS7(pkcs7, info.filename) 208 out.add(cert) 209 ALL_CERTS.Add(cert) 210 if not pkcs7: 211 AddProblem("no signature") 212 finally: 213 f.close() 214 self.certs = frozenset(out) 215 216 def ReadManifest(self, full_filename): 217 p = common.Run(["aapt", "dump", "xmltree", full_filename, 218 "AndroidManifest.xml"], 219 stdout=subprocess.PIPE) 220 manifest, err = p.communicate() 221 if err: 222 AddProblem("failed to read manifest") 223 return 224 225 self.shared_uid = None 226 self.package = None 227 228 for line in manifest.split("\n"): 229 line = line.strip() 230 m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line) 231 if m: 232 name = m.group(1) 233 if name == "android:sharedUserId": 234 if self.shared_uid is not None: 235 AddProblem("multiple sharedUserId declarations") 236 self.shared_uid = m.group(2) 237 elif name == "package": 238 if self.package is not None: 239 AddProblem("multiple package declarations") 240 self.package = m.group(2) 241 242 if self.package is None: 243 AddProblem("no package declaration") 244 245 246 class TargetFiles(object): 247 def __init__(self): 248 self.max_pkg_len = 30 249 self.max_fn_len = 20 250 251 def LoadZipFile(self, filename): 252 d, z = common.UnzipTemp(filename, '*.apk') 253 try: 254 self.apks = {} 255 self.apks_by_basename = {} 256 for dirpath, dirnames, filenames in os.walk(d): 257 for fn in filenames: 258 if fn.endswith(".apk"): 259 fullname = os.path.join(dirpath, fn) 260 displayname = fullname[len(d)+1:] 261 apk = APK(fullname, displayname) 262 self.apks[apk.package] = apk 263 self.apks_by_basename[os.path.basename(apk.filename)] = apk 264 265 self.max_pkg_len = max(self.max_pkg_len, len(apk.package)) 266 self.max_fn_len = max(self.max_fn_len, len(apk.filename)) 267 finally: 268 shutil.rmtree(d) 269 270 self.certmap = common.ReadApkCerts(z) 271 z.close() 272 273 def CheckSharedUids(self): 274 """Look for any instances where packages signed with different 275 certs request the same sharedUserId.""" 276 apks_by_uid = {} 277 for apk in self.apks.itervalues(): 278 if apk.shared_uid: 279 apks_by_uid.setdefault(apk.shared_uid, []).append(apk) 280 281 for uid in sorted(apks_by_uid.keys()): 282 apks = apks_by_uid[uid] 283 for apk in apks[1:]: 284 if apk.certs != apks[0].certs: 285 break 286 else: 287 # all packages have the same set of certs; this uid is fine. 288 continue 289 290 AddProblem("different cert sets for packages with uid %s" % (uid,)) 291 292 print "uid %s is shared by packages with different cert sets:" % (uid,) 293 for apk in apks: 294 print "%-*s [%s]" % (self.max_pkg_len, apk.package, apk.filename) 295 for cert in apk.certs: 296 print " ", ALL_CERTS.Get(cert) 297 print 298 299 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 313 def PrintCerts(self): 314 """Display a table of packages grouped by cert.""" 315 by_cert = {} 316 for apk in self.apks.itervalues(): 317 for cert in apk.certs: 318 by_cert.setdefault(cert, []).append((apk.package, apk)) 319 320 order = [(-len(v), k) for (k, v) in by_cert.iteritems()] 321 order.sort() 322 323 for _, cert in order: 324 print "%s:" % (ALL_CERTS.Get(cert),) 325 apks = by_cert[cert] 326 apks.sort() 327 for _, apk in apks: 328 if apk.shared_uid: 329 print " %-*s %-*s [%s]" % (self.max_fn_len, apk.filename, 330 self.max_pkg_len, apk.package, 331 apk.shared_uid) 332 else: 333 print " %-*s %-*s" % (self.max_fn_len, apk.filename, 334 self.max_pkg_len, apk.package) 335 print 336 337 def CompareWith(self, other): 338 """Look for instances where a given package that exists in both 339 self and other have different certs.""" 340 341 all = set(self.apks.keys()) 342 all.update(other.apks.keys()) 343 344 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len) 345 346 by_certpair = {} 347 348 for i in all: 349 if i in self.apks: 350 if i in other.apks: 351 # in both; should have same set of certs 352 if self.apks[i].certs != other.apks[i].certs: 353 by_certpair.setdefault((other.apks[i].certs, 354 self.apks[i].certs), []).append(i) 355 else: 356 print "%s [%s]: new APK (not in comparison target_files)" % ( 357 i, self.apks[i].filename) 358 else: 359 if i in other.apks: 360 print "%s [%s]: removed APK (only in comparison target_files)" % ( 361 i, other.apks[i].filename) 362 363 if by_certpair: 364 AddProblem("some APKs changed certs") 365 Banner("APK signing differences") 366 for (old, new), packages in sorted(by_certpair.items()): 367 for i, o in enumerate(old): 368 if i == 0: 369 print "was", ALL_CERTS.Get(o) 370 else: 371 print " ", ALL_CERTS.Get(o) 372 for i, n in enumerate(new): 373 if i == 0: 374 print "now", ALL_CERTS.Get(n) 375 else: 376 print " ", ALL_CERTS.Get(n) 377 for i in sorted(packages): 378 old_fn = other.apks[i].filename 379 new_fn = self.apks[i].filename 380 if old_fn == new_fn: 381 print " %-*s [%s]" % (max_pkg_len, i, old_fn) 382 else: 383 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i, 384 old_fn, new_fn) 385 print 386 387 388 def main(argv): 389 def option_handler(o, a): 390 if o in ("-c", "--compare_with"): 391 OPTIONS.compare_with = a 392 elif o in ("-l", "--local_cert_dirs"): 393 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")] 394 elif o in ("-t", "--text"): 395 OPTIONS.text = True 396 else: 397 return False 398 return True 399 400 args = common.ParseOptions(argv, __doc__, 401 extra_opts="c:l:t", 402 extra_long_opts=["compare_with=", 403 "local_cert_dirs="], 404 extra_option_handler=option_handler) 405 406 if len(args) != 1: 407 common.Usage(__doc__) 408 sys.exit(1) 409 410 ALL_CERTS.FindLocalCerts() 411 412 Push("input target_files:") 413 try: 414 target_files = TargetFiles() 415 target_files.LoadZipFile(args[0]) 416 finally: 417 Pop() 418 419 compare_files = None 420 if OPTIONS.compare_with: 421 Push("comparison target_files:") 422 try: 423 compare_files = TargetFiles() 424 compare_files.LoadZipFile(OPTIONS.compare_with) 425 finally: 426 Pop() 427 428 if OPTIONS.text or not compare_files: 429 Banner("target files") 430 target_files.PrintCerts() 431 target_files.CheckSharedUids() 432 target_files.CheckExternalSignatures() 433 if compare_files: 434 if OPTIONS.text: 435 Banner("comparison files") 436 compare_files.PrintCerts() 437 target_files.CompareWith(compare_files) 438 439 if PROBLEMS: 440 print "%d problem(s) found:\n" % (len(PROBLEMS),) 441 for p in PROBLEMS: 442 print p 443 return 1 444 445 return 0 446 447 448 if __name__ == '__main__': 449 try: 450 r = main(sys.argv[1:]) 451 sys.exit(r) 452 except common.ExternalError, e: 453 print 454 print " ERROR: %s" % (e,) 455 print 456 sys.exit(1) 457