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