add signing checker script to releasetools

The check_target_files_signatures determines what key was used to sign
every .apk in a given target_files.  It can compare that signature to
that of another target_files (eg, the previous release for that
device) and flag any problems such as .apks signed with a different
key.
diff --git a/tools/releasetools/check_target_files_signatures b/tools/releasetools/check_target_files_signatures
new file mode 100755
index 0000000..b91f3d4
--- /dev/null
+++ b/tools/releasetools/check_target_files_signatures
@@ -0,0 +1,428 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2009 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Check the signatures of all APKs in a target_files .zip file.  With
+-c, compare the signatures of each package to the ones in a separate
+target_files (usually a previously distributed build for the same
+device) and flag any changes.
+
+Usage:  check_target_file_signatures [flags] target_files
+
+  -c  (--compare_with)  <other_target_files>
+      Look for compatibility problems between the two sets of target
+      files (eg., packages whose keys have changed).
+
+  -l  (--local_cert_dirs)  <dir,dir,...>
+      Comma-separated list of top-level directories to scan for
+      .x509.pem files.  Defaults to "vendor,build".  Where cert files
+      can be found that match APK signatures, the filename will be
+      printed as the cert name, otherwise a hash of the cert plus its
+      subject string will be printed instead.
+
+  -t  (--text)
+      Dump the certificate information for both packages in comparison
+      mode (this output is normally suppressed).
+
+"""
+
+import sys
+
+if sys.hexversion < 0x02040000:
+  print >> sys.stderr, "Python 2.4 or newer is required."
+  sys.exit(1)
+
+import os
+import re
+import sha
+import shutil
+import subprocess
+import tempfile
+import zipfile
+
+import common
+
+# Work around a bug in python's zipfile module that prevents opening
+# of zipfiles if any entry has an extra field of between 1 and 3 bytes
+# (which is common with zipaligned APKs).  This overrides the
+# ZipInfo._decodeExtra() method (which contains the bug) with an empty
+# version (since we don't need to decode the extra field anyway).
+class MyZipInfo(zipfile.ZipInfo):
+  def _decodeExtra(self):
+    pass
+zipfile.ZipInfo = MyZipInfo
+
+OPTIONS = common.OPTIONS
+
+OPTIONS.text = False
+OPTIONS.compare_with = None
+OPTIONS.local_cert_dirs = ("vendor", "build")
+
+PROBLEMS = []
+PROBLEM_PREFIX = []
+
+def AddProblem(msg):
+  PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
+def Push(msg):
+  PROBLEM_PREFIX.append(msg)
+def Pop():
+  PROBLEM_PREFIX.pop()
+
+
+def Banner(msg):
+  print "-" * 70
+  print "  ", msg
+  print "-" * 70
+
+
+def GetCertSubject(cert):
+  p = common.Run(["openssl", "x509", "-inform", "DER", "-text"],
+                 stdin=subprocess.PIPE,
+                 stdout=subprocess.PIPE)
+  out, err = p.communicate(cert)
+  if err and not err.strip():
+    return "(error reading cert subject)"
+  for line in out.split("\n"):
+    line = line.strip()
+    if line.startswith("Subject:"):
+      return line[8:].strip()
+  return "(unknown cert subject)"
+
+
+class CertDB(object):
+  def __init__(self):
+    self.certs = {}
+
+  def Add(self, cert, name=None):
+    if cert in self.certs:
+      if name:
+        self.certs[cert] = self.certs[cert] + "," + name
+    else:
+      if name is None:
+        name = "unknown cert %s (%s)" % (sha.sha(cert).hexdigest()[:12],
+                                         GetCertSubject(cert))
+      self.certs[cert] = name
+
+  def Get(self, cert):
+    """Return the name for a given cert."""
+    return self.certs.get(cert, None)
+
+  def FindLocalCerts(self):
+    to_load = []
+    for top in OPTIONS.local_cert_dirs:
+      for dirpath, dirnames, filenames in os.walk(top):
+        certs = [os.path.join(dirpath, i)
+                 for i in filenames if i.endswith(".x509.pem")]
+        if certs:
+          to_load.extend(certs)
+
+    for i in to_load:
+      f = open(i)
+      cert = ParseCertificate(f.read())
+      f.close()
+      name, _ = os.path.splitext(i)
+      name, _ = os.path.splitext(name)
+      self.Add(cert, name)
+
+ALL_CERTS = CertDB()
+
+
+def ParseCertificate(data):
+  """Parse a PEM-format certificate."""
+  cert = []
+  save = False
+  for line in data.split("\n"):
+    if "--END CERTIFICATE--" in line:
+      break
+    if save:
+      cert.append(line)
+    if "--BEGIN CERTIFICATE--" in line:
+      save = True
+  cert = "".join(cert).decode('base64')
+  return cert
+
+
+def CertFromPKCS7(data, filename):
+  """Read the cert out of a PKCS#7-format file (which is what is
+  stored in a signed .apk)."""
+  Push(filename + ":")
+  try:
+    p = common.Run(["openssl", "pkcs7",
+                    "-inform", "DER",
+                    "-outform", "PEM",
+                    "-print_certs"],
+                   stdin=subprocess.PIPE,
+                   stdout=subprocess.PIPE)
+    out, err = p.communicate(data)
+    if err and not err.strip():
+      AddProblem("error reading cert:\n" + err)
+      return None
+
+    cert = ParseCertificate(out)
+    if not cert:
+      AddProblem("error parsing cert output")
+      return None
+    return cert
+  finally:
+    Pop()
+
+
+class APK(object):
+  def __init__(self, full_filename, filename):
+    self.filename = filename
+    self.cert = None
+    Push(filename+":")
+    try:
+      self.RecordCert(full_filename)
+      self.ReadManifest(full_filename)
+    finally:
+      Pop()
+
+  def RecordCert(self, full_filename):
+    try:
+      f = open(full_filename)
+      apk = zipfile.ZipFile(f, "r")
+      pkcs7 = None
+      for info in apk.infolist():
+        if info.filename.startswith("META-INF/") and \
+           (info.filename.endswith(".DSA") or info.filename.endswith(".RSA")):
+          if pkcs7 is not None:
+            AddProblem("multiple certs")
+          pkcs7 = apk.read(info.filename)
+          self.cert = CertFromPKCS7(pkcs7, info.filename)
+          ALL_CERTS.Add(self.cert)
+      if not pkcs7:
+        AddProblem("no signature")
+    finally:
+      f.close()
+
+  def ReadManifest(self, full_filename):
+    p = common.Run(["aapt", "dump", "xmltree", full_filename,
+                    "AndroidManifest.xml"],
+                   stdout=subprocess.PIPE)
+    manifest, err = p.communicate()
+    if err:
+      AddProblem("failed to read manifest")
+      return
+
+    self.shared_uid = None
+    self.package = None
+
+    for line in manifest.split("\n"):
+      line = line.strip()
+      m = re.search('A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
+      if m:
+        name = m.group(1)
+        if name == "android:sharedUserId":
+          if self.shared_uid is not None:
+            AddProblem("multiple sharedUserId declarations")
+          self.shared_uid = m.group(2)
+        elif name == "package":
+          if self.package is not None:
+            AddProblem("multiple package declarations")
+          self.package = m.group(2)
+
+    if self.package is None:
+      AddProblem("no package declaration")
+
+
+class TargetFiles(object):
+  def __init__(self):
+    self.max_pkg_len = 30
+    self.max_fn_len = 20
+
+  def LoadZipFile(self, filename):
+    d = common.UnzipTemp(filename, '*.apk')
+    try:
+      self.apks = {}
+      for dirpath, dirnames, filenames in os.walk(d):
+        for fn in filenames:
+          if fn.endswith(".apk"):
+            fullname = os.path.join(dirpath, fn)
+            displayname = fullname[len(d)+1:]
+            apk = APK(fullname, displayname)
+            self.apks[apk.package] = apk
+
+            self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
+            self.max_fn_len = max(self.max_fn_len, len(apk.filename))
+    finally:
+      shutil.rmtree(d)
+
+  def CheckSharedUids(self):
+    """Look for any instances where packages signed with different
+    certs request the same sharedUserId."""
+    apks_by_uid = {}
+    for apk in self.apks.itervalues():
+      if apk.shared_uid:
+        apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
+
+    for uid in sorted(apks_by_uid.keys()):
+      apks = apks_by_uid[uid]
+      for apk in apks[1:]:
+        if apk.cert != apks[0].cert:
+          break
+      else:
+        # all the certs are the same; this uid is fine
+        continue
+
+      AddProblem("uid %s shared across multiple certs" % (uid,))
+
+      print "uid %s is shared by packages with different certs:" % (uid,)
+      x = [(i.cert, i.package, i) for i in apks]
+      x.sort()
+      lastcert = None
+      for cert, _, apk in x:
+        if cert != lastcert:
+          lastcert = cert
+          print "    %s:" % (ALL_CERTS.Get(cert),)
+        print "        %-*s  [%s]" % (self.max_pkg_len,
+                                      apk.package, apk.filename)
+      print
+
+  def PrintCerts(self):
+    """Display a table of packages grouped by cert."""
+    by_cert = {}
+    for apk in self.apks.itervalues():
+      by_cert.setdefault(apk.cert, []).append((apk.package, apk))
+
+    order = [(-len(v), k) for (k, v) in by_cert.iteritems()]
+    order.sort()
+
+    for _, cert in order:
+      print "%s:" % (ALL_CERTS.Get(cert),)
+      apks = by_cert[cert]
+      apks.sort()
+      for _, apk in apks:
+        if apk.shared_uid:
+          print "  %-*s  %-*s  [%s]" % (self.max_fn_len, apk.filename,
+                                        self.max_pkg_len, apk.package,
+                                        apk.shared_uid)
+        else:
+          print "  %-*s  %-*s" % (self.max_fn_len, apk.filename,
+                                  self.max_pkg_len, apk.package)
+      print
+
+  def CompareWith(self, other):
+    """Look for instances where a given package that exists in both
+    self and other have different certs."""
+
+    all = set(self.apks.keys())
+    all.update(other.apks.keys())
+
+    max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
+
+    by_certpair = {}
+
+    for i in all:
+      if i in self.apks:
+        if i in other.apks:
+          # in both; should have the same cert
+          if self.apks[i].cert != other.apks[i].cert:
+            by_certpair.setdefault((other.apks[i].cert,
+                                    self.apks[i].cert), []).append(i)
+        else:
+          print "%s [%s]: new APK (not in comparison target_files)" % (
+              i, self.apks[i].filename)
+      else:
+        if i in other.apks:
+          print "%s [%s]: removed APK (only in comparison target_files)" % (
+              i, other.apks[i].filename)
+
+    if by_certpair:
+      AddProblem("some APKs changed certs")
+      Banner("APK signing differences")
+      for (old, new), packages in sorted(by_certpair.items()):
+        print "was", ALL_CERTS.Get(old)
+        print "now", ALL_CERTS.Get(new)
+        for i in sorted(packages):
+          old_fn = other.apks[i].filename
+          new_fn = self.apks[i].filename
+          if old_fn == new_fn:
+            print "  %-*s  [%s]" % (max_pkg_len, i, old_fn)
+          else:
+            print "  %-*s  [was: %s; now: %s]" % (max_pkg_len, i,
+                                                  old_fn, new_fn)
+        print
+
+
+def main(argv):
+  def option_handler(o, a):
+    if o in ("-c", "--compare_with"):
+      OPTIONS.compare_with = a
+    elif o in ("-l", "--local_cert_dirs"):
+      OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
+    elif o in ("-t", "--text"):
+      OPTIONS.text = True
+    else:
+      return False
+    return True
+
+  args = common.ParseOptions(argv, __doc__,
+                             extra_opts="c:l:t",
+                             extra_long_opts=["compare_with=",
+                                              "local_cert_dirs="],
+                             extra_option_handler=option_handler)
+
+  if len(args) != 1:
+    common.Usage(__doc__)
+    sys.exit(1)
+
+  ALL_CERTS.FindLocalCerts()
+
+  Push("input target_files:")
+  try:
+    target_files = TargetFiles()
+    target_files.LoadZipFile(args[0])
+  finally:
+    Pop()
+
+  compare_files = None
+  if OPTIONS.compare_with:
+    Push("comparison target_files:")
+    try:
+      compare_files = TargetFiles()
+      compare_files.LoadZipFile(OPTIONS.compare_with)
+    finally:
+      Pop()
+
+  if OPTIONS.text or not compare_files:
+    Banner("target files")
+    target_files.PrintCerts()
+  target_files.CheckSharedUids()
+  if compare_files:
+    if OPTIONS.text:
+      Banner("comparison files")
+      compare_files.PrintCerts()
+    target_files.CompareWith(compare_files)
+
+  if PROBLEMS:
+    print "%d problem(s) found:\n" % (len(PROBLEMS),)
+    for p in PROBLEMS:
+      print p
+    return 1
+
+  return 0
+
+
+if __name__ == '__main__':
+  try:
+    r = main(sys.argv[1:])
+    sys.exit(r)
+  except common.ExternalError, e:
+    print
+    print "   ERROR: %s" % (e,)
+    print
+    sys.exit(1)
diff --git a/tools/releasetools/common.py b/tools/releasetools/common.py
index 26f216d..0e17a5f 100644
--- a/tools/releasetools/common.py
+++ b/tools/releasetools/common.py
@@ -141,12 +141,15 @@
   BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
                            "boot.img", output_zip)
 
-def UnzipTemp(filename):
+def UnzipTemp(filename, pattern=None):
   """Unzip the given archive into a temporary directory and return the name."""
 
   tmp = tempfile.mkdtemp(prefix="targetfiles-")
   OPTIONS.tempfiles.append(tmp)
-  p = Run(["unzip", "-o", "-q", filename, "-d", tmp], stdout=subprocess.PIPE)
+  cmd = ["unzip", "-o", "-q", filename, "-d", tmp]
+  if pattern is not None:
+    cmd.append(pattern)
+  p = Run(cmd, stdout=subprocess.PIPE)
   p.communicate()
   if p.returncode != 0:
     raise ExternalError("failed to unzip input target-files \"%s\"" %