1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
|
#!/usr/bin/env python
# -*- coding: iso8859-1 -*-
##############################################################################
# cryptutil.py - OpenSSL command line utility wrappers for Ensymble
# Copyright 2006, 2007, 2008 Jussi Ylnen
#
# 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)
|