check_target_files_signatures revision cad0bb9f621ff1ccfb584e18249b09768c30a0c0
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 55try: 56 from hashlib import sha1 as sha1 57except ImportError: 58 from sha import sha as sha1 59 60import 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). 67class MyZipInfo(zipfile.ZipInfo): 68 def _decodeExtra(self): 69 pass 70zipfile.ZipInfo = MyZipInfo 71 72OPTIONS = common.OPTIONS 73 74OPTIONS.text = False 75OPTIONS.compare_with = None 76OPTIONS.local_cert_dirs = ("vendor", "build") 77 78PROBLEMS = [] 79PROBLEM_PREFIX = [] 80 81def AddProblem(msg): 82 PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg) 83def Push(msg): 84 PROBLEM_PREFIX.append(msg) 85def Pop(): 86 PROBLEM_PREFIX.pop() 87 88 89def Banner(msg): 90 print "-" * 70 91 print " ", msg 92 print "-" * 70 93 94 95def 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 109class 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)" % (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 144ALL_CERTS = CertDB() 145 146 147def 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 162def 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 187class APK(object): 188 def __init__(self, full_filename, filename): 189 self.filename = filename 190 self.cert = None 191 Push(filename+":") 192 try: 193 self.RecordCert(full_filename) 194 self.ReadManifest(full_filename) 195 finally: 196 Pop() 197 198 def RecordCert(self, full_filename): 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 if pkcs7 is not None: 207 AddProblem("multiple certs") 208 pkcs7 = apk.read(info.filename) 209 self.cert = CertFromPKCS7(pkcs7, info.filename) 210 ALL_CERTS.Add(self.cert) 211 if not pkcs7: 212 AddProblem("no signature") 213 finally: 214 f.close() 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 246class 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 = 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 z = zipfile.ZipFile(open(filename, "rb")) 271 self.certmap = common.ReadApkCerts(z) 272 z.close() 273 274 def CheckSharedUids(self): 275 """Look for any instances where packages signed with different 276 certs request the same sharedUserId.""" 277 apks_by_uid = {} 278 for apk in self.apks.itervalues(): 279 if apk.shared_uid: 280 apks_by_uid.setdefault(apk.shared_uid, []).append(apk) 281 282 for uid in sorted(apks_by_uid.keys()): 283 apks = apks_by_uid[uid] 284 for apk in apks[1:]: 285 if apk.cert != apks[0].cert: 286 break 287 else: 288 # all the certs are the same; this uid is fine 289 continue 290 291 AddProblem("uid %s shared across multiple certs" % (uid,)) 292 293 print "uid %s is shared by packages with different certs:" % (uid,) 294 x = [(i.cert, i.package, i) for i in apks] 295 x.sort() 296 lastcert = None 297 for cert, _, apk in x: 298 if cert != lastcert: 299 lastcert = cert 300 print " %s:" % (ALL_CERTS.Get(cert),) 301 print " %-*s [%s]" % (self.max_pkg_len, 302 apk.package, apk.filename) 303 print 304 305 def CheckExternalSignatures(self): 306 for apk_filename, certname in self.certmap.iteritems(): 307 if certname == "EXTERNAL": 308 # Apps marked EXTERNAL should be signed with the test key 309 # during development, then manually re-signed after 310 # predexopting. Consider it an error if this app is now 311 # signed with any key that is present in our tree. 312 apk = self.apks_by_basename[apk_filename] 313 name = ALL_CERTS.Get(apk.cert) 314 if not name.startswith("unknown "): 315 Push(apk.filename) 316 AddProblem("hasn't been signed with EXTERNAL cert") 317 Pop() 318 319 def PrintCerts(self): 320 """Display a table of packages grouped by cert.""" 321 by_cert = {} 322 for apk in self.apks.itervalues(): 323 by_cert.setdefault(apk.cert, []).append((apk.package, apk)) 324 325 order = [(-len(v), k) for (k, v) in by_cert.iteritems()] 326 order.sort() 327 328 for _, cert in order: 329 print "%s:" % (ALL_CERTS.Get(cert),) 330 apks = by_cert[cert] 331 apks.sort() 332 for _, apk in apks: 333 if apk.shared_uid: 334 print " %-*s %-*s [%s]" % (self.max_fn_len, apk.filename, 335 self.max_pkg_len, apk.package, 336 apk.shared_uid) 337 else: 338 print " %-*s %-*s" % (self.max_fn_len, apk.filename, 339 self.max_pkg_len, apk.package) 340 print 341 342 def CompareWith(self, other): 343 """Look for instances where a given package that exists in both 344 self and other have different certs.""" 345 346 all = set(self.apks.keys()) 347 all.update(other.apks.keys()) 348 349 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len) 350 351 by_certpair = {} 352 353 for i in all: 354 if i in self.apks: 355 if i in other.apks: 356 # in both; should have the same cert 357 if self.apks[i].cert != other.apks[i].cert: 358 by_certpair.setdefault((other.apks[i].cert, 359 self.apks[i].cert), []).append(i) 360 else: 361 print "%s [%s]: new APK (not in comparison target_files)" % ( 362 i, self.apks[i].filename) 363 else: 364 if i in other.apks: 365 print "%s [%s]: removed APK (only in comparison target_files)" % ( 366 i, other.apks[i].filename) 367 368 if by_certpair: 369 AddProblem("some APKs changed certs") 370 Banner("APK signing differences") 371 for (old, new), packages in sorted(by_certpair.items()): 372 print "was", ALL_CERTS.Get(old) 373 print "now", ALL_CERTS.Get(new) 374 for i in sorted(packages): 375 old_fn = other.apks[i].filename 376 new_fn = self.apks[i].filename 377 if old_fn == new_fn: 378 print " %-*s [%s]" % (max_pkg_len, i, old_fn) 379 else: 380 print " %-*s [was: %s; now: %s]" % (max_pkg_len, i, 381 old_fn, new_fn) 382 print 383 384 385def main(argv): 386 def option_handler(o, a): 387 if o in ("-c", "--compare_with"): 388 OPTIONS.compare_with = a 389 elif o in ("-l", "--local_cert_dirs"): 390 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")] 391 elif o in ("-t", "--text"): 392 OPTIONS.text = True 393 else: 394 return False 395 return True 396 397 args = common.ParseOptions(argv, __doc__, 398 extra_opts="c:l:t", 399 extra_long_opts=["compare_with=", 400 "local_cert_dirs="], 401 extra_option_handler=option_handler) 402 403 if len(args) != 1: 404 common.Usage(__doc__) 405 sys.exit(1) 406 407 ALL_CERTS.FindLocalCerts() 408 409 Push("input target_files:") 410 try: 411 target_files = TargetFiles() 412 target_files.LoadZipFile(args[0]) 413 finally: 414 Pop() 415 416 compare_files = None 417 if OPTIONS.compare_with: 418 Push("comparison target_files:") 419 try: 420 compare_files = TargetFiles() 421 compare_files.LoadZipFile(OPTIONS.compare_with) 422 finally: 423 Pop() 424 425 if OPTIONS.text or not compare_files: 426 Banner("target files") 427 target_files.PrintCerts() 428 target_files.CheckSharedUids() 429 target_files.CheckExternalSignatures() 430 if compare_files: 431 if OPTIONS.text: 432 Banner("comparison files") 433 compare_files.PrintCerts() 434 target_files.CompareWith(compare_files) 435 436 if PROBLEMS: 437 print "%d problem(s) found:\n" % (len(PROBLEMS),) 438 for p in PROBLEMS: 439 print p 440 return 1 441 442 return 0 443 444 445if __name__ == '__main__': 446 try: 447 r = main(sys.argv[1:]) 448 sys.exit(r) 449 except common.ExternalError, e: 450 print 451 print " ERROR: %s" % (e,) 452 print 453 sys.exit(1) 454