Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 
      3 from xml.sax import saxutils, handler, make_parser
      4 from optparse import OptionParser
      5 import ConfigParser
      6 import logging
      7 import base64
      8 import sys
      9 import os
     10 
     11 __VERSION = (0, 1)
     12 
     13 '''
     14 This tool reads a mac_permissions.xml and replaces keywords in the signature
     15 clause with keys provided by pem files.
     16 '''
     17 
     18 class GenerateKeys(object):
     19     def __init__(self, path):
     20         '''
     21         Generates an object with Base16 and Base64 encoded versions of the keys
     22         found in the supplied pem file argument. PEM files can contain multiple
     23         certs, however this seems to be unused in Android as pkg manager grabs
     24         the first cert in the APK. This will however support multiple certs in
     25         the resulting generation with index[0] being the first cert in the pem
     26         file.
     27         '''
     28 
     29         self._base64Key = list()
     30         self._base16Key = list()
     31 
     32         if not os.path.isfile(path):
     33             sys.exit("Path " + path + " does not exist or is not a file!")
     34 
     35         pkFile = open(path, 'rb').readlines()
     36         base64Key = ""
     37         lineNo = 1
     38         certNo = 1
     39         inCert = False
     40         for line in pkFile:
     41             line = line.strip()
     42             # Are we starting the certificate?
     43             if line == "-----BEGIN CERTIFICATE-----":
     44                 if inCert:
     45                     sys.exit("Encountered another BEGIN CERTIFICATE without END CERTIFICATE on " +
     46                              "line: " + str(lineNo))
     47 
     48                 inCert = True
     49 
     50             # Are we ending the ceritifcate?
     51             elif line == "-----END CERTIFICATE-----":
     52                 if not inCert:
     53                     sys.exit("Encountered END CERTIFICATE before BEGIN CERTIFICATE on line: "
     54                             + str(lineNo))
     55 
     56                 # If we ended the certificate trip the flag
     57                 inCert = False
     58 
     59                 # Sanity check the input
     60                 if len(base64Key) == 0:
     61                     sys.exit("Empty certficate , certificate "+ str(certNo) + " found in file: "
     62                             + path)
     63 
     64                 # ... and append the certificate to the list
     65                 # Base 64 includes uppercase. DO NOT tolower()
     66                 self._base64Key.append(base64Key)
     67                 try:
     68                     # Pkgmanager and setool see hex strings with lowercase, lets be consistent
     69                     self._base16Key.append(base64.b16encode(base64.b64decode(base64Key)).lower())
     70                 except TypeError:
     71                     sys.exit("Invalid certificate, certificate "+ str(certNo) + " found in file: "
     72                             + path)
     73 
     74                 # After adding the key, reset the accumulator as pem files may have subsequent keys
     75                 base64Key=""
     76 
     77                 # And increment your cert number
     78                 certNo = certNo + 1
     79 
     80             # If we haven't started the certificate, then we should not encounter any data
     81             elif not inCert:
     82                 if line is not "":
     83                     sys.exit("Detected erroneous line \""+ line + "\" on " + str(lineNo)
     84                         + " in pem file: " + path)
     85 
     86             # else we have started the certicate and need to append the data
     87             elif inCert:
     88                 base64Key += line
     89 
     90             else:
     91                 # We should never hit this assert, if we do then an unaccounted for state
     92                 # was entered that was NOT addressed by the if/elif statements above
     93                 assert(False == True)
     94 
     95             # The last thing to do before looping up is to increment line number
     96             lineNo = lineNo + 1
     97 
     98     def __len__(self):
     99         return len(self._base16Key)
    100 
    101     def __str__(self):
    102         return str(self.getBase16Keys())
    103 
    104     def getBase16Keys(self):
    105         return self._base16Key
    106 
    107     def getBase64Keys(self):
    108         return self._base64Key
    109 
    110 class ParseConfig(ConfigParser.ConfigParser):
    111 
    112     # This must be lowercase
    113     OPTION_WILDCARD_TAG = "all"
    114 
    115     def generateKeyMap(self, target_build_variant, key_directory):
    116 
    117         keyMap = dict()
    118 
    119         for tag in self.sections():
    120 
    121             options = self.options(tag)
    122 
    123             for option in options:
    124 
    125                 # Only generate the key map for debug or release,
    126                 # not both!
    127                 if option != target_build_variant and \
    128                 option != ParseConfig.OPTION_WILDCARD_TAG:
    129                     logging.info("Skipping " + tag + " : " + option +
    130                         " because target build variant is set to " +
    131                         str(target_build_variant))
    132                     continue
    133 
    134                 if tag in keyMap:
    135                     sys.exit("Duplicate tag detected " + tag)
    136 
    137                 tag_path = os.path.expandvars(self.get(tag, option))
    138                 path = os.path.join(key_directory, tag_path)
    139 
    140                 keyMap[tag] = GenerateKeys(path)
    141 
    142                 # Multiple certificates may exist in
    143                 # the pem file. GenerateKeys supports
    144                 # this however, the mac_permissions.xml
    145                 # as well as PMS do not.
    146                 assert len(keyMap[tag]) == 1
    147 
    148         return keyMap
    149 
    150 class ReplaceTags(handler.ContentHandler):
    151 
    152     DEFAULT_TAG = "default"
    153     PACKAGE_TAG = "package"
    154     POLICY_TAG = "policy"
    155     SIGNER_TAG = "signer"
    156     SIGNATURE_TAG = "signature"
    157 
    158     TAGS_WITH_CHILDREN = [ DEFAULT_TAG, PACKAGE_TAG, POLICY_TAG, SIGNER_TAG ]
    159 
    160     XML_ENCODING_TAG = '<?xml version="1.0" encoding="iso-8859-1"?>'
    161 
    162     def __init__(self, keyMap, out=sys.stdout):
    163 
    164         handler.ContentHandler.__init__(self)
    165         self._keyMap = keyMap
    166         self._out = out
    167         self._out.write(ReplaceTags.XML_ENCODING_TAG)
    168         self._out.write("<!-- AUTOGENERATED FILE DO NOT MODIFY -->")
    169         self._out.write("<policy>")
    170 
    171     def __del__(self):
    172         self._out.write("</policy>")
    173 
    174     def startElement(self, tag, attrs):
    175         if tag == ReplaceTags.POLICY_TAG:
    176             return
    177 
    178         self._out.write('<' + tag)
    179 
    180         for (name, value) in attrs.items():
    181 
    182             if name == ReplaceTags.SIGNATURE_TAG and value in self._keyMap:
    183                 for key in self._keyMap[value].getBase16Keys():
    184                     logging.info("Replacing " + name + " " + value + " with " + key)
    185                     self._out.write(' %s="%s"' % (name, saxutils.escape(key)))
    186             else:
    187                 self._out.write(' %s="%s"' % (name, saxutils.escape(value)))
    188 
    189         if tag in ReplaceTags.TAGS_WITH_CHILDREN:
    190             self._out.write('>')
    191         else:
    192             self._out.write('/>')
    193 
    194     def endElement(self, tag):
    195         if tag == ReplaceTags.POLICY_TAG:
    196             return
    197 
    198         if tag in ReplaceTags.TAGS_WITH_CHILDREN:
    199             self._out.write('</%s>' % tag)
    200 
    201     def characters(self, content):
    202         if not content.isspace():
    203             self._out.write(saxutils.escape(content))
    204 
    205     def ignorableWhitespace(self, content):
    206         pass
    207 
    208     def processingInstruction(self, target, data):
    209         self._out.write('<?%s %s?>' % (target, data))
    210 
    211 if __name__ == "__main__":
    212 
    213     # Intentional double space to line up equls signs and opening " for
    214     # readability.
    215     usage  = "usage: %prog [options] CONFIG_FILE MAC_PERMISSIONS_FILE [MAC_PERMISSIONS_FILE...]\n"
    216     usage += "This tool allows one to configure an automatic inclusion\n"
    217     usage += "of signing keys into the mac_permision.xml file(s) from the\n"
    218     usage += "pem files. If mulitple mac_permision.xml files are included\n"
    219     usage += "then they are unioned to produce a final version."
    220 
    221     version = "%prog " + str(__VERSION)
    222 
    223     parser = OptionParser(usage=usage, version=version)
    224 
    225     parser.add_option("-v", "--verbose",
    226                       action="store_true", dest="verbose", default=False,
    227                       help="Print internal operations to stdout")
    228 
    229     parser.add_option("-o", "--output", default="stdout", dest="output_file",
    230                       metavar="FILE", help="Specify an output file, default is stdout")
    231 
    232     parser.add_option("-c", "--cwd", default=os.getcwd(), dest="root",
    233                       metavar="DIR", help="Specify a root (CWD) directory to run this from, it" \
    234                                           "chdirs' AFTER loading the config file")
    235 
    236     parser.add_option("-t", "--target-build-variant", default="eng", dest="target_build_variant",
    237                       help="Specify the TARGET_BUILD_VARIANT, defaults to eng")
    238 
    239     parser.add_option("-d", "--key-directory", default="", dest="key_directory",
    240                       help="Specify a parent directory for keys")
    241 
    242     (options, args) = parser.parse_args()
    243 
    244     if len(args) < 2:
    245         parser.error("Must specify a config file (keys.conf) AND mac_permissions.xml file(s)!")
    246 
    247     logging.basicConfig(level=logging.INFO if options.verbose == True else logging.WARN)
    248 
    249     # Read the config file
    250     config = ParseConfig()
    251     config.read(args[0])
    252 
    253     os.chdir(options.root)
    254 
    255     output_file = sys.stdout if options.output_file == "stdout" else open(options.output_file, "w")
    256     logging.info("Setting output file to: " + options.output_file)
    257 
    258     # Generate the key list
    259     key_map = config.generateKeyMap(options.target_build_variant.lower(), options.key_directory)
    260     logging.info("Generate key map:")
    261     for k in key_map:
    262         logging.info(k + " : " + str(key_map[k]))
    263     # Generate the XML file with markup replaced with keys
    264     parser = make_parser()
    265     parser.setContentHandler(ReplaceTags(key_map, output_file))
    266     for f in args[1:]:
    267         parser.parse(f)
    268