~ubuntu-branches/ubuntu/utopic/ensymble/utopic

« back to all changes in this revision

Viewing changes to cmd_signsis.py

  • Committer: Package Import Robot
  • Author(s): Logan Rosen
  • Date: 2013-05-09 17:13:58 UTC
  • mfrom: (7.1.1 sid)
  • Revision ID: package-import@ubuntu.com-20130509171358-2jblsah6kes5lhub
Tags: 0.29-1ubuntu1
* Merge from Debian unstable. Remaining changes:
  - debian/control X-Python-Version: Use >= 2.6, instead of now gone "all"
    keyword.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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