|
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) |