1#!/usr/bin/env python 2 3from xml.sax import saxutils, handler, make_parser 4from optparse import OptionParser 5import ConfigParser 6import logging 7import base64 8import sys 9import os 10 11__VERSION = (0, 1) 12 13''' 14This tool reads a mac_permissions.xml and replaces keywords in the signature 15clause with keys provided by pem files. 16''' 17 18class 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 110class 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 150class 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 211if __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