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