1#!/usr/bin/env python3 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, 'r').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 # 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)).decode('ascii').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 != "": 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 handler.ContentHandler.__init__(self) 164 self._keyMap = keyMap 165 self._out = out 166 167 def prologue(self): 168 self._out.write(ReplaceTags.XML_ENCODING_TAG) 169 self._out.write("<!-- AUTOGENERATED FILE DO NOT MODIFY -->") 170 self._out.write("<policy>") 171 172 def epilogue(self): 173 self._out.write("</policy>") 174 175 def startElement(self, tag, attrs): 176 if tag == ReplaceTags.POLICY_TAG: 177 return 178 179 self._out.write('<' + tag) 180 181 for (name, value) in attrs.items(): 182 183 if name == ReplaceTags.SIGNATURE_TAG and value in self._keyMap: 184 for key in self._keyMap[value].getBase16Keys(): 185 logging.info("Replacing " + name + " " + value + " with " + key) 186 self._out.write(' %s="%s"' % (name, saxutils.escape(key))) 187 else: 188 self._out.write(' %s="%s"' % (name, saxutils.escape(value))) 189 190 if tag in ReplaceTags.TAGS_WITH_CHILDREN: 191 self._out.write('>') 192 else: 193 self._out.write('/>') 194 195 def endElement(self, tag): 196 if tag == ReplaceTags.POLICY_TAG: 197 return 198 199 if tag in ReplaceTags.TAGS_WITH_CHILDREN: 200 self._out.write('</%s>' % tag) 201 202 def characters(self, content): 203 if not content.isspace(): 204 self._out.write(saxutils.escape(content)) 205 206 def ignorableWhitespace(self, content): 207 pass 208 209 def processingInstruction(self, target, data): 210 self._out.write('<?%s %s?>' % (target, data)) 211 212if __name__ == "__main__": 213 214 usage = "usage: %prog [options] CONFIG_FILE MAC_PERMISSIONS_FILE [MAC_PERMISSIONS_FILE...]\n" 215 usage += "This tool allows one to configure an automatic inclusion\n" 216 usage += "of signing keys into the mac_permision.xml file(s) from the\n" 217 usage += "pem files. If mulitple mac_permision.xml files are included\n" 218 usage += "then they are unioned to produce a final version." 219 220 version = "%prog " + str(__VERSION) 221 222 parser = OptionParser(usage=usage, version=version) 223 224 parser.add_option("-v", "--verbose", 225 action="store_true", dest="verbose", default=False, 226 help="Print internal operations to stdout") 227 228 parser.add_option("-o", "--output", default="stdout", dest="output_file", 229 metavar="FILE", help="Specify an output file, default is stdout") 230 231 parser.add_option("-c", "--cwd", default=os.getcwd(), dest="root", 232 metavar="DIR", help="Specify a root (CWD) directory to run this from, it" \ 233 "chdirs' AFTER loading the config file") 234 235 parser.add_option("-t", "--target-build-variant", default="eng", dest="target_build_variant", 236 help="Specify the TARGET_BUILD_VARIANT, defaults to eng") 237 238 parser.add_option("-d", "--key-directory", default="", dest="key_directory", 239 help="Specify a parent directory for keys") 240 241 (options, args) = parser.parse_args() 242 243 if len(args) < 2: 244 parser.error("Must specify a config file (keys.conf) AND mac_permissions.xml file(s)!") 245 246 logging.basicConfig(level=logging.INFO if options.verbose == True else logging.WARN) 247 248 # Read the config file 249 config = ParseConfig() 250 config.read(args[0]) 251 252 os.chdir(options.root) 253 254 output_file = sys.stdout if options.output_file == "stdout" else open(options.output_file, "w") 255 logging.info("Setting output file to: " + options.output_file) 256 257 # Generate the key list 258 key_map = config.generateKeyMap(options.target_build_variant.lower(), options.key_directory) 259 logging.info("Generate key map:") 260 for k in key_map: 261 logging.info(k + " : " + str(key_map[k])) 262 # Generate the XML file with markup replaced with keys 263 parser = make_parser() 264 handler = ReplaceTags(key_map, output_file) 265 parser.setContentHandler(handler) 266 handler.prologue() 267 for f in args[1:]: 268 parser.parse(f) 269 handler.epilogue() 270