2
# -*- coding: utf-8 -*-
4
##############################################################################
5
# cmd_mergesis.py - Ensymble command line tool, mergesis command
6
# Copyright 2006, 2007 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
##############################################################################
36
##############################################################################
38
##############################################################################
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>
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.
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
61
Merging SIS files that already contain other SIS files is not supported.
65
##############################################################################
67
##############################################################################
69
MAXPASSPHRASELENGTH = 256
70
MAXCERTIFICATELENGTH = 65536
71
MAXPRIVATEKEYLENGTH = 65536
72
MAXSISFILESIZE = 1024 * 1024 * 8 # Eight megabytes
75
##############################################################################
77
##############################################################################
82
##############################################################################
83
# Public module-level functions
84
##############################################################################
86
def run(pgmname, argv):
89
# Determine system character encodings.
91
# getdefaultlocale() may sometimes return None.
92
# Fall back to ASCII encoding in that case.
93
terminalenc = locale.getdefaultlocale()[1] + ""
95
# Invalid locale, fall back to ASCII terminal encoding.
99
# sys.getfilesystemencoding() was introduced in Python v2.3 and
100
# it can sometimes return None. Fall back to ASCII if something
102
filesystemenc = sys.getfilesystemencoding() + ""
103
except (AttributeError, TypeError):
104
filesystemenc = "ascii"
107
gopt = getopt.gnu_getopt
109
# Python <v2.3, GNU-style parameter ordering not supported.
112
# Parse command line arguments.
113
short_opts = "a:k:p:e:vh"
115
"cert=", "privkey=", "passphrase=",
116
"encoding=", "verbose", "debug", "help"
118
args = gopt(argv, short_opts, long_opts)
124
raise ValueError("wrong number of arguments")
126
# Override character encoding of command line and filesystem.
127
encs = opts.get("--encoding", opts.get("-e", "%s,%s" % (terminalenc,
130
terminalenc, filesystemenc = encs.split(",")
131
except (ValueError, TypeError):
132
raise ValueError("invalid encoding string '%s'" % encs)
134
# Get input SIS file names.
135
infiles = [f.decode(terminalenc).encode(filesystemenc) for f in pargs[:-1]]
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]))
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)
151
# Read certificate file.
153
certdata = f.read(MAXCERTIFICATELENGTH + 1)
156
if len(certdata) > MAXCERTIFICATELENGTH:
157
raise ValueError("certificate file too large")
159
# Read private key file.
160
f = file(privkey, "rb")
161
privkeydata = f.read(MAXPRIVATEKEYLENGTH + 1)
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.
171
certdata = defaultcert.cert
172
privkeydata = defaultcert.privkey
174
print ("%s: warning: no certificate given, using "
175
"insecure built-in one" % pgmname)
177
raise ValueError("missing certificate or private key")
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:")
187
# Not connected to a TTY, read stdin non-interactively instead.
188
passphrase = sys.stdin.read(MAXPASSPHRASELENGTH + 1)
190
if len(passphrase) > MAXPASSPHRASELENGTH:
191
raise ValueError("pass phrase too long")
193
passphrase = passphrase.strip()
195
# Determine verbosity.
197
if "--verbose" in opts.keys() or "-v" in opts.keys():
200
# Determine if debug output is requested.
201
if "--debug" in opts.keys():
204
# Enable debug output for OpenSSL-related functions.
205
cryptutil.setdebug(True)
207
# Ingredients for successful SIS generation:
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
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>")
231
for n in xrange(len(infiles)):
232
# Read input SIS files.
233
f = file(infiles[n], "rb")
234
instring = f.read(MAXSISFILESIZE + 1)
237
if len(instring) > MAXSISFILESIZE:
238
raise ValueError("%s: input SIS file too large" % infiles[n])
241
# Store UIDs for later use.
242
uids = instring[:16] # UID1, UID2, UID3 and UIDCRC
244
# Convert input SIS file to SISFields.
245
sf, rlen = sisfield.SISField(instring[16:], False)
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))))
252
# Try to release some memory early.
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])
262
# Temporarily remove the SISDataIndex SISField from the first SISController.
263
ctrlfield = insis[0].Controller.Data
264
didxfield = ctrlfield.DataIndex
265
ctrlfield.DataIndex = None
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([])
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])
277
# Set data index in SISController SISField.
278
insis[n].Controller.Data.DataIndex.DataIndex = n
280
# Embed SISController into SISInstallBlock of the first SIS file.
281
ctrlfield.InstallBlock.EmbeddedSISFiles.append(insis[n].Controller.Data)
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)
288
# Create a SISCertificateChain SISField from certificate data.
289
sf1 = sisfield.SISBlob(Data = cryptutil.certtobinary(certdata))
290
sf2 = sisfield.SISCertificateChain(CertificateData = sf1)
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)
298
# Create a new SISSignatureCertificateChain SISField.
299
sa = sisfield.SISArray(SISFields = [sf6])
300
sf7 = sisfield.SISSignatureCertificateChain(Signatures = sa,
301
CertificateChain = sf2)
303
# Set certificate, restore data index.
304
ctrlfield.Signature0 = sf7
305
ctrlfield.DataIndex = didxfield
307
# Convert SISFields to string.
308
outstring = insis[0].tostring()
310
# Write output SIS file.
311
f = file(outfile, "wb")