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