blob: ca1e4328032407b354a1e3163e35740fb89a84a3 [file] [log] [blame]
Geremy Condraedf7b4c2013-03-26 22:19:03 +00001#!/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 = ""
William Roberts1ecb4e82013-10-06 18:32:05 -040037 lineNo = 1
38 certNo = 1
Geremy Condraedf7b4c2013-03-26 22:19:03 +000039 inCert = False
40 for line in pkFile:
William Roberts1ecb4e82013-10-06 18:32:05 -040041 line = line.strip()
42 # Are we starting the certificate?
William Roberts14138332013-10-14 15:51:48 -070043 if line == "-----BEGIN CERTIFICATE-----":
William Roberts1ecb4e82013-10-06 18:32:05 -040044 if inCert:
45 sys.exit("Encountered another BEGIN CERTIFICATE without END CERTIFICATE on " +
46 "line: " + str(lineNo))
Geremy Condraedf7b4c2013-03-26 22:19:03 +000047
William Roberts1ecb4e82013-10-06 18:32:05 -040048 inCert = True
Geremy Condraedf7b4c2013-03-26 22:19:03 +000049
William Roberts1ecb4e82013-10-06 18:32:05 -040050 # Are we ending the ceritifcate?
William Roberts14138332013-10-14 15:51:48 -070051 elif line == "-----END CERTIFICATE-----":
William Roberts1ecb4e82013-10-06 18:32:05 -040052 if not inCert:
53 sys.exit("Encountered END CERTIFICATE before BEGIN CERTIFICATE on line: "
54 + str(lineNo))
Geremy Condraedf7b4c2013-03-26 22:19:03 +000055
William Roberts1ecb4e82013-10-06 18:32:05 -040056 # 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:
Mike Palmiotto070c01f2013-10-10 16:37:03 -040082 if line is not "":
83 sys.exit("Detected erroneous line \""+ line + "\" on " + str(lineNo)
William Roberts1ecb4e82013-10-06 18:32:05 -040084 + " 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
Geremy Condraedf7b4c2013-03-26 22:19:03 +000097
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
Geremy Condra020b5ff2013-03-27 19:33:02 -0700115 def generateKeyMap(self, target_build_variant, key_directory):
Geremy Condraedf7b4c2013-03-26 22:19:03 +0000116
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
Richard Haines1b46b2f2013-08-08 15:13:29 +0100137 tag_path = os.path.expandvars(self.get(tag, option))
138 path = os.path.join(key_directory, tag_path)
Geremy Condraedf7b4c2013-03-26 22:19:03 +0000139
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
Geremy Condraedf7b4c2013-03-26 22:19:03 +0000167 self._out.write(ReplaceTags.XML_ENCODING_TAG)
168 self._out.write("<!-- AUTOGENERATED FILE DO NOT MODIFY -->")
Robert Craig7f2392e2013-03-27 08:35:39 -0400169 self._out.write("<policy>")
170
171 def __del__(self):
172 self._out.write("</policy>")
Geremy Condraedf7b4c2013-03-26 22:19:03 +0000173
174 def startElement(self, tag, attrs):
Robert Craig7f2392e2013-03-27 08:35:39 -0400175 if tag == ReplaceTags.POLICY_TAG:
176 return
Geremy Condraedf7b4c2013-03-26 22:19:03 +0000177
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):
Robert Craig7f2392e2013-03-27 08:35:39 -0400195 if tag == ReplaceTags.POLICY_TAG:
196 return
197
Geremy Condraedf7b4c2013-03-26 22:19:03 +0000198 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.
Robert Craig7f2392e2013-03-27 08:35:39 -0400215 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."
Geremy Condraedf7b4c2013-03-26 22:19:03 +0000220
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
Geremy Condra020b5ff2013-03-27 19:33:02 -0700239 parser.add_option("-d", "--key-directory", default="", dest="key_directory",
240 help="Specify a parent directory for keys")
241
Geremy Condraedf7b4c2013-03-26 22:19:03 +0000242 (options, args) = parser.parse_args()
243
Robert Craig7f2392e2013-03-27 08:35:39 -0400244 if len(args) < 2:
245 parser.error("Must specify a config file (keys.conf) AND mac_permissions.xml file(s)!")
Geremy Condraedf7b4c2013-03-26 22:19:03 +0000246
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
Geremy Condra020b5ff2013-03-27 19:33:02 -0700259 key_map = config.generateKeyMap(options.target_build_variant.lower(), options.key_directory)
Geremy Condraedf7b4c2013-03-26 22:19:03 +0000260 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))
Robert Craig7f2392e2013-03-27 08:35:39 -0400266 for f in args[1:]:
267 parser.parse(f)