|
1 #!/usr/bin/env python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 ############################################################################## |
|
5 # cmd_signsis.py - Ensymble command line tool, signsis command |
|
6 # Copyright 2006, 2007, 2008, 2009 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 import struct |
|
31 import sha |
|
32 |
|
33 import sisfile |
|
34 import sisfield |
|
35 import symbianutil |
|
36 import cryptutil |
|
37 |
|
38 |
|
39 ############################################################################## |
|
40 # Help texts |
|
41 ############################################################################## |
|
42 |
|
43 shorthelp = 'Sign a SIS package' |
|
44 longhelp = '''signsis |
|
45 [--unsign] [--cert=mycert.cer] [--privkey=mykey.key] [--passphrase=12345] |
|
46 [--execaps=Cap1+Cap2+...] [--dllcaps=Cap1+Cap2+...] |
|
47 [--encoding=terminal,filesystem] [--verbose] |
|
48 <infile> [outfile] |
|
49 |
|
50 Sign a SIS file with the certificate provided (stripping out any |
|
51 existing certificates, if any). Optionally modify capabilities of |
|
52 all EXE and DLL files contained in the SIS package. |
|
53 |
|
54 Options: |
|
55 infile - Path of the original SIS file |
|
56 outfile - Path of the signed SIS file (or the original is overwritten) |
|
57 unsign - Remove all signatures from SIS file instead of signing |
|
58 cert - Certificate to use for signing (PEM format) |
|
59 privkey - Private key of the certificate (PEM format) |
|
60 passphrase - Pass phrase of the private key (insecure, use stdin instead) |
|
61 execaps - Capability names, separated by "+" (not altered by default) |
|
62 dllcaps - Capability names, separated by "+" (not altered by default) |
|
63 encoding - Local character encodings for terminal and filesystem |
|
64 verbose - Print extra statistics |
|
65 |
|
66 If no certificate and its private key are given, a default self-signed |
|
67 certificate is used to sign the SIS file. Software authors are encouraged |
|
68 to create their own unique certificates for SIS packages that are to be |
|
69 distributed. |
|
70 |
|
71 Embedded SIS files are ignored, i.e their certificates are not modified. |
|
72 Also, capabilities of EXE and DLL files inside embedded SIS files are |
|
73 not affected. |
|
74 ''' |
|
75 |
|
76 |
|
77 ############################################################################## |
|
78 # Parameters |
|
79 ############################################################################## |
|
80 |
|
81 MAXPASSPHRASELENGTH = 256 |
|
82 MAXCERTIFICATELENGTH = 65536 |
|
83 MAXPRIVATEKEYLENGTH = 65536 |
|
84 MAXSISFILESIZE = 1024 * 1024 * 8 # Eight megabytes |
|
85 |
|
86 |
|
87 ############################################################################## |
|
88 # Global variables |
|
89 ############################################################################## |
|
90 |
|
91 debug = False |
|
92 |
|
93 |
|
94 ############################################################################## |
|
95 # Public module-level functions |
|
96 ############################################################################## |
|
97 |
|
98 def run(pgmname, argv): |
|
99 global debug |
|
100 |
|
101 # Determine system character encodings. |
|
102 try: |
|
103 # getdefaultlocale() may sometimes return None. |
|
104 # Fall back to ASCII encoding in that case. |
|
105 terminalenc = locale.getdefaultlocale()[1] + "" |
|
106 except TypeError: |
|
107 # Invalid locale, fall back to ASCII terminal encoding. |
|
108 terminalenc = "ascii" |
|
109 |
|
110 try: |
|
111 # sys.getfilesystemencoding() was introduced in Python v2.3 and |
|
112 # it can sometimes return None. Fall back to ASCII if something |
|
113 # goes wrong. |
|
114 filesystemenc = sys.getfilesystemencoding() + "" |
|
115 except (AttributeError, TypeError): |
|
116 filesystemenc = "ascii" |
|
117 |
|
118 try: |
|
119 gopt = getopt.gnu_getopt |
|
120 except: |
|
121 # Python <v2.3, GNU-style parameter ordering not supported. |
|
122 gopt = getopt.getopt |
|
123 |
|
124 # Parse command line arguments. |
|
125 short_opts = "ua:k:p:b:d:e:vh" |
|
126 long_opts = [ |
|
127 "unsign", "cert=", "privkey=", "passphrase=", "execaps=", |
|
128 "dllcaps=", "encoding=", "verbose", "debug", "help" |
|
129 ] |
|
130 args = gopt(argv, short_opts, long_opts) |
|
131 |
|
132 opts = dict(args[0]) |
|
133 pargs = args[1] |
|
134 |
|
135 if len(pargs) == 0: |
|
136 raise ValueError("no SIS file name given") |
|
137 |
|
138 # Override character encoding of command line and filesystem. |
|
139 encs = opts.get("--encoding", opts.get("-e", "%s,%s" % (terminalenc, |
|
140 filesystemenc))) |
|
141 try: |
|
142 terminalenc, filesystemenc = encs.split(",") |
|
143 except (ValueError, TypeError): |
|
144 raise ValueError("invalid encoding string '%s'" % encs) |
|
145 |
|
146 # Get input SIS file name. |
|
147 infile = pargs[0].decode(terminalenc).encode(filesystemenc) |
|
148 |
|
149 # Determine output SIS file name. |
|
150 if len(pargs) == 1: |
|
151 # No output file, overwrite original SIS file. |
|
152 outfile = infile |
|
153 elif len(pargs) == 2: |
|
154 outfile = pargs[1].decode(terminalenc).encode(filesystemenc) |
|
155 if os.path.isdir(outfile): |
|
156 # Output to directory, use input file name. |
|
157 outfile = os.path.join(outfile, os.path.basename(infile)) |
|
158 else: |
|
159 raise ValueError("wrong number of arguments") |
|
160 |
|
161 # Get unsign option. |
|
162 unsign = False |
|
163 if "--unsign" in opts.keys() or "-u" in opts.keys(): |
|
164 unsign = True |
|
165 |
|
166 # Get certificate and its private key file names. |
|
167 cert = opts.get("--cert", opts.get("-a", None)) |
|
168 privkey = opts.get("--privkey", opts.get("-k", None)) |
|
169 if unsign: |
|
170 if cert != None or privkey != None: |
|
171 raise ValueError("certificate or private key given when unsigning") |
|
172 elif cert != None and privkey != None: |
|
173 # Convert file names from terminal encoding to filesystem encoding. |
|
174 cert = cert.decode(terminalenc).encode(filesystemenc) |
|
175 privkey = privkey.decode(terminalenc).encode(filesystemenc) |
|
176 |
|
177 # Read certificate file. |
|
178 f = file(cert, "rb") |
|
179 certdata = f.read(MAXCERTIFICATELENGTH + 1) |
|
180 f.close() |
|
181 |
|
182 if len(certdata) > MAXCERTIFICATELENGTH: |
|
183 raise ValueError("certificate file too large") |
|
184 |
|
185 # Read private key file. |
|
186 f = file(privkey, "rb") |
|
187 privkeydata = f.read(MAXPRIVATEKEYLENGTH + 1) |
|
188 f.close() |
|
189 |
|
190 if len(privkeydata) > MAXPRIVATEKEYLENGTH: |
|
191 raise ValueError("private key file too large") |
|
192 elif cert == None and privkey == None: |
|
193 # No certificate given, use the Ensymble default certificate. |
|
194 # defaultcert.py is not imported when not needed. This speeds |
|
195 # up program start-up a little. |
|
196 import defaultcert |
|
197 certdata = defaultcert.cert |
|
198 privkeydata = defaultcert.privkey |
|
199 |
|
200 print ("%s: warning: no certificate given, using " |
|
201 "insecure built-in one" % pgmname) |
|
202 else: |
|
203 raise ValueError("missing certificate or private key") |
|
204 |
|
205 # Get pass phrase. Pass phrase remains in terminal encoding. |
|
206 passphrase = opts.get("--passphrase", opts.get("-p", None)) |
|
207 if passphrase == None and privkey != None: |
|
208 # Private key given without "--passphrase" option, ask it. |
|
209 if sys.stdin.isatty(): |
|
210 # Standard input is a TTY, ask password interactively. |
|
211 passphrase = getpass.getpass("Enter private key pass phrase:") |
|
212 else: |
|
213 # Not connected to a TTY, read stdin non-interactively instead. |
|
214 passphrase = sys.stdin.read(MAXPASSPHRASELENGTH + 1) |
|
215 |
|
216 if len(passphrase) > MAXPASSPHRASELENGTH: |
|
217 raise ValueError("pass phrase too long") |
|
218 |
|
219 passphrase = passphrase.strip() |
|
220 |
|
221 # Get EXE capabilities and normalize the names. |
|
222 execaps = opts.get("--execaps", opts.get("-b", None)) |
|
223 if execaps != None: |
|
224 execapmask = symbianutil.capstringtomask(execaps) |
|
225 execaps = symbianutil.capmasktostring(execapmask, True) |
|
226 else: |
|
227 execapmask = None |
|
228 |
|
229 # Get DLL capabilities and normalize the names. |
|
230 dllcaps = opts.get("--dllcaps", opts.get("-d", None)) |
|
231 if dllcaps != None: |
|
232 dllcapmask = symbianutil.capstringtomask(dllcaps) |
|
233 dllcaps = symbianutil.capmasktostring(dllcapmask, True) |
|
234 else: |
|
235 dllcapmask = None |
|
236 |
|
237 # Determine verbosity. |
|
238 verbose = False |
|
239 if "--verbose" in opts.keys() or "-v" in opts.keys(): |
|
240 verbose = True |
|
241 |
|
242 # Determine if debug output is requested. |
|
243 if "--debug" in opts.keys(): |
|
244 debug = True |
|
245 |
|
246 # Enable debug output for OpenSSL-related functions. |
|
247 cryptutil.setdebug(True) |
|
248 |
|
249 # Ingredients for successful SIS generation: |
|
250 # |
|
251 # terminalenc Terminal character encoding (autodetected) |
|
252 # filesystemenc File system name encoding (autodetected) |
|
253 # infile Input SIS file name, filesystemenc encoded |
|
254 # outfile Output SIS file name, filesystemenc encoded |
|
255 # cert Certificate in PEM format |
|
256 # privkey Certificate private key in PEM format |
|
257 # passphrase Pass phrase of priv. key, terminalenc encoded string |
|
258 # execaps, execapmask Capability names and bitmask for EXE files or None |
|
259 # dllcaps, dllcapmask Capability names and bitmask for DLL files or None |
|
260 # verbose Boolean indicating verbose terminal output |
|
261 |
|
262 if verbose: |
|
263 print |
|
264 print "Input SIS file %s" % ( |
|
265 infile.decode(filesystemenc).encode(terminalenc)) |
|
266 print "Output SIS file %s" % ( |
|
267 outfile.decode(filesystemenc).encode(terminalenc)) |
|
268 if unsign: |
|
269 print "Remove signatures Yes" |
|
270 else: |
|
271 print "Certificate %s" % ((cert and |
|
272 cert.decode(filesystemenc).encode(terminalenc)) or |
|
273 "<default>") |
|
274 print "Private key %s" % ((privkey and |
|
275 privkey.decode(filesystemenc).encode(terminalenc)) or |
|
276 "<default>") |
|
277 if execaps != None: |
|
278 print "EXE capabilities 0x%x (%s)" % (execapmask, execaps) |
|
279 else: |
|
280 print "EXE capabilities <not set>" |
|
281 if dllcaps != None: |
|
282 print "DLL capabilities 0x%x (%s)" % (dllcapmask, dllcaps) |
|
283 else: |
|
284 print "DLL capabilities <not set>" |
|
285 print |
|
286 |
|
287 # Read input SIS file. |
|
288 f = file(infile, "rb") |
|
289 instring = f.read(MAXSISFILESIZE + 1) |
|
290 f.close() |
|
291 |
|
292 if len(instring) > MAXSISFILESIZE: |
|
293 raise ValueError("input SIS file too large") |
|
294 |
|
295 # Convert input SIS file to SISFields. |
|
296 uids = instring[:16] # UID1, UID2, UID3 and UIDCRC |
|
297 insis, rlen = sisfield.SISField(instring[16:], False) |
|
298 |
|
299 # Ignore extra bytes after SIS file. |
|
300 if len(instring) > (rlen + 16): |
|
301 print ("%s: warning: %d extra bytes after input SIS file (ignored)" % |
|
302 (pgmname, (len(instring) - (rlen + 16)))) |
|
303 |
|
304 # Try to release some memory early. |
|
305 del instring |
|
306 |
|
307 # Check if there are embedded SIS files. Warn if there are. |
|
308 if len(insis.Data.DataUnits) > 1: |
|
309 print ("%s: warning: input SIS file contains " |
|
310 "embedded SIS files (ignored)" % pgmname) |
|
311 |
|
312 # Modify EXE- and DLL-files according to new capabilities. |
|
313 if execaps != None or dllcaps != None: |
|
314 # Generate FileIndex to SISFileDescription mapping. |
|
315 sisfiledescmap = mapfiledesc(insis.Controller.Data.InstallBlock) |
|
316 |
|
317 exemods, dllmods = modifycaps(insis, sisfiledescmap, |
|
318 execapmask, dllcapmask) |
|
319 print ("%s: %d EXE-files will be modified, " |
|
320 "%d DLL-files will be modified" % (pgmname, exemods, dllmods)) |
|
321 |
|
322 # Temporarily remove the SISDataIndex SISField from SISController. |
|
323 ctrlfield = insis.Controller.Data |
|
324 didxfield = ctrlfield.DataIndex |
|
325 ctrlfield.DataIndex = None |
|
326 |
|
327 if not unsign: |
|
328 # Remove old signatures. |
|
329 if len(ctrlfield.getsignatures()) > 0: |
|
330 print ("%s: warning: removing old signatures " |
|
331 "from input SIS file" % pgmname) |
|
332 ctrlfield.setsignatures([]) |
|
333 |
|
334 # Calculate a signature of the modified SISController. |
|
335 string = ctrlfield.tostring() |
|
336 string = sisfield.stripheaderandpadding(string) |
|
337 signature, algoid = sisfile.signstring(privkeydata, passphrase, string) |
|
338 |
|
339 # Create a SISCertificateChain SISField from certificate data. |
|
340 sf1 = sisfield.SISBlob(Data = cryptutil.certtobinary(certdata)) |
|
341 sf2 = sisfield.SISCertificateChain(CertificateData = sf1) |
|
342 |
|
343 # Create a SISSignature SISField from calculated signature. |
|
344 sf3 = sisfield.SISString(String = algoid) |
|
345 sf4 = sisfield.SISSignatureAlgorithm(AlgorithmIdentifier = sf3) |
|
346 sf5 = sisfield.SISBlob(Data = signature) |
|
347 sf6 = sisfield.SISSignature(SignatureAlgorithm = sf4, |
|
348 SignatureData = sf5) |
|
349 |
|
350 # Create a new SISSignatureCertificateChain SISField. |
|
351 sa = sisfield.SISArray(SISFields = [sf6]) |
|
352 sf7 = sisfield.SISSignatureCertificateChain(Signatures = sa, |
|
353 CertificateChain = sf2) |
|
354 |
|
355 # Set new certificate. |
|
356 ctrlfield.Signature0 = sf7 |
|
357 else: |
|
358 # Unsign, remove old signatures. |
|
359 ctrlfield.setsignatures([]) |
|
360 |
|
361 # Restore data index. |
|
362 ctrlfield.DataIndex = didxfield |
|
363 |
|
364 # Convert SISFields to string. |
|
365 outstring = insis.tostring() |
|
366 |
|
367 # Write output SIS file. |
|
368 f = file(outfile, "wb") |
|
369 f.write(uids) |
|
370 f.write(outstring) |
|
371 f.close() |
|
372 |
|
373 |
|
374 ############################################################################## |
|
375 # Module-level functions which are normally only used by this module |
|
376 ############################################################################## |
|
377 |
|
378 def modifycaps(siscontents, sisfiledescmap, execapmask, dllcapmask): |
|
379 '''Scan SISData SISFields for EXE- and DLL-files |
|
380 and modify their headers for the new capabilities.''' |
|
381 |
|
382 # Prepare UID1 strings for EXE and DLL. |
|
383 exeuids = struct.pack("<L", 0x1000007AL) |
|
384 dlluids = struct.pack("<L", 0x10000079L) |
|
385 |
|
386 exemods = 0 |
|
387 dllmods = 0 |
|
388 |
|
389 # Only examine the first SISDataUnit. Ignore embedded SIS files. |
|
390 sisfiledata = siscontents.Data.DataUnits[0].FileData |
|
391 |
|
392 for fileindex in xrange(len(sisfiledata)): |
|
393 capmask = None |
|
394 |
|
395 # Get file contents (uncompressed). |
|
396 contents = sisfiledata[fileindex].FileData.Data |
|
397 |
|
398 # Determine file type. |
|
399 if execapmask != None and contents[:4] == exeuids: |
|
400 capmask = execapmask |
|
401 exemods += 1 |
|
402 elif dllcapmask != None and contents[:4] == dlluids: |
|
403 capmask = dllcapmask |
|
404 dllmods += 1 |
|
405 |
|
406 if capmask != None: |
|
407 # Modify capabilities contained in the E32Image header. |
|
408 contents = symbianutil.e32imagecrc(contents, capabilities = capmask) |
|
409 |
|
410 # Replace file contents. |
|
411 sisfiledata[fileindex].FileData.Data = contents |
|
412 |
|
413 # Find the SISFileDescription SISField for this file index. |
|
414 try: |
|
415 sisfiledesc = sisfiledescmap[fileindex] |
|
416 except KeyError: |
|
417 # No file index found, SIS file is probably corrupted. |
|
418 raise ValueError("missing file metadata in input SIS file") |
|
419 |
|
420 # Set new capabilities in the SISFileDescription SISField. |
|
421 if capmask != 0: |
|
422 capstring = symbianutil.capmasktorawdata(capmask) |
|
423 capfield = sisfield.SISCapabilities(Capabilities = capstring) |
|
424 sisfiledesc.Capabilities = capfield |
|
425 else: |
|
426 # If capability mask is 0, no capability field is generated. |
|
427 # Otherwise the original signsis.exe from Symbian cannot |
|
428 # sign the resulting SIS file. |
|
429 sisfiledesc.Capabilities = None |
|
430 |
|
431 # Re-calculate file hash in the SISFileDescription SISField. |
|
432 sha1hash = sha.new(contents).digest() |
|
433 hashblob = sisfield.SISBlob(Data = sha1hash) |
|
434 hashfield = sisfield.SISHash(HashAlgorithm = |
|
435 sisfield.ESISHashAlgSHA1, |
|
436 HashData = hashblob) |
|
437 sisfiledesc.Hash = hashfield |
|
438 |
|
439 if debug: |
|
440 # Print target names of modified files. |
|
441 print sisfiledesc.Target.String |
|
442 |
|
443 return (exemods, dllmods) |
|
444 |
|
445 def mapfiledesc(sisinstallblock, sisfiledescmap = {}): |
|
446 '''Recursively scan SISInstallBlocks for file indexes in |
|
447 SISFileDescription SISFields.''' |
|
448 |
|
449 # First add normal files to SISFileDescription file index map. |
|
450 for filedesc in sisinstallblock.Files: |
|
451 idx = filedesc.FileIndex |
|
452 if idx in sisfiledescmap.keys(): |
|
453 # In theory, SIS files could re-use file data by using the |
|
454 # same file index in more than one place. This special case |
|
455 # is not supported, for now. |
|
456 raise ValueError("duplicate file index in input SIS file") |
|
457 sisfiledescmap[idx] = filedesc |
|
458 |
|
459 # Then, recursively call mapfiledesc() for SISIf and SISElseIf SISArrays. |
|
460 for sisif in sisinstallblock.IfBlocks: |
|
461 mapfiledesc(sisif.InstallBlock, sisfiledescmap) # Map modified in-place. |
|
462 |
|
463 for siselseif in sisif.ElseIfs: |
|
464 mapfiledesc(siselseif.InstallBlock, sisfiledescmap) |
|
465 |
|
466 return sisfiledescmap |