2
# -*- coding: utf-8 -*-
4
##############################################################################
5
# cmd_signsis.py - Ensymble command line tool, signsis command
6
# Copyright 2006, 2007, 2008, 2009 Jussi Ylänen
8
# This file is part of Ensymble developer utilities for Symbian OS(TM).
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.
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.
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
##############################################################################
39
##############################################################################
41
##############################################################################
43
shorthelp = 'Sign a SIS package'
45
[--unsign] [--cert=mycert.cer] [--privkey=mykey.key] [--passphrase=12345]
46
[--execaps=Cap1+Cap2+...] [--dllcaps=Cap1+Cap2+...]
47
[--encoding=terminal,filesystem] [--verbose]
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.
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
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
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
77
##############################################################################
79
##############################################################################
81
MAXPASSPHRASELENGTH = 256
82
MAXCERTIFICATELENGTH = 65536
83
MAXPRIVATEKEYLENGTH = 65536
84
MAXSISFILESIZE = 1024 * 1024 * 8 # Eight megabytes
87
##############################################################################
89
##############################################################################
94
##############################################################################
95
# Public module-level functions
96
##############################################################################
98
def run(pgmname, argv):
101
# Determine system character encodings.
103
# getdefaultlocale() may sometimes return None.
104
# Fall back to ASCII encoding in that case.
105
terminalenc = locale.getdefaultlocale()[1] + ""
107
# Invalid locale, fall back to ASCII terminal encoding.
108
terminalenc = "ascii"
111
# sys.getfilesystemencoding() was introduced in Python v2.3 and
112
# it can sometimes return None. Fall back to ASCII if something
114
filesystemenc = sys.getfilesystemencoding() + ""
115
except (AttributeError, TypeError):
116
filesystemenc = "ascii"
119
gopt = getopt.gnu_getopt
121
# Python <v2.3, GNU-style parameter ordering not supported.
124
# Parse command line arguments.
125
short_opts = "ua:k:p:b:d:e:vh"
127
"unsign", "cert=", "privkey=", "passphrase=", "execaps=",
128
"dllcaps=", "encoding=", "verbose", "debug", "help"
130
args = gopt(argv, short_opts, long_opts)
136
raise ValueError("no SIS file name given")
138
# Override character encoding of command line and filesystem.
139
encs = opts.get("--encoding", opts.get("-e", "%s,%s" % (terminalenc,
142
terminalenc, filesystemenc = encs.split(",")
143
except (ValueError, TypeError):
144
raise ValueError("invalid encoding string '%s'" % encs)
146
# Get input SIS file name.
147
infile = pargs[0].decode(terminalenc).encode(filesystemenc)
149
# Determine output SIS file name.
151
# No output file, overwrite original SIS file.
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))
159
raise ValueError("wrong number of arguments")
163
if "--unsign" in opts.keys() or "-u" in opts.keys():
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))
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)
177
# Read certificate file.
179
certdata = f.read(MAXCERTIFICATELENGTH + 1)
182
if len(certdata) > MAXCERTIFICATELENGTH:
183
raise ValueError("certificate file too large")
185
# Read private key file.
186
f = file(privkey, "rb")
187
privkeydata = f.read(MAXPRIVATEKEYLENGTH + 1)
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.
197
certdata = defaultcert.cert
198
privkeydata = defaultcert.privkey
200
print ("%s: warning: no certificate given, using "
201
"insecure built-in one" % pgmname)
203
raise ValueError("missing certificate or private key")
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:")
213
# Not connected to a TTY, read stdin non-interactively instead.
214
passphrase = sys.stdin.read(MAXPASSPHRASELENGTH + 1)
216
if len(passphrase) > MAXPASSPHRASELENGTH:
217
raise ValueError("pass phrase too long")
219
passphrase = passphrase.strip()
221
# Get EXE capabilities and normalize the names.
222
execaps = opts.get("--execaps", opts.get("-b", None))
224
execapmask = symbianutil.capstringtomask(execaps)
225
execaps = symbianutil.capmasktostring(execapmask, True)
229
# Get DLL capabilities and normalize the names.
230
dllcaps = opts.get("--dllcaps", opts.get("-d", None))
232
dllcapmask = symbianutil.capstringtomask(dllcaps)
233
dllcaps = symbianutil.capmasktostring(dllcapmask, True)
237
# Determine verbosity.
239
if "--verbose" in opts.keys() or "-v" in opts.keys():
242
# Determine if debug output is requested.
243
if "--debug" in opts.keys():
246
# Enable debug output for OpenSSL-related functions.
247
cryptutil.setdebug(True)
249
# Ingredients for successful SIS generation:
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
264
print "Input SIS file %s" % (
265
infile.decode(filesystemenc).encode(terminalenc))
266
print "Output SIS file %s" % (
267
outfile.decode(filesystemenc).encode(terminalenc))
269
print "Remove signatures Yes"
271
print "Certificate %s" % ((cert and
272
cert.decode(filesystemenc).encode(terminalenc)) or
274
print "Private key %s" % ((privkey and
275
privkey.decode(filesystemenc).encode(terminalenc)) or
278
print "EXE capabilities 0x%x (%s)" % (execapmask, execaps)
280
print "EXE capabilities <not set>"
282
print "DLL capabilities 0x%x (%s)" % (dllcapmask, dllcaps)
284
print "DLL capabilities <not set>"
287
# Read input SIS file.
288
f = file(infile, "rb")
289
instring = f.read(MAXSISFILESIZE + 1)
292
if len(instring) > MAXSISFILESIZE:
293
raise ValueError("input SIS file too large")
295
# Convert input SIS file to SISFields.
296
uids = instring[:16] # UID1, UID2, UID3 and UIDCRC
297
insis, rlen = sisfield.SISField(instring[16:], False)
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))))
304
# Try to release some memory early.
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)
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)
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))
322
# Temporarily remove the SISDataIndex SISField from SISController.
323
ctrlfield = insis.Controller.Data
324
didxfield = ctrlfield.DataIndex
325
ctrlfield.DataIndex = None
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([])
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)
339
# Create a SISCertificateChain SISField from certificate data.
340
sf1 = sisfield.SISBlob(Data = cryptutil.certtobinary(certdata))
341
sf2 = sisfield.SISCertificateChain(CertificateData = sf1)
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,
350
# Create a new SISSignatureCertificateChain SISField.
351
sa = sisfield.SISArray(SISFields = [sf6])
352
sf7 = sisfield.SISSignatureCertificateChain(Signatures = sa,
353
CertificateChain = sf2)
355
# Set new certificate.
356
ctrlfield.Signature0 = sf7
358
# Unsign, remove old signatures.
359
ctrlfield.setsignatures([])
361
# Restore data index.
362
ctrlfield.DataIndex = didxfield
364
# Convert SISFields to string.
365
outstring = insis.tostring()
367
# Write output SIS file.
368
f = file(outfile, "wb")
374
##############################################################################
375
# Module-level functions which are normally only used by this module
376
##############################################################################
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.'''
382
# Prepare UID1 strings for EXE and DLL.
383
exeuids = struct.pack("<L", 0x1000007AL)
384
dlluids = struct.pack("<L", 0x10000079L)
389
# Only examine the first SISDataUnit. Ignore embedded SIS files.
390
sisfiledata = siscontents.Data.DataUnits[0].FileData
392
for fileindex in xrange(len(sisfiledata)):
395
# Get file contents (uncompressed).
396
contents = sisfiledata[fileindex].FileData.Data
398
# Determine file type.
399
if execapmask != None and contents[:4] == exeuids:
402
elif dllcapmask != None and contents[:4] == dlluids:
407
# Modify capabilities contained in the E32Image header.
408
contents = symbianutil.e32imagecrc(contents, capabilities = capmask)
410
# Replace file contents.
411
sisfiledata[fileindex].FileData.Data = contents
413
# Find the SISFileDescription SISField for this file index.
415
sisfiledesc = sisfiledescmap[fileindex]
417
# No file index found, SIS file is probably corrupted.
418
raise ValueError("missing file metadata in input SIS file")
420
# Set new capabilities in the SISFileDescription SISField.
422
capstring = symbianutil.capmasktorawdata(capmask)
423
capfield = sisfield.SISCapabilities(Capabilities = capstring)
424
sisfiledesc.Capabilities = capfield
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
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,
437
sisfiledesc.Hash = hashfield
440
# Print target names of modified files.
441
print sisfiledesc.Target.String
443
return (exemods, dllmods)
445
def mapfiledesc(sisinstallblock, sisfiledescmap = {}):
446
'''Recursively scan SISInstallBlocks for file indexes in
447
SISFileDescription SISFields.'''
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
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.
463
for siselseif in sisif.ElseIfs:
464
mapfiledesc(siselseif.InstallBlock, sisfiledescmap)
466
return sisfiledescmap