src/tools/py2sis/ensymble/cmd_signsis.py
changeset 0 ca70ae20a155
equal deleted inserted replaced
-1:000000000000 0:ca70ae20a155
       
     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