src/tools/py2sis/ensymble/cmd_mergesis.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_mergesis.py - Ensymble command line tool, mergesis command
       
     6 # Copyright 2006, 2007 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 
       
    31 import sisfile
       
    32 import sisfield
       
    33 import cryptutil
       
    34 
       
    35 
       
    36 ##############################################################################
       
    37 # Help texts
       
    38 ##############################################################################
       
    39 
       
    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>
       
    45 
       
    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.
       
    50 
       
    51 Options:
       
    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
       
    60 
       
    61 Merging SIS files that already contain other SIS files is not supported.
       
    62 '''
       
    63 
       
    64 
       
    65 ##############################################################################
       
    66 # Parameters
       
    67 ##############################################################################
       
    68 
       
    69 MAXPASSPHRASELENGTH     = 256
       
    70 MAXCERTIFICATELENGTH    = 65536
       
    71 MAXPRIVATEKEYLENGTH     = 65536
       
    72 MAXSISFILESIZE          = 1024 * 1024 * 8   # Eight megabytes
       
    73 
       
    74 
       
    75 ##############################################################################
       
    76 # Global variables
       
    77 ##############################################################################
       
    78 
       
    79 debug = False
       
    80 
       
    81 
       
    82 ##############################################################################
       
    83 # Public module-level functions
       
    84 ##############################################################################
       
    85 
       
    86 def run(pgmname, argv):
       
    87     global debug
       
    88 
       
    89     # Determine system character encodings.
       
    90     try:
       
    91         # getdefaultlocale() may sometimes return None.
       
    92         # Fall back to ASCII encoding in that case.
       
    93         terminalenc = locale.getdefaultlocale()[1] + ""
       
    94     except TypeError:
       
    95         # Invalid locale, fall back to ASCII terminal encoding.
       
    96         terminalenc = "ascii"
       
    97 
       
    98     try:
       
    99         # sys.getfilesystemencoding() was introduced in Python v2.3 and
       
   100         # it can sometimes return None. Fall back to ASCII if something
       
   101         # goes wrong.
       
   102         filesystemenc = sys.getfilesystemencoding() + ""
       
   103     except (AttributeError, TypeError):
       
   104         filesystemenc = "ascii"
       
   105 
       
   106     try:
       
   107         gopt = getopt.gnu_getopt
       
   108     except:
       
   109         # Python <v2.3, GNU-style parameter ordering not supported.
       
   110         gopt = getopt.getopt
       
   111 
       
   112     # Parse command line arguments.
       
   113     short_opts = "a:k:p:e:vh"
       
   114     long_opts = [
       
   115         "cert=", "privkey=", "passphrase=",
       
   116         "encoding=", "verbose", "debug", "help"
       
   117     ]
       
   118     args = gopt(argv, short_opts, long_opts)
       
   119 
       
   120     opts = dict(args[0])
       
   121     pargs = args[1]
       
   122 
       
   123     if len(pargs) < 2:
       
   124         raise ValueError("wrong number of arguments")
       
   125 
       
   126     # Override character encoding of command line and filesystem.
       
   127     encs = opts.get("--encoding", opts.get("-e", "%s,%s" % (terminalenc,
       
   128                                                             filesystemenc)))
       
   129     try:
       
   130         terminalenc, filesystemenc = encs.split(",")
       
   131     except (ValueError, TypeError):
       
   132         raise ValueError("invalid encoding string '%s'" % encs)
       
   133 
       
   134     # Get input SIS file names.
       
   135     infiles = [f.decode(terminalenc).encode(filesystemenc) for f in pargs[:-1]]
       
   136 
       
   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]))
       
   142 
       
   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)
       
   150 
       
   151         # Read certificate file.
       
   152         f = file(cert, "rb")
       
   153         certdata = f.read(MAXCERTIFICATELENGTH + 1)
       
   154         f.close()
       
   155 
       
   156         if len(certdata) > MAXCERTIFICATELENGTH:
       
   157             raise ValueError("certificate file too large")
       
   158 
       
   159         # Read private key file.
       
   160         f = file(privkey, "rb")
       
   161         privkeydata = f.read(MAXPRIVATEKEYLENGTH + 1)
       
   162         f.close()
       
   163 
       
   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.
       
   170         import defaultcert
       
   171         certdata = defaultcert.cert
       
   172         privkeydata = defaultcert.privkey
       
   173 
       
   174         print ("%s: warning: no certificate given, using "
       
   175                "insecure built-in one" % pgmname)
       
   176     else:
       
   177         raise ValueError("missing certificate or private key")
       
   178 
       
   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:")
       
   186         else:
       
   187             # Not connected to a TTY, read stdin non-interactively instead.
       
   188             passphrase = sys.stdin.read(MAXPASSPHRASELENGTH + 1)
       
   189 
       
   190             if len(passphrase) > MAXPASSPHRASELENGTH:
       
   191                 raise ValueError("pass phrase too long")
       
   192 
       
   193             passphrase = passphrase.strip()
       
   194 
       
   195     # Determine verbosity.
       
   196     verbose = False
       
   197     if "--verbose" in opts.keys() or "-v" in opts.keys():
       
   198         verbose = True
       
   199 
       
   200     # Determine if debug output is requested.
       
   201     if "--debug" in opts.keys():
       
   202         debug = True
       
   203 
       
   204         # Enable debug output for OpenSSL-related functions.
       
   205         cryptutil.setdebug(True)
       
   206 
       
   207     # Ingredients for successful SIS generation:
       
   208     #
       
   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
       
   217 
       
   218     if verbose:
       
   219         print
       
   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>")
       
   228         print
       
   229 
       
   230     insis = []
       
   231     for n in xrange(len(infiles)):
       
   232         # Read input SIS files.
       
   233         f = file(infiles[n], "rb")
       
   234         instring = f.read(MAXSISFILESIZE + 1)
       
   235         f.close()
       
   236 
       
   237         if len(instring) > MAXSISFILESIZE:
       
   238             raise ValueError("%s: input SIS file too large" % infiles[n])
       
   239 
       
   240         if n == 0:
       
   241             # Store UIDs for later use.
       
   242             uids = instring[:16]    # UID1, UID2, UID3 and UIDCRC
       
   243 
       
   244         # Convert input SIS file to SISFields.
       
   245         sf, rlen = sisfield.SISField(instring[16:], False)
       
   246 
       
   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))))
       
   251 
       
   252         # Try to release some memory early.
       
   253         del instring
       
   254 
       
   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])
       
   259 
       
   260         insis.append(sf)
       
   261 
       
   262     # Temporarily remove the SISDataIndex SISField from the first SISController.
       
   263     ctrlfield = insis[0].Controller.Data
       
   264     didxfield = ctrlfield.DataIndex
       
   265     ctrlfield.DataIndex = None
       
   266 
       
   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([])
       
   272 
       
   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])
       
   276 
       
   277         # Set data index in SISController SISField.
       
   278         insis[n].Controller.Data.DataIndex.DataIndex = n
       
   279 
       
   280         # Embed SISController into SISInstallBlock of the first SIS file.
       
   281         ctrlfield.InstallBlock.EmbeddedSISFiles.append(insis[n].Controller.Data)
       
   282 
       
   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)
       
   287 
       
   288     # Create a SISCertificateChain SISField from certificate data.
       
   289     sf1 = sisfield.SISBlob(Data = cryptutil.certtobinary(certdata))
       
   290     sf2 = sisfield.SISCertificateChain(CertificateData = sf1)
       
   291 
       
   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)
       
   297 
       
   298     # Create a new SISSignatureCertificateChain SISField.
       
   299     sa  = sisfield.SISArray(SISFields = [sf6])
       
   300     sf7 = sisfield.SISSignatureCertificateChain(Signatures = sa,
       
   301                                                 CertificateChain = sf2)
       
   302 
       
   303     # Set certificate, restore data index.
       
   304     ctrlfield.Signature0 = sf7
       
   305     ctrlfield.DataIndex = didxfield
       
   306 
       
   307     # Convert SISFields to string.
       
   308     outstring = insis[0].tostring()
       
   309 
       
   310     # Write output SIS file.
       
   311     f = file(outfile, "wb")
       
   312     f.write(uids)
       
   313     f.write(outstring)
       
   314     f.close()