diff -r 000000000000 -r ca70ae20a155 src/tools/py2sis/ensymble/cryptutil.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/tools/py2sis/ensymble/cryptutil.py Tue Feb 16 10:07:05 2010 +0530 @@ -0,0 +1,414 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################## +# cryptutil.py - OpenSSL command line utility wrappers for Ensymble +# Copyright 2006, 2007, 2008 Jussi Ylänen +# +# This file is part of Ensymble developer utilities for Symbian OS(TM). +# +# Ensymble is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Ensymble is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ensymble; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +############################################################################## + +import sys +import os +import errno +import tempfile +import random + + +opensslcommand = None # Path to OpenSSL command line tool +openssldebug = False # True for extra debug output + + +############################################################################## +# Public module-level functions +############################################################################## + +def setdebug(active): + ''' + Activate or deactivate debug output. + + setdebug(...) -> None + + active Debug output enabled / disabled, a boolean value + + Debug output consists of OpenSSL binary command line and + any output produced to the standard error stream by OpenSSL. + ''' + + global openssldebug + openssldebug = not not active # Convert to boolean. + +def signstring(privkey, passphrase, string): + ''' + Sign a binary string using a given private key and its pass phrase. + + signstring(...) -> (signature, keytype) + + privkey RSA or DSA private key, a string in PEM (base-64) format + passphrase pass phrase for the private key, a non-Unicode string or None + string a binary string to sign + + signature signature, an ASN.1 encoded binary string + keytype detected key type, string, "RSA" or "DSA" + + NOTE: On platforms with poor file system security, decrypted version + of the private key may be grabbed from the temporary directory! + ''' + + if passphrase == None or len(passphrase) == 0: + # OpenSSL does not like empty stdin while reading a passphrase from it. + passphrase = "\n" + + # Create a temporary directory for OpenSSL to work in. + tempdir = mkdtemp("ensymble-XXXXXX") + + keyfilename = os.path.join(tempdir, "privkey.pem") + sigfilename = os.path.join(tempdir, "signature.dat") + stringfilename = os.path.join(tempdir, "string.dat") + + try: + # If the private key is in PKCS#8 format, it needs to be converted. + privkey = convertpkcs8key(tempdir, privkey, passphrase) + + # Decrypt the private key. Older versions of OpenSSL do not + # accept the "-passin" parameter for the "dgst" command. + privkey, keytype = decryptkey(tempdir, privkey, passphrase) + + if keytype == "DSA": + signcmd = "-dss1" + elif keytype == "RSA": + signcmd = "-sha1" + else: + raise ValueError("unknown private key type %s" % keytype) + + # Write decrypted PEM format private key to file. + keyfile = file(keyfilename, "wb") + keyfile.write(privkey) + keyfile.close() + + # Write binary string to a file. On some systems, stdin is + # always in text mode and thus unsuitable for binary data. + stringfile = file(stringfilename, "wb") + stringfile.write(string) + stringfile.close() + + # Sign binary string using the decrypted private key. + command = ("dgst %s -binary -sign %s " + "-out %s %s") % (signcmd, quote(keyfilename), + quote(sigfilename), quote(stringfilename)) + runopenssl(command) + + signature = "" + if os.path.isfile(sigfilename): + # Read signature from file. + sigfile = file(sigfilename, "rb") + signature = sigfile.read() + sigfile.close() + + if signature.strip() == "": + # OpenSSL did not create output, something went wrong. + raise ValueError("unspecified error during signing") + finally: + # Delete temporary files. + for fname in (keyfilename, sigfilename, stringfilename): + try: + os.remove(fname) + except OSError: + pass + + # Remove temporary directory. + os.rmdir(tempdir) + + return (signature, keytype) + + +def certtobinary(pemcert): + ''' + Convert X.509 certificates from PEM (base-64) format to DER (binary). + + certtobinary(...) -> dercert + + pemcert One or more X.509 certificates in PEM (base-64) format, a string + + dercert X.509 certificate(s), an ASN.1 encoded binary string + ''' + + # Find base-64 encoded data between header and footer. + header = "-----BEGIN CERTIFICATE-----" + footer = "-----END CERTIFICATE-----" + endoffset = 0 + certs = [] + while True: + # First find a header. + startoffset = pemcert.find(header, endoffset) + if startoffset < 0: + # No header found, stop search. + break + + startoffset += len(header) + + # Next find a footer. + endoffset = pemcert.find(footer, startoffset) + if endoffset < 0: + # No footer found. + raise ValueError("missing PEM certificate footer") + + # Extract the base-64 encoded certificate and decode it. + try: + cert = pemcert[startoffset:endoffset].decode("base-64") + except: + # Base-64 decoding error. + raise ValueError("invalid PEM format certificate") + + certs.append(cert) + + endoffset += len(footer) + + if len(certs) == 0: + raise ValueError("not a PEM format certificate") + + # DER certificates are simply raw binary versions + # of the base-64 encoded PEM certificates. + return "".join(certs) + + +############################################################################## +# Module-level functions which are normally only used by this module +############################################################################## + +def convertpkcs8key(tempdir, privkey, passphrase): + ''' + Convert a PKCS#8-format RSA or DSA private key to an older + SSLeay-compatible format. + + convertpkcs8key(...) -> privkeyout + + tempdir Path to pre-existing temporary directory with read/write access + privkey RSA or DSA private key, a string in PEM (base-64) format + passphrase pass phrase for the private key, a non-Unicode string or None + + privkeyout decrypted private key in PEM (base-64) format + ''' + + # Determine PKCS#8 private key type. + if privkey.find("-----BEGIN PRIVATE KEY-----") >= 0: + # Unencrypted PKCS#8 private key + encryptcmd = "-nocrypt" + elif privkey.find("-----BEGIN ENCRYPTED PRIVATE KEY-----") >= 0: + # Encrypted PKCS#8 private key + encryptcmd = "" + else: + # Not a PKCS#8 private key, nothing to do. + return privkey + + keyinfilename = os.path.join(tempdir, "keyin.pem") + keyoutfilename = os.path.join(tempdir, "keyout.pem") + + try: + # Write PEM format private key to file. + keyinfile = file(keyinfilename, "wb") + keyinfile.write(privkey) + keyinfile.close() + + # Convert a PKCS#8 private key to older SSLeay-compatible format. + # Keep pass phrase as-is. + runopenssl("pkcs8 -in %s -out %s -passin stdin -passout stdin %s" % + (quote(keyinfilename), quote(keyoutfilename), encryptcmd), + "%s\n%s\n" % (passphrase, passphrase)) + + privkey = "" + if os.path.isfile(keyoutfilename): + # Read converted private key back. + keyoutfile = file(keyoutfilename, "rb") + privkey = keyoutfile.read() + keyoutfile.close() + + if privkey.strip() == "": + # OpenSSL did not create output. Probably a wrong pass phrase. + raise ValueError("wrong pass phrase or invalid PKCS#8 private key") + finally: + # Delete temporary files. + for fname in (keyinfilename, keyoutfilename): + try: + os.remove(fname) + except OSError: + pass + + return privkey + +def decryptkey(tempdir, privkey, passphrase): + ''' + decryptkey(...) -> (privkeyout, keytype) + + tempdir Path to pre-existing temporary directory with read/write access + privkey RSA or DSA private key, a string in PEM (base-64) format + passphrase pass phrase for the private key, a non-Unicode string or None + string a binary string to sign + + keytype detected key type, string, "RSA" or "DSA" + privkeyout decrypted private key in PEM (base-64) format + + NOTE: On platforms with poor file system security, decrypted version + of the private key may be grabbed from the temporary directory! + ''' + + # Determine private key type. + if privkey.find("-----BEGIN DSA PRIVATE KEY-----") >= 0: + keytype = "DSA" + convcmd = "dsa" + elif privkey.find("-----BEGIN RSA PRIVATE KEY-----") >= 0: + keytype = "RSA" + convcmd = "rsa" + else: + raise ValueError("not an RSA or DSA private key in PEM format") + + keyinfilename = os.path.join(tempdir, "keyin.pem") + keyoutfilename = os.path.join(tempdir, "keyout.pem") + + try: + # Write PEM format private key to file. + keyinfile = file(keyinfilename, "wb") + keyinfile.write(privkey) + keyinfile.close() + + # Decrypt the private key. Older versions of OpenSSL do not + # accept the "-passin" parameter for the "dgst" command. + runopenssl("%s -in %s -out %s -passin stdin" % + (convcmd, quote(keyinfilename), + quote(keyoutfilename)), passphrase) + + privkey = "" + if os.path.isfile(keyoutfilename): + # Read decrypted private key back. + keyoutfile = file(keyoutfilename, "rb") + privkey = keyoutfile.read() + keyoutfile.close() + + if privkey.strip() == "": + # OpenSSL did not create output. Probably a wrong pass phrase. + raise ValueError("wrong pass phrase or invalid private key") + finally: + # Delete temporary files. + for fname in (keyinfilename, keyoutfilename): + try: + os.remove(fname) + except OSError: + pass + + return (privkey, keytype) + +def mkdtemp(template): + ''' + Create a unique temporary directory. + + tempfile.mkdtemp() was introduced in Python v2.3. This is for + backward compatibility. + ''' + + # Cross-platform way to determine a suitable location for temporary files. + systemp = tempfile.gettempdir() + + if not template.endswith("XXXXXX"): + raise ValueError("invalid template for mkdtemp(): %s" % template) + + for n in xrange(10000): + randchars = [] + for m in xrange(6): + randchars.append(random.choice("abcdefghijklmnopqrstuvwxyz")) + + tempdir = os.path.join(systemp, template[: -6]) + "".join(randchars) + + try: + os.mkdir(tempdir, 0700) + return tempdir + except OSError: + pass + else: + # All unique names in use, raise an error. + raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), + os.path.join(systemp, template)) + +def quote(filename): + '''Quote a filename if it has spaces in it.''' + if " " in filename: + filename = '"%s"' % filename + return filename + +def runopenssl(command, datain = ""): + '''Run the OpenSSL command line tool with the given parameters and data.''' + + global opensslcommand + + if opensslcommand == None: + # Find path to the OpenSSL command. + findopenssl() + + # Construct a command line for os.popen3(). + cmdline = '%s %s' % (opensslcommand, command) + + if openssldebug: + # Print command line. + print "DEBUG: os.popen3(%s)" % repr(cmdline) + + # Run command. Use os.popen3() to capture stdout and stderr. + pipein, pipeout, pipeerr = os.popen3(cmdline) + pipein.write(datain) + pipein.close() + dataout = pipeout.read() + pipeout.close() + errout = pipeerr.read() + pipeerr.close() + + if openssldebug: + # Print standard error output. + print "DEBUG: pipeerr.read() = %s" % repr(errout) + + return (dataout, errout) + +def findopenssl(): + '''Find the OpenSSL command line tool.''' + + global opensslcommand + + # Get PATH and split it to a list of paths. + paths = os.environ["PATH"].split(os.pathsep) + + # Insert script path in front of others. + # On Windows, this is where openssl.exe resides by default. + if sys.path[0] != "": + paths.insert(0, sys.path[0]) + + for path in paths: + cmd = os.path.join(path, "openssl") + try: + # Try to query OpenSSL version. + pin, pout = os.popen4('"%s" version' % cmd) + pin.close() + verstr = pout.read() + pout.close() + except OSError: + # Could not run command, skip to the next path candidate. + continue + + if verstr.split()[0] == "OpenSSL": + # Command found, stop searching. + break + else: + raise IOError("no valid OpenSSL command line tool found in PATH") + + # Add quotes around command in case of embedded whitespace on path. + opensslcommand = quote(cmd)