|
1 #!/usr/bin/env python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 ############################################################################## |
|
5 # cmd_mergesis.py - Ensymble command line tool, mergesis command |
|
6 # Copyright 2006, 2007 Jussi Ylänen |
|
7 # |
|
8 # This file is part of Ensymble developer utilities for Symbian OS(TM). |
|
9 # |
|
10 # Ensymble is free software; you can redistribute it and/or modify |
|
11 # it under the terms of the GNU General Public License as published by |
|
12 # the Free Software Foundation; either version 2 of the License, or |
|
13 # (at your option) any later version. |
|
14 # |
|
15 # Ensymble is distributed in the hope that it will be useful, |
|
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
18 # GNU General Public License for more details. |
|
19 # |
|
20 # You should have received a copy of the GNU General Public License |
|
21 # along with Ensymble; if not, write to the Free Software |
|
22 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
|
23 ############################################################################## |
|
24 |
|
25 import sys |
|
26 import os |
|
27 import getopt |
|
28 import getpass |
|
29 import locale |
|
30 |
|
31 import sisfile |
|
32 import sisfield |
|
33 import cryptutil |
|
34 |
|
35 |
|
36 ############################################################################## |
|
37 # Help texts |
|
38 ############################################################################## |
|
39 |
|
40 shorthelp = 'Merge several SIS packages into one' |
|
41 longhelp = '''mergesis |
|
42 [--cert=mycert.cer] [--privkey=mykey.key] [--passphrase=12345] |
|
43 [--encoding=terminal,filesystem] [--verbose] |
|
44 <infile> [mergefile]... <outfile> |
|
45 |
|
46 Merge several SIS packages into one and sign the resulting SIS file with |
|
47 the certificate provided. The first SIS file is used as the base file and |
|
48 the remaining SIS files are added as unconditional embedded SIS files |
|
49 into it. Any signatures present in the first SIS file are stripped. |
|
50 |
|
51 Options: |
|
52 infile - Path of the base SIS file |
|
53 mergefile - Path of SIS file(s) to add to the base SIS file |
|
54 outfile - Path of the resulting SIS file |
|
55 cert - Certificate to use for signing (PEM format) |
|
56 privkey - Private key of the certificate (PEM format) |
|
57 passphrase - Pass phrase of the private key (insecure, use stdin instead) |
|
58 encoding - Local character encodings for terminal and filesystem |
|
59 verbose - Print extra statistics |
|
60 |
|
61 Merging SIS files that already contain other SIS files is not supported. |
|
62 ''' |
|
63 |
|
64 |
|
65 ############################################################################## |
|
66 # Parameters |
|
67 ############################################################################## |
|
68 |
|
69 MAXPASSPHRASELENGTH = 256 |
|
70 MAXCERTIFICATELENGTH = 65536 |
|
71 MAXPRIVATEKEYLENGTH = 65536 |
|
72 MAXSISFILESIZE = 1024 * 1024 * 8 # Eight megabytes |
|
73 |
|
74 |
|
75 ############################################################################## |
|
76 # Global variables |
|
77 ############################################################################## |
|
78 |
|
79 debug = False |
|
80 |
|
81 |
|
82 ############################################################################## |
|
83 # Public module-level functions |
|
84 ############################################################################## |
|
85 |
|
86 def run(pgmname, argv): |
|
87 global debug |
|
88 |
|
89 # Determine system character encodings. |
|
90 try: |
|
91 # getdefaultlocale() may sometimes return None. |
|
92 # Fall back to ASCII encoding in that case. |
|
93 terminalenc = locale.getdefaultlocale()[1] + "" |
|
94 except TypeError: |
|
95 # Invalid locale, fall back to ASCII terminal encoding. |
|
96 terminalenc = "ascii" |
|
97 |
|
98 try: |
|
99 # sys.getfilesystemencoding() was introduced in Python v2.3 and |
|
100 # it can sometimes return None. Fall back to ASCII if something |
|
101 # goes wrong. |
|
102 filesystemenc = sys.getfilesystemencoding() + "" |
|
103 except (AttributeError, TypeError): |
|
104 filesystemenc = "ascii" |
|
105 |
|
106 try: |
|
107 gopt = getopt.gnu_getopt |
|
108 except: |
|
109 # Python <v2.3, GNU-style parameter ordering not supported. |
|
110 gopt = getopt.getopt |
|
111 |
|
112 # Parse command line arguments. |
|
113 short_opts = "a:k:p:e:vh" |
|
114 long_opts = [ |
|
115 "cert=", "privkey=", "passphrase=", |
|
116 "encoding=", "verbose", "debug", "help" |
|
117 ] |
|
118 args = gopt(argv, short_opts, long_opts) |
|
119 |
|
120 opts = dict(args[0]) |
|
121 pargs = args[1] |
|
122 |
|
123 if len(pargs) < 2: |
|
124 raise ValueError("wrong number of arguments") |
|
125 |
|
126 # Override character encoding of command line and filesystem. |
|
127 encs = opts.get("--encoding", opts.get("-e", "%s,%s" % (terminalenc, |
|
128 filesystemenc))) |
|
129 try: |
|
130 terminalenc, filesystemenc = encs.split(",") |
|
131 except (ValueError, TypeError): |
|
132 raise ValueError("invalid encoding string '%s'" % encs) |
|
133 |
|
134 # Get input SIS file names. |
|
135 infiles = [f.decode(terminalenc).encode(filesystemenc) for f in pargs[:-1]] |
|
136 |
|
137 # Determine output SIS file name. |
|
138 outfile = pargs[-1].decode(terminalenc).encode(filesystemenc) |
|
139 if os.path.isdir(outfile): |
|
140 # Output to directory, use input file name. |
|
141 outfile = os.path.join(outfile, os.path.basename(infiles[0])) |
|
142 |
|
143 # Get certificate and its private key file names. |
|
144 cert = opts.get("--cert", opts.get("-a", None)) |
|
145 privkey = opts.get("--privkey", opts.get("-k", None)) |
|
146 if cert != None and privkey != None: |
|
147 # Convert file names from terminal encoding to filesystem encoding. |
|
148 cert = cert.decode(terminalenc).encode(filesystemenc) |
|
149 privkey = privkey.decode(terminalenc).encode(filesystemenc) |
|
150 |
|
151 # Read certificate file. |
|
152 f = file(cert, "rb") |
|
153 certdata = f.read(MAXCERTIFICATELENGTH + 1) |
|
154 f.close() |
|
155 |
|
156 if len(certdata) > MAXCERTIFICATELENGTH: |
|
157 raise ValueError("certificate file too large") |
|
158 |
|
159 # Read private key file. |
|
160 f = file(privkey, "rb") |
|
161 privkeydata = f.read(MAXPRIVATEKEYLENGTH + 1) |
|
162 f.close() |
|
163 |
|
164 if len(privkeydata) > MAXPRIVATEKEYLENGTH: |
|
165 raise ValueError("private key file too large") |
|
166 elif cert == None and privkey == None: |
|
167 # No certificate given, use the Ensymble default certificate. |
|
168 # defaultcert.py is not imported when not needed. This speeds |
|
169 # up program start-up a little. |
|
170 import defaultcert |
|
171 certdata = defaultcert.cert |
|
172 privkeydata = defaultcert.privkey |
|
173 |
|
174 print ("%s: warning: no certificate given, using " |
|
175 "insecure built-in one" % pgmname) |
|
176 else: |
|
177 raise ValueError("missing certificate or private key") |
|
178 |
|
179 # Get pass phrase. Pass phrase remains in terminal encoding. |
|
180 passphrase = opts.get("--passphrase", opts.get("-p", None)) |
|
181 if passphrase == None and privkey != None: |
|
182 # Private key given without "--passphrase" option, ask it. |
|
183 if sys.stdin.isatty(): |
|
184 # Standard input is a TTY, ask password interactively. |
|
185 passphrase = getpass.getpass("Enter private key pass phrase:") |
|
186 else: |
|
187 # Not connected to a TTY, read stdin non-interactively instead. |
|
188 passphrase = sys.stdin.read(MAXPASSPHRASELENGTH + 1) |
|
189 |
|
190 if len(passphrase) > MAXPASSPHRASELENGTH: |
|
191 raise ValueError("pass phrase too long") |
|
192 |
|
193 passphrase = passphrase.strip() |
|
194 |
|
195 # Determine verbosity. |
|
196 verbose = False |
|
197 if "--verbose" in opts.keys() or "-v" in opts.keys(): |
|
198 verbose = True |
|
199 |
|
200 # Determine if debug output is requested. |
|
201 if "--debug" in opts.keys(): |
|
202 debug = True |
|
203 |
|
204 # Enable debug output for OpenSSL-related functions. |
|
205 cryptutil.setdebug(True) |
|
206 |
|
207 # Ingredients for successful SIS generation: |
|
208 # |
|
209 # terminalenc Terminal character encoding (autodetected) |
|
210 # filesystemenc File system name encoding (autodetected) |
|
211 # infiles A list of input SIS file names, filesystemenc encoded |
|
212 # outfile Output SIS file name, filesystemenc encoded |
|
213 # cert Certificate in PEM format |
|
214 # privkey Certificate private key in PEM format |
|
215 # passphrase Pass phrase of priv. key, terminalenc encoded string |
|
216 # verbose Boolean indicating verbose terminal output |
|
217 |
|
218 if verbose: |
|
219 print |
|
220 print "Input SIS files %s" % " ".join( |
|
221 [f.decode(filesystemenc).encode(terminalenc) for f in infiles]) |
|
222 print "Output SIS file %s" % ( |
|
223 outfile.decode(filesystemenc).encode(terminalenc)) |
|
224 print "Certificate %s" % ((cert and |
|
225 cert.decode(filesystemenc).encode(terminalenc)) or "<default>") |
|
226 print "Private key %s" % ((privkey and |
|
227 privkey.decode(filesystemenc).encode(terminalenc)) or "<default>") |
|
228 print |
|
229 |
|
230 insis = [] |
|
231 for n in xrange(len(infiles)): |
|
232 # Read input SIS files. |
|
233 f = file(infiles[n], "rb") |
|
234 instring = f.read(MAXSISFILESIZE + 1) |
|
235 f.close() |
|
236 |
|
237 if len(instring) > MAXSISFILESIZE: |
|
238 raise ValueError("%s: input SIS file too large" % infiles[n]) |
|
239 |
|
240 if n == 0: |
|
241 # Store UIDs for later use. |
|
242 uids = instring[:16] # UID1, UID2, UID3 and UIDCRC |
|
243 |
|
244 # Convert input SIS file to SISFields. |
|
245 sf, rlen = sisfield.SISField(instring[16:], False) |
|
246 |
|
247 # Ignore extra bytes after SIS file. |
|
248 if len(instring) > (rlen + 16): |
|
249 print ("%s: %s: warning: %d extra bytes after SIS file (ignored)" % |
|
250 (pgmname, infiles[n], (len(instring) - (rlen + 16)))) |
|
251 |
|
252 # Try to release some memory early. |
|
253 del instring |
|
254 |
|
255 # Check that there are no embedded SIS files. |
|
256 if len(sf.Data.DataUnits) > 1: |
|
257 raise ValueError("%s: input SIS file contains " |
|
258 "embedded SIS files" % infiles[n]) |
|
259 |
|
260 insis.append(sf) |
|
261 |
|
262 # Temporarily remove the SISDataIndex SISField from the first SISController. |
|
263 ctrlfield = insis[0].Controller.Data |
|
264 didxfield = ctrlfield.DataIndex |
|
265 ctrlfield.DataIndex = None |
|
266 |
|
267 # Remove old signatures from the first SIS file. |
|
268 if len(ctrlfield.getsignatures()) > 0: |
|
269 print ("%s: warning: removing old signatures " |
|
270 "from the first input SIS file" % pgmname) |
|
271 ctrlfield.setsignatures([]) |
|
272 |
|
273 for n in xrange(1, len(insis)): |
|
274 # Append SISDataUnit SISFields into SISData array of the first SIS file. |
|
275 insis[0].Data.DataUnits.append(insis[n].Data.DataUnits[0]) |
|
276 |
|
277 # Set data index in SISController SISField. |
|
278 insis[n].Controller.Data.DataIndex.DataIndex = n |
|
279 |
|
280 # Embed SISController into SISInstallBlock of the first SIS file. |
|
281 ctrlfield.InstallBlock.EmbeddedSISFiles.append(insis[n].Controller.Data) |
|
282 |
|
283 # Calculate a signature of the modified SISController. |
|
284 string = ctrlfield.tostring() |
|
285 string = sisfield.stripheaderandpadding(string) |
|
286 signature, algoid = sisfile.signstring(privkeydata, passphrase, string) |
|
287 |
|
288 # Create a SISCertificateChain SISField from certificate data. |
|
289 sf1 = sisfield.SISBlob(Data = cryptutil.certtobinary(certdata)) |
|
290 sf2 = sisfield.SISCertificateChain(CertificateData = sf1) |
|
291 |
|
292 # Create a SISSignature SISField from calculated signature. |
|
293 sf3 = sisfield.SISString(String = algoid) |
|
294 sf4 = sisfield.SISSignatureAlgorithm(AlgorithmIdentifier = sf3) |
|
295 sf5 = sisfield.SISBlob(Data = signature) |
|
296 sf6 = sisfield.SISSignature(SignatureAlgorithm = sf4, SignatureData = sf5) |
|
297 |
|
298 # Create a new SISSignatureCertificateChain SISField. |
|
299 sa = sisfield.SISArray(SISFields = [sf6]) |
|
300 sf7 = sisfield.SISSignatureCertificateChain(Signatures = sa, |
|
301 CertificateChain = sf2) |
|
302 |
|
303 # Set certificate, restore data index. |
|
304 ctrlfield.Signature0 = sf7 |
|
305 ctrlfield.DataIndex = didxfield |
|
306 |
|
307 # Convert SISFields to string. |
|
308 outstring = insis[0].tostring() |
|
309 |
|
310 # Write output SIS file. |
|
311 f = file(outfile, "wb") |
|
312 f.write(uids) |
|
313 f.write(outstring) |
|
314 f.close() |