src/tools/py2sis/ensymble/cryptutil.py
changeset 0 ca70ae20a155
equal deleted inserted replaced
-1:000000000000 0:ca70ae20a155
       
     1 #!/usr/bin/env python
       
     2 # -*- coding: utf-8 -*-
       
     3 
       
     4 ##############################################################################
       
     5 # cryptutil.py - OpenSSL command line utility wrappers for Ensymble
       
     6 # Copyright 2006, 2007, 2008 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 errno
       
    28 import tempfile
       
    29 import random
       
    30 
       
    31 
       
    32 opensslcommand = None   # Path to OpenSSL command line tool
       
    33 openssldebug   = False  # True for extra debug output
       
    34 
       
    35 
       
    36 ##############################################################################
       
    37 # Public module-level functions
       
    38 ##############################################################################
       
    39 
       
    40 def setdebug(active):
       
    41     '''
       
    42     Activate or deactivate debug output.
       
    43 
       
    44     setdebug(...) -> None
       
    45 
       
    46     active      Debug output enabled / disabled, a boolean value
       
    47 
       
    48     Debug output consists of OpenSSL binary command line and
       
    49     any output produced to the standard error stream by OpenSSL.
       
    50     '''
       
    51 
       
    52     global openssldebug
       
    53     openssldebug =  not not active  # Convert to boolean.
       
    54 
       
    55 def signstring(privkey, passphrase, string):
       
    56     '''
       
    57     Sign a binary string using a given private key and its pass phrase.
       
    58 
       
    59     signstring(...) -> (signature, keytype)
       
    60 
       
    61     privkey     RSA or DSA private key, a string in PEM (base-64) format
       
    62     passphrase  pass phrase for the private key, a non-Unicode string or None
       
    63     string      a binary string to sign
       
    64 
       
    65     signature   signature, an ASN.1 encoded binary string
       
    66     keytype     detected key type, string, "RSA" or "DSA"
       
    67 
       
    68     NOTE: On platforms with poor file system security, decrypted version
       
    69     of the private key may be grabbed from the temporary directory!
       
    70     '''
       
    71 
       
    72     if passphrase == None or len(passphrase) == 0:
       
    73         # OpenSSL does not like empty stdin while reading a passphrase from it.
       
    74         passphrase = "\n"
       
    75 
       
    76     # Create a temporary directory for OpenSSL to work in.
       
    77     tempdir = mkdtemp("ensymble-XXXXXX")
       
    78 
       
    79     keyfilename     = os.path.join(tempdir, "privkey.pem")
       
    80     sigfilename     = os.path.join(tempdir, "signature.dat")
       
    81     stringfilename  = os.path.join(tempdir, "string.dat")
       
    82 
       
    83     try:
       
    84         # If the private key is in PKCS#8 format, it needs to be converted.
       
    85         privkey = convertpkcs8key(tempdir, privkey, passphrase)
       
    86 
       
    87         # Decrypt the private key. Older versions of OpenSSL do not
       
    88         # accept the "-passin" parameter for the "dgst" command.
       
    89         privkey, keytype = decryptkey(tempdir, privkey, passphrase)
       
    90 
       
    91         if keytype == "DSA":
       
    92             signcmd = "-dss1"
       
    93         elif keytype == "RSA":
       
    94             signcmd = "-sha1"
       
    95         else:
       
    96             raise ValueError("unknown private key type %s" % keytype)
       
    97 
       
    98         # Write decrypted PEM format private key to file.
       
    99         keyfile = file(keyfilename, "wb")
       
   100         keyfile.write(privkey)
       
   101         keyfile.close()
       
   102 
       
   103         # Write binary string to a file. On some systems, stdin is
       
   104         # always in text mode and thus unsuitable for binary data.
       
   105         stringfile = file(stringfilename, "wb")
       
   106         stringfile.write(string)
       
   107         stringfile.close()
       
   108 
       
   109         # Sign binary string using the decrypted private key.
       
   110         command = ("dgst %s -binary -sign %s "
       
   111                    "-out %s %s") % (signcmd, quote(keyfilename),
       
   112                                     quote(sigfilename), quote(stringfilename))
       
   113         runopenssl(command)
       
   114 
       
   115         signature = ""
       
   116         if os.path.isfile(sigfilename):
       
   117             # Read signature from file.
       
   118             sigfile = file(sigfilename, "rb")
       
   119             signature = sigfile.read()
       
   120             sigfile.close()
       
   121 
       
   122         if signature.strip() == "":
       
   123             # OpenSSL did not create output, something went wrong.
       
   124             raise ValueError("unspecified error during signing")
       
   125     finally:
       
   126         # Delete temporary files.
       
   127         for fname in (keyfilename, sigfilename, stringfilename):
       
   128             try:
       
   129                 os.remove(fname)
       
   130             except OSError:
       
   131                pass
       
   132 
       
   133         # Remove temporary directory.
       
   134         os.rmdir(tempdir)
       
   135 
       
   136     return (signature, keytype)
       
   137 
       
   138 
       
   139 def certtobinary(pemcert):
       
   140     '''
       
   141     Convert X.509 certificates from PEM (base-64) format to DER (binary).
       
   142 
       
   143     certtobinary(...) -> dercert
       
   144 
       
   145     pemcert     One or more X.509 certificates in PEM (base-64) format, a string
       
   146 
       
   147     dercert     X.509 certificate(s), an ASN.1 encoded binary string
       
   148     '''
       
   149 
       
   150     # Find base-64 encoded data between header and footer.
       
   151     header = "-----BEGIN CERTIFICATE-----"
       
   152     footer = "-----END CERTIFICATE-----"
       
   153     endoffset = 0
       
   154     certs = []
       
   155     while True:
       
   156         # First find a header.
       
   157         startoffset = pemcert.find(header, endoffset)
       
   158         if startoffset < 0:
       
   159             # No header found, stop search.
       
   160             break
       
   161 
       
   162         startoffset += len(header)
       
   163 
       
   164         # Next find a footer.
       
   165         endoffset = pemcert.find(footer, startoffset)
       
   166         if endoffset < 0:
       
   167             # No footer found.
       
   168             raise ValueError("missing PEM certificate footer")
       
   169 
       
   170         # Extract the base-64 encoded certificate and decode it.
       
   171         try:
       
   172             cert = pemcert[startoffset:endoffset].decode("base-64")
       
   173         except:
       
   174             # Base-64 decoding error.
       
   175             raise ValueError("invalid PEM format certificate")
       
   176 
       
   177         certs.append(cert)
       
   178 
       
   179         endoffset += len(footer)
       
   180 
       
   181     if len(certs) == 0:
       
   182         raise ValueError("not a PEM format certificate")
       
   183 
       
   184     # DER certificates are simply raw binary versions
       
   185     # of the base-64 encoded PEM certificates.
       
   186     return "".join(certs)
       
   187 
       
   188 
       
   189 ##############################################################################
       
   190 # Module-level functions which are normally only used by this module
       
   191 ##############################################################################
       
   192 
       
   193 def convertpkcs8key(tempdir, privkey, passphrase):
       
   194     '''
       
   195     Convert a PKCS#8-format RSA or DSA private key to an older
       
   196     SSLeay-compatible format.
       
   197 
       
   198     convertpkcs8key(...) -> privkeyout
       
   199 
       
   200     tempdir     Path to pre-existing temporary directory with read/write access
       
   201     privkey     RSA or DSA private key, a string in PEM (base-64) format
       
   202     passphrase  pass phrase for the private key, a non-Unicode string or None
       
   203 
       
   204     privkeyout  decrypted private key in PEM (base-64) format
       
   205     '''
       
   206 
       
   207     # Determine PKCS#8 private key type.
       
   208     if privkey.find("-----BEGIN PRIVATE KEY-----") >= 0:
       
   209         # Unencrypted PKCS#8 private key
       
   210         encryptcmd = "-nocrypt"
       
   211     elif privkey.find("-----BEGIN ENCRYPTED PRIVATE KEY-----") >= 0:
       
   212         # Encrypted PKCS#8 private key
       
   213         encryptcmd = ""
       
   214     else:
       
   215         # Not a PKCS#8 private key, nothing to do.
       
   216         return privkey
       
   217 
       
   218     keyinfilename = os.path.join(tempdir, "keyin.pem")
       
   219     keyoutfilename = os.path.join(tempdir, "keyout.pem")
       
   220 
       
   221     try:
       
   222         # Write PEM format private key to file.
       
   223         keyinfile = file(keyinfilename, "wb")
       
   224         keyinfile.write(privkey)
       
   225         keyinfile.close()
       
   226 
       
   227         # Convert a PKCS#8 private key to older SSLeay-compatible format.
       
   228         # Keep pass phrase as-is.
       
   229         runopenssl("pkcs8 -in %s -out %s -passin stdin -passout stdin %s" %
       
   230                    (quote(keyinfilename), quote(keyoutfilename), encryptcmd),
       
   231                    "%s\n%s\n" % (passphrase, passphrase))
       
   232 
       
   233         privkey = ""
       
   234         if os.path.isfile(keyoutfilename):
       
   235             # Read converted private key back.
       
   236             keyoutfile = file(keyoutfilename, "rb")
       
   237             privkey = keyoutfile.read()
       
   238             keyoutfile.close()
       
   239 
       
   240         if privkey.strip() == "":
       
   241             # OpenSSL did not create output. Probably a wrong pass phrase.
       
   242             raise ValueError("wrong pass phrase or invalid PKCS#8 private key")
       
   243     finally:
       
   244         # Delete temporary files.
       
   245         for fname in (keyinfilename, keyoutfilename):
       
   246             try:
       
   247                 os.remove(fname)
       
   248             except OSError:
       
   249                pass
       
   250 
       
   251     return privkey
       
   252 
       
   253 def decryptkey(tempdir, privkey, passphrase):
       
   254     '''
       
   255     decryptkey(...) -> (privkeyout, keytype)
       
   256 
       
   257     tempdir     Path to pre-existing temporary directory with read/write access
       
   258     privkey     RSA or DSA private key, a string in PEM (base-64) format
       
   259     passphrase  pass phrase for the private key, a non-Unicode string or None
       
   260     string      a binary string to sign
       
   261 
       
   262     keytype     detected key type, string, "RSA" or "DSA"
       
   263     privkeyout  decrypted private key in PEM (base-64) format
       
   264 
       
   265     NOTE: On platforms with poor file system security, decrypted version
       
   266     of the private key may be grabbed from the temporary directory!
       
   267     '''
       
   268 
       
   269     # Determine private key type.
       
   270     if privkey.find("-----BEGIN DSA PRIVATE KEY-----") >= 0:
       
   271         keytype = "DSA"
       
   272         convcmd = "dsa"
       
   273     elif privkey.find("-----BEGIN RSA PRIVATE KEY-----") >= 0:
       
   274         keytype = "RSA"
       
   275         convcmd = "rsa"
       
   276     else:
       
   277         raise ValueError("not an RSA or DSA private key in PEM format")
       
   278 
       
   279     keyinfilename = os.path.join(tempdir, "keyin.pem")
       
   280     keyoutfilename = os.path.join(tempdir, "keyout.pem")
       
   281 
       
   282     try:
       
   283         # Write PEM format private key to file.
       
   284         keyinfile = file(keyinfilename, "wb")
       
   285         keyinfile.write(privkey)
       
   286         keyinfile.close()
       
   287 
       
   288         # Decrypt the private key. Older versions of OpenSSL do not
       
   289         # accept the "-passin" parameter for the "dgst" command.
       
   290         runopenssl("%s -in %s -out %s -passin stdin" %
       
   291                    (convcmd, quote(keyinfilename),
       
   292                     quote(keyoutfilename)), passphrase)
       
   293 
       
   294         privkey = ""
       
   295         if os.path.isfile(keyoutfilename):
       
   296             # Read decrypted private key back.
       
   297             keyoutfile = file(keyoutfilename, "rb")
       
   298             privkey = keyoutfile.read()
       
   299             keyoutfile.close()
       
   300 
       
   301         if privkey.strip() == "":
       
   302             # OpenSSL did not create output. Probably a wrong pass phrase.
       
   303             raise ValueError("wrong pass phrase or invalid private key")
       
   304     finally:
       
   305         # Delete temporary files.
       
   306         for fname in (keyinfilename, keyoutfilename):
       
   307             try:
       
   308                 os.remove(fname)
       
   309             except OSError:
       
   310                pass
       
   311 
       
   312     return (privkey, keytype)
       
   313 
       
   314 def mkdtemp(template):
       
   315     '''
       
   316     Create a unique temporary directory.
       
   317 
       
   318     tempfile.mkdtemp() was introduced in Python v2.3. This is for
       
   319     backward compatibility.
       
   320     '''
       
   321 
       
   322     # Cross-platform way to determine a suitable location for temporary files.
       
   323     systemp = tempfile.gettempdir()
       
   324 
       
   325     if not template.endswith("XXXXXX"):
       
   326         raise ValueError("invalid template for mkdtemp(): %s" % template)
       
   327 
       
   328     for n in xrange(10000):
       
   329         randchars = []
       
   330         for m in xrange(6):
       
   331             randchars.append(random.choice("abcdefghijklmnopqrstuvwxyz"))
       
   332 
       
   333         tempdir = os.path.join(systemp, template[: -6]) + "".join(randchars)
       
   334 
       
   335         try:
       
   336             os.mkdir(tempdir, 0700)
       
   337             return tempdir
       
   338         except OSError:
       
   339             pass
       
   340     else:
       
   341         # All unique names in use, raise an error.
       
   342         raise OSError(errno.EEXIST, os.strerror(errno.EEXIST),
       
   343                       os.path.join(systemp, template))
       
   344 
       
   345 def quote(filename):
       
   346     '''Quote a filename if it has spaces in it.'''
       
   347     if " " in filename:
       
   348         filename = '"%s"' % filename
       
   349     return filename
       
   350 
       
   351 def runopenssl(command, datain = ""):
       
   352     '''Run the OpenSSL command line tool with the given parameters and data.'''
       
   353 
       
   354     global opensslcommand
       
   355 
       
   356     if opensslcommand == None:
       
   357         # Find path to the OpenSSL command.
       
   358         findopenssl()
       
   359 
       
   360     # Construct a command line for os.popen3().
       
   361     cmdline = '%s %s' % (opensslcommand, command)
       
   362 
       
   363     if openssldebug:
       
   364         # Print command line.
       
   365         print "DEBUG: os.popen3(%s)" % repr(cmdline)
       
   366 
       
   367     # Run command. Use os.popen3() to capture stdout and stderr.
       
   368     pipein, pipeout, pipeerr = os.popen3(cmdline)
       
   369     pipein.write(datain)
       
   370     pipein.close()
       
   371     dataout = pipeout.read()
       
   372     pipeout.close()
       
   373     errout = pipeerr.read()
       
   374     pipeerr.close()
       
   375 
       
   376     if openssldebug:
       
   377         # Print standard error output.
       
   378         print "DEBUG: pipeerr.read() = %s" % repr(errout)
       
   379 
       
   380     return (dataout, errout)
       
   381 
       
   382 def findopenssl():
       
   383     '''Find the OpenSSL command line tool.'''
       
   384 
       
   385     global opensslcommand
       
   386 
       
   387     # Get PATH and split it to a list of paths.
       
   388     paths = os.environ["PATH"].split(os.pathsep)
       
   389 
       
   390     # Insert script path in front of others.
       
   391     # On Windows, this is where openssl.exe resides by default.
       
   392     if sys.path[0] != "":
       
   393         paths.insert(0, sys.path[0])
       
   394 
       
   395     for path in paths:
       
   396         cmd = os.path.join(path, "openssl")
       
   397         try:
       
   398             # Try to query OpenSSL version.
       
   399             pin, pout = os.popen4('"%s" version' % cmd)
       
   400             pin.close()
       
   401             verstr = pout.read()
       
   402             pout.close()
       
   403         except OSError:
       
   404             # Could not run command, skip to the next path candidate.
       
   405             continue
       
   406 
       
   407         if verstr.split()[0] == "OpenSSL":
       
   408             # Command found, stop searching.
       
   409             break
       
   410     else:
       
   411         raise IOError("no valid OpenSSL command line tool found in PATH")
       
   412 
       
   413     # Add quotes around command in case of embedded whitespace on path.
       
   414     opensslcommand = quote(cmd)