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                # 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