Index: Tools/ssl/get-remote-certificate.py =================================================================== --- Tools/ssl/get-remote-certificate.py (revision 0) +++ Tools/ssl/get-remote-certificate.py (revision 0) @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# +# fetch the certificate that the server(s) are providing in PEM form +# +# args are HOST:PORT [, HOST:PORT...] + +import sys, os + +def fetch_server_certificate (host, port): + + import re, tempfile, os, ssl + + def subproc(cmd): + from subprocess import Popen, PIPE, STDOUT + proc = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True) + status = proc.wait() + output = proc.stdout.read() + return status, output + + def strip_to_x509_cert(certfile_contents, outfile=None): + m = re.search("^([-]+BEGIN CERTIFICATE[-]+[\r]*\n.*[\r]*^[-]+END CERTIFICATE[-]+)$", + certfile_contents, re.MULTILINE | re.DOTALL) + if not m: + return None + else: + tn = tempfile.mktemp() + fp = open(tn, "w") + fp.write(m.group(1) + "\n") + fp.close() + try: + tn2 = (outfile or tempfile.mktemp()) + status, output = subproc(r'openssl x509 -in "%s" -out "%s"' % (tn, tn2)) + if status != 0: + raise OperationError(status, tsig, output) + fp = open(tn2, 'rb') + data = fp.read() + fp.close() + os.unlink(tn2) + return data + finally: + os.unlink(tn) + + if sys.platform.startswith("win"): + tfile = tempfile.mktemp() + fp = open(tfile, "w") + fp.write("quit\n") + fp.close() + try: + status, output = subproc('openssl s_client -connect "%s:%s" -showcerts < "%s"' % (host, port, tfile)) + finally: + os.unlink(tfile) + else: + status, output = subproc('openssl s_client -connect "%s:%s" -showcerts < /dev/null' % (host, port)) + if status != 0: + raise OSError(status) + certtext = strip_to_x509_cert(output) + if not certtext: + raise ValueError("Invalid response received from server at %s:%s", host, port) + return certtext + +if __name__ == "__main__": + if len(sys.argv) < 2: + sys.stderr.write("Usage: %s HOSTNAME:PORTNUMBER [, HOSTNAME:PORTNUMBER...]\n" % (sys.argv[0],)) + sys.exit(1) + for arg in sys.argv[1:]: + host, port = arg.split(":") + sys.stdout.write(fetch_server_certificate(host, int(port))) + sys.exit(0) Property changes on: Tools/ssl/get-remote-certificate.py ___________________________________________________________________ Name: svn:keywords + Id Name: svn:eol-style + native Index: Doc/library/socket.rst =================================================================== --- Doc/library/socket.rst (revision 57412) +++ Doc/library/socket.rst (working copy) @@ -776,7 +776,15 @@ Returns a string describing the server's certificate. Useful for debugging purposes; do not parse the content of this string because its format can't be - parsed unambiguously. + parsed unambiguously. And don't *trust* the content of this string, because + certificates aren't validated if you use the function :func:`ssl` to create an + SSL binding. If you + need to see the content of a peer certificate, you should use the :func:`sslsocket` + function in the :mod:`ssl` module to create the SSL object, specifying the parameter `cert_req` as :const:`CERT_REQUIRED`, + and passing the name of a file containing a collection of certificates to use to validate the peer + certificate as the value of the `ca_certs` parameter. Then use + the :meth:`getpeercert` method on that instance + to retrieve the contents of the certificate. .. method:: SSL.issuer() @@ -785,7 +793,6 @@ debugging purposes; do not parse the content of this string because its format can't be parsed unambiguously. - .. _socket-example: Example Index: Lib/socket.py =================================================================== --- Lib/socket.py (revision 57412) +++ Lib/socket.py (working copy) @@ -49,10 +49,17 @@ _have_ssl = False try: import _ssl - from _ssl import * - _have_ssl = True except ImportError: pass +else: + def ssl (sock, keyfile=None, certfile=None): + import ssl as realssl + return realssl.sslwrap_simple(sock, keyfile, certfile) + sslerror = _ssl.sslerror + from _ssl import SSLType, SSL_ERROR_ZERO_RETURN, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE, \ + SSL_ERROR_WANT_X509_LOOKUP, SSL_ERROR_SYSCALL, SSL_ERROR_SSL, SSL_ERROR_WANT_CONNECT, \ + SSL_ERROR_EOF, SSL_ERROR_INVALID_ERROR_CODE, RAND_add, RAND_egd, RAND_status + _have_ssl = True import os, sys @@ -63,17 +70,10 @@ __all__ = ["getfqdn"] __all__.extend(os._get_exports_list(_socket)) -if _have_ssl: - __all__.extend(os._get_exports_list(_ssl)) _realsocket = socket -if _have_ssl: - _realssl = ssl - def ssl(sock, keyfile=None, certfile=None): - if hasattr(sock, "_sock"): - sock = sock._sock - return _realssl(sock, keyfile, certfile) + # WSA error codes if sys.platform.lower().startswith("win"): errorTab = {} Index: Lib/ssl.py =================================================================== --- Lib/ssl.py (revision 0) +++ Lib/ssl.py (revision 0) @@ -0,0 +1,421 @@ +# Wrapper module for _ssl, providing some additional facilities +# implemented in Python. + +"""\ +This module provides some more Pythonic support for SSL. + +Object types: + + sslsocket -- subtype of socket.socket which does SSL over the socket + +Exceptions: + + sslerror -- exception raised for I/O errors + +Functions: + + cert_time_to_seconds -- convert time string used for certificate + notBefore and notAfter functions to integer + seconds past the Epoch (the time values + returned from time.time()) + + fetch_server_certificate (HOST, PORT) -- fetch the certificate provided + by the server running on HOST at port PORT. No + validation of the certificate is performed. + +Integer constants: + +SSL_ERROR_ZERO_RETURN +SSL_ERROR_WANT_READ +SSL_ERROR_WANT_WRITE +SSL_ERROR_WANT_X509_LOOKUP +SSL_ERROR_SYSCALL +SSL_ERROR_SSL +SSL_ERROR_WANT_CONNECT + +SSL_ERROR_EOF +SSL_ERROR_INVALID_ERROR_CODE + +The following group define certificate requirements that one side is +allowing/requiring from the other side: + +CERT_NONE - no certificates from the other side are required (or will + be looked at if provided) +CERT_OPTIONAL - certificates are not required, but if provided will be + validated, and if validation fails, the connection will also fail +CERT_REQUIRED - certificates are required, and will be validated, and + if validation fails, the connection will also fail + +The following constants identify various SSL protocol variants: + +PROTOCOL_SSLv2 +PROTOCOL_SSLv3 +PROTOCOL_SSLv23 +PROTOCOL_TLSv1 +""" + +import os, sys + +import _ssl # if we can't import it, let the error propagate +import socket +from _ssl import sslerror +from _ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED +from _ssl import PROTOCOL_SSLv2, PROTOCOL_SSLv3, PROTOCOL_SSLv23, PROTOCOL_TLSv1 + +# Root certs: +# +# The "ca_certs" argument to sslsocket() expects a file containing one or more +# certificates that are roots of various certificate signing chains. This file +# contains the certificates in PEM format (RFC ) where each certificate is +# encoded in base64 encoding and surrounded with a header and footer: +# -----BEGIN CERTIFICATE----- +# ... (CA certificate in base64 encoding) ... +# -----END CERTIFICATE----- +# The various certificates in the file are just concatenated together: +# -----BEGIN CERTIFICATE----- +# ... (CA certificate in base64 encoding) ... +# -----END CERTIFICATE----- +# -----BEGIN CERTIFICATE----- +# ... (a second CA certificate in base64 encoding) ... +# -----END CERTIFICATE----- +# +# Some "standard" root certificates are available at +# +# http://www.thawte.com/roots/ (for Thawte roots) +# http://www.verisign.com/support/roots.html (for Verisign) + +class sslsocket (socket.socket): + + def __init__(self, sock, keyfile=None, certfile=None, + server_side=False, cert_reqs=CERT_NONE, ssl_version=PROTOCOL_SSLv23, + ca_certs=None): + socket.socket.__init__(self, _sock=sock._sock) + if certfile and not keyfile: + keyfile = certfile + if server_side: + self._sslobj = _ssl.sslwrap(self._sock, 1, keyfile, certfile, cert_reqs, ssl_version, ca_certs) + else: + # see if it's connected + try: + self.getpeername() + except socket.error: + # no + self._sslobj = None + else: + # yes + self._sslobj = _ssl.sslwrap(self._sock, 0, keyfile, certfile, cert_reqs, ssl_version, ca_certs) + self.keyfile = keyfile + self.certfile = certfile + self.cert_reqs = cert_reqs + self.ssl_version = ssl_version + self.ca_certs = ca_certs + + def read(self, len=1024): + return self._sslobj.read(len) + + def write(self, data): + return self._sslobj.write(data) + + def getpeercert(self): + return self._sslobj.peer_certificate() + + def send (self, data, flags=0): + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to send() on %s" % self.__class__) + return self._sslobj.write(data) + + def send_to (self, data, addr, flags=0): + raise ValueError("send_to not allowed on instances of %s" % self.__class__) + + def sendall (self, data, flags=0): + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to sendall() on %s" % self.__class__) + return self._sslobj.write(data) + + def recv (self, buflen=1024, flags=0): + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv() on %s" % self.__class__) + return self._sslobj.read(data, buflen) + + def recv_from (self, addr, buflen=1024, flags=0): + raise ValueError("recv_from not allowed on instances of %s" % self.__class__) + + def shutdown(self): + if self._sslobj: + self._sslobj.shutdown() + self._sslobj = None + else: + socket.socket.shutdown(self) + + def close(self): + if self._sslobj: + self.shutdown() + else: + socket.socket.close(self) + + def connect(self, addr): + """Here we assume that the socket is client-side, and not connected at the + time of the call. We connect it, then wrap it.""" + if self._sslobj or (self.getsockname()[1] != 0): + raise ValueError("attempt to connect already-connected sslsocket!") + socket.socket.connect(self, addr) + self._sslobj = _ssl.sslwrap(self._sock, 0, self.keyfile, self.certfile, self.cert_reqs, self.ssl_version, self.ca_certs) + + def accept(self): + raise ValueError("accept() not supported on an sslsocket") + + +# some utility functions + +def cert_time_to_seconds(cert_time): + import time + return time.mktime(time.strptime(cert_time, "%b %d %H:%M:%S %Y GMT")) + +# a replacement for the old socket.ssl function + +def sslwrap_simple (sock, keyfile=None, certfile=None): + + return _ssl.sslwrap(sock._sock, 0, keyfile, certfile, CERT_NONE, PROTOCOL_SSLv23, None) + + + + + + +# Wrapper module for _ssl, providing some additional facilities +# implemented in Python. + +"""\ +This module provides some more Pythonic support for SSL. + +Object types: + + sslsocket -- subtype of socket.socket which does SSL over the socket + +Exceptions: + + sslerror -- exception raised for I/O errors + +Functions: + + cert_time_to_seconds -- convert time string used for certificate + notBefore and notAfter functions to integer + seconds past the Epoch (the time values + returned from time.time()) + + fetch_server_certificate (HOST, PORT) -- fetch the certificate provided + by the server running on HOST at port PORT. No + validation of the certificate is performed. + +Integer constants: + +SSL_ERROR_ZERO_RETURN +SSL_ERROR_WANT_READ +SSL_ERROR_WANT_WRITE +SSL_ERROR_WANT_X509_LOOKUP +SSL_ERROR_SYSCALL +SSL_ERROR_SSL +SSL_ERROR_WANT_CONNECT + +SSL_ERROR_EOF +SSL_ERROR_INVALID_ERROR_CODE + +The following group define certificate requirements that one side is +allowing/requiring from the other side: + +CERT_NONE - no certificates from the other side are required (or will + be looked at if provided) +CERT_OPTIONAL - certificates are not required, but if provided will be + validated, and if validation fails, the connection will also fail +CERT_REQUIRED - certificates are required, and will be validated, and + if validation fails, the connection will also fail + +The following constants identify various SSL protocol variants: + +PROTOCOL_SSLv2 +PROTOCOL_SSLv3 +PROTOCOL_SSLv23 +PROTOCOL_TLSv1 +""" + +import os, sys + +import _ssl # if we can't import it, let the error propagate +from socket import socket +from _ssl import sslerror +from _ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED +from _ssl import PROTOCOL_SSLv2, PROTOCOL_SSLv3, PROTOCOL_SSLv23, PROTOCOL_TLSv1 + +# Root certs: +# +# The "ca_certs" argument to sslsocket() expects a file containing one or more +# certificates that are roots of various certificate signing chains. This file +# contains the certificates in PEM format (RFC ) where each certificate is +# encoded in base64 encoding and surrounded with a header and footer: +# -----BEGIN CERTIFICATE----- +# ... (CA certificate in base64 encoding) ... +# -----END CERTIFICATE----- +# The various certificates in the file are just concatenated together: +# -----BEGIN CERTIFICATE----- +# ... (CA certificate in base64 encoding) ... +# -----END CERTIFICATE----- +# -----BEGIN CERTIFICATE----- +# ... (a second CA certificate in base64 encoding) ... +# -----END CERTIFICATE----- +# +# Some "standard" root certificates are available at +# +# http://www.thawte.com/roots/ (for Thawte roots) +# http://www.verisign.com/support/roots.html (for Verisign) + +class sslsocket (socket): + + def __init__(self, sock, keyfile=None, certfile=None, + server_side=False, cert_reqs=CERT_NONE, ssl_version=PROTOCOL_SSLv23, + ca_certs=None): + socket.__init__(self, _sock=sock._sock) + if certfile and not keyfile: + keyfile = certfile + if server_side: + self._sslobj = _ssl.sslwrap(self._sock, 1, keyfile, certfile, cert_reqs, ssl_version, ca_certs) + else: + # see if it's connected + try: + socket.getpeername(self) + # yes + self._sslobj = _ssl.sslwrap(self._sock, 0, keyfile, certfile, cert_reqs, ssl_version, ca_certs) + except: + # no + self._sslobj = None + self.keyfile = keyfile + self.certfile = certfile + self.cert_reqs = cert_reqs + self.ssl_version = ssl_version + self.ca_certs = ca_certs + + def read(self, len=1024): + return self._sslobj.read(len) + + def write(self, data): + return self._sslobj.write(data) + + def getpeercert(self): + return self._sslobj.peer_certificate() + + def send (self, data, flags=0): + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to send() on %s" % self.__class__) + return self._sslobj.write(data) + + def send_to (self, data, addr, flags=0): + raise ValueError("send_to not allowed on instances of %s" % self.__class__) + + def sendall (self, data, flags=0): + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to sendall() on %s" % self.__class__) + return self._sslobj.write(data) + + def recv (self, buflen=1024, flags=0): + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to sendall() on %s" % self.__class__) + return self._sslobj.read(data, buflen) + + def recv_from (self, addr, buflen=1024, flags=0): + raise ValueError("recv_from not allowed on instances of %s" % self.__class__) + + def shutdown(self): + if self._sslobj: + self._sslobj.shutdown() + self._sslobj = None + else: + socket.shutdown(self) + + def close(self): + if self._sslobj: + self.shutdown() + else: + socket.close(self) + + def connect(self, addr): + """Here we assume that the socket is client-side, and not connected at the + time of the call. We connect it, then wrap it.""" + if self._sslobj or (self.getsockname()[1] != 0): + raise ValueError("attempt to connect already-connected sslsocket!") + socket.connect(self, addr) + self._sslobj = _ssl.sslwrap(self._sock, 0, self.keyfile, self.certfile, self.cert_reqs, self.ssl_version, self.ca_certs) + + def accept(self): + raise ValueError("accept() not supported on an sslsocket") + + +# some utility functions + +def cert_time_to_seconds(cert_time): + import time + return time.mktime(time.strptime(cert_time, "%b %d %H:%M:%S %Y GMT")) + +# a replacement for the old socket.ssl function + +def sslwrap_simple (sock, keyfile=None, certfile=None): + + return _ssl.sslwrap(sock._sock, 0, keyfile, certfile, CERT_NONE, PROTOCOL_SSLv23, None) + +# fetch the certificate that the server is providing in PEM form + +def fetch_server_certificate (host, port): + + import re, tempfile, os + + def subproc(cmd): + from subprocess import Popen, PIPE, STDOUT + proc = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True) + status = proc.wait() + output = proc.stdout.read() + return status, output + + def strip_to_x509_cert(certfile_contents, outfile=None): + m = re.search("^([-]+BEGIN CERTIFICATE[-]+[\r]*\n.*[\r]*^[-]+END CERTIFICATE[-]+)$", + certfile_contents, re.MULTILINE | re.DOTALL) + if not m: + return None + else: + tn = tempfile.mktemp() + fp = open(tn, "w") + fp.write(m.group(1) + "\n") + fp.close() + try: + tn2 = (outfile or tempfile.mktemp()) + status, output = subproc(r'openssl x509 -in "%s" -out "%s"' % (tn, tn2)) + if status != 0: + raise OperationError(status, tsig, output) + fp = open(tn2, 'rb') + data = fp.read() + fp.close() + os.unlink(tn2) + return data + finally: + os.unlink(tn) + + if sys.platform.startswith("win"): + tfile = tempfile.mktemp() + fp = open(tfile, "w") + fp.write("quit\n") + fp.close() + try: + status, output = subproc('openssl s_client -connect "%s:%s" -showcerts < "%s"' % (host, port, tfile)) + finally: + os.unlink(tfile) + else: + status, output = subproc('openssl s_client -connect "%s:%s" -showcerts < /dev/null' % (host, port)) + if status != 0: + raise OSError(status) + certtext = strip_to_x509_cert(output) + if not certtext: + raise ValueError("Invalid response received from server at %s:%s", host, port) + return certtext + + + + + + Property changes on: Lib/ssl.py ___________________________________________________________________ Name: svn:keywords + Id Name: svn:eol-style + native Index: Lib/test/test_ssl.py =================================================================== --- Lib/test/test_ssl.py (revision 0) +++ Lib/test/test_ssl.py (revision 0) @@ -0,0 +1,583 @@ +# Test the support for SSL and sockets + +import sys +import unittest +from test import test_support +import socket +import errno +import threading +import subprocess +import time +import os +import pprint +import urllib +import shutil +import string +import traceback + +# Optionally test SSL support, if we have it in the tested platform +skip_expected = False +try: + import ssl +except ImportError: + skip_expected = True + +CERTFILE = None +GMAIL_POP_CERTFILE = None + +class BasicTests(unittest.TestCase): + + def testRudeShutdown(self): + # Some random port to connect to. + PORT = [9934] + + listener_ready = threading.Event() + listener_gone = threading.Event() + + # `listener` runs in a thread. It opens a socket listening on + # PORT, and sits in an accept() until the main thread connects. + # Then it rudely closes the socket, and sets Event `listener_gone` + # to let the main thread know the socket is gone. + def listener(): + s = socket.socket() + PORT[0] = test_support.bind_port(s, '', PORT[0]) + s.listen(5) + listener_ready.set() + s.accept() + s = None # reclaim the socket object, which also closes it + listener_gone.set() + + def connector(): + listener_ready.wait() + s = socket.socket() + s.connect(('localhost', PORT[0])) + listener_gone.wait() + try: + ssl_sock = socket.ssl(s) + except socket.sslerror: + pass + else: + raise test_support.TestFailed( + 'connecting to closed SSL socket should have failed') + + t = threading.Thread(target=listener) + t.start() + connector() + t.join() + +class ConnectedTests(unittest.TestCase): + + def testTLSecho (self): + + s1 = socket.socket() + s1.connect(('127.0.0.1', 10024)) + c1 = ssl.sslsocket(s1, ssl_version=ssl.PROTOCOL_TLSv1) + indata = "FOO\n" + c1.write(indata) + outdata = c1.read() + if outdata != indata.lower(): + sys.stderr.write("bad data <<%s>> received\n" % data) + c1.close() + + def testReadCert(self): + + s2 = socket.socket() + s2.connect(('127.0.0.1', 10024)) + c2 = ssl.sslsocket(s2, ssl_version=ssl.PROTOCOL_TLSv1, cert_reqs=ssl.CERT_REQUIRED, ca_certs=CERTFILE) + cert = c2.getpeercert() + if not cert: + raise test_support.TestFailed("Can't get peer certificate.") + elif not cert.has_key('subject'): + raise test_support.TestFailed("No subject field in certificate: %s." % (pprint.pformat(cert),)); + elif not (cert['subject'].has_key('organizationName')): + raise test_support.TestFailed("No 'organizationName' field in certificate subject: %s." % (pprint.pformat(cert),)); + elif (cert['subject']['organizationName'] != "Python Software Foundation"): + raise test_support.TestFailed("Invalid 'organizationName' field '%s' in certificate subject; should be 'Python Software Foundation'."); + c2.close() + + +class ExternalNetworkTests(unittest.TestCase): + + def testSSLconnect(self): + import os + with test_support.transient_internet(): + s = ssl.sslsocket(socket.socket(socket.AF_INET), cert_reqs=ssl.CERT_NONE) + s.connect(("pop.gmail.com", 995)) + c = s.getpeercert() + if c: + raise test_support.TestFailed("Peer cert %s shouldn't be here!") + s.close() + + # this should fail because we have no verification certs + s = ssl.sslsocket(socket.socket(socket.AF_INET), cert_reqs=ssl.CERT_REQUIRED) + try: + s.connect(("pop.gmail.com", 995)) + except ssl.sslerror: + pass + finally: + s.close() + +class threadedEchoServer(threading.Thread): + + class connectionHandler(threading.Thread): + + def __init__(self, server, connsock): + self.server = server + self.running = False + self.sock = connsock + threading.Thread.__init__(self) + self.setDaemon(True) + + def run (self): + self.running = True + sslconn = ssl.sslsocket(self.sock, server_side=True, certfile=self.server.certificate, + ssl_version=self.server.protocol, cert_reqs=self.server.certreqs) + while self.running: + try: + msg = sslconn.read() + if not msg: + # eof, so quit this handler + self.running = False + sslconn.close() + elif msg.strip() == 'over': + sslconn.close() + self.server.stop() + self.running = False + else: + # print "server:", msg.strip().lower() + sslconn.write(msg.lower()) + except ssl.sslerror: + sys.stderr.write(string.join(traceback.format_exception(*sys.exc_info()))) + sslconn.close() + self.running = False + except: + sys.stderr.write(string.join(traceback.format_exception(*sys.exc_info()))) + + def __init__(self, port, certificate, ssl_version=ssl.PROTOCOL_TLSv1, certreqs=ssl.CERT_NONE, cacerts=None): + self.certificate = certificate + self.protocol = ssl_version + self.certreqs = certreqs + self.cacerts = cacerts + self.sock = None + self.active = False + self.signalEvent = None + self.port = port + threading.Thread.__init__(self) + self.sock = socket.socket() + self.setDaemon(False) + + def start (self, flag=None): + self.signalEvent = flag + threading.Thread.start(self) + + def run (self): + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + self.sock.bind(('127.0.0.1', self.port)) + self.sock.settimeout(0.5) + self.sock.listen(5) + self.active = True + if self.signalEvent: + # notify listeners + self.signalEvent.set() + while self.active: + try: + newconn, connaddr = self.sock.accept() + # sys.stderr.write('new connection from ' + str(connaddr)) + handler = self.connectionHandler(self, newconn) + handler.start() + except socket.timeout: + pass + except KeyboardInterrupt: + self.active = False + except: + sys.stderr.write(string.join(traceback.format_exception(*sys.exc_info()))) + + def stop (self): + self.active = False + + +CERTFILE_CONFIG_TEMPLATE = """ +# create RSA certs - Server + +[ req ] +default_bits = 1024 +encrypt_key = yes +distinguished_name = req_dn +x509_extensions = cert_type + +[ req_dn ] +countryName = Country Name (2 letter code) +countryName_default = US +countryName_min = 2 +countryName_max = 2 + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = %(state)s + +localityName = Locality Name (eg, city) +localityName_default = %(city)s + +0.organizationName = Organization Name (eg, company) +0.organizationName_default = %(organization)s + +organizationalUnitName = Organizational Unit Name (eg, section) +organizationalUnitName_default = %(unit)s + +0.commonName = Common Name (FQDN of your server) +0.commonName_default = %(common-name)s + +# To create a certificate for more than one name uncomment: +# 1.commonName = DNS alias of your server +# 2.commonName = DNS alias of your server +# ... +# See http://home.netscape.com/eng/security/ssl_2.0_certificate.html +# to see how Netscape understands commonName. + +[ cert_type ] +nsCertType = server +""" + +def create_cert_files(): + + import tempfile, socket, os + d = tempfile.mkdtemp() + # now create a configuration file for the CA signing cert + fqdn = socket.getfqdn() + crtfile = os.path.join(d, "cert.pem") + conffile = os.path.join(d, "ca.conf") + fp = open(conffile, "w") + fp.write(CERTFILE_CONFIG_TEMPLATE % { 'state' : "Delaware", + 'city' : "Wilmington", + 'organization' : "Python Software Foundation", + 'unit' : "SSL", + 'common-name' : fqdn }) + fp.close() + os.system("openssl req -batch -new -x509 -days 10 -nodes -config %s -keyout \"%s\" -out \"%s\" > /dev/null < /dev/null 2>&1" + % (conffile, crtfile, crtfile)) + # now we have a self-signed server cert in crtfile + os.unlink(conffile) + #sf_certfile = os.path.join(d, "sourceforge-imap.pem") + #sf_cert = ssl.fetch_server_certificate('pop.gmail.com', 995) + #open(sf_certfile, 'w').write(sf_cert) + #return d, crtfile, sf_certfile + # sys.stderr.write(open(crtfile, 'r').read() + '\n') + return d, crtfile + +def test_main(): + if skip_expected: + raise test_support.TestSkipped("socket module has no ssl support") + + global CERTFILE + tdir, CERTFILE = create_cert_files() + + tests = [BasicTests] + + server = threadedEchoServer(10024, CERTFILE) + readyFlag = threading.Event() + server.start(readyFlag) + while not readyFlag.isSet(): + readyFlag.wait() + tests.append(ConnectedTests) + + if test_support.is_resource_enabled('network'): + tests.append(ExternalNetworkTests) + + thread_info = test_support.threading_setup() + + try: + test_support.run_unittest(*tests) + finally: + if server is not None and server.active: + server.stop() + # wait for it to stop + server.join() + + shutil.rmtree(tdir) + test_support.threading_cleanup(*thread_info) + +if __name__ == "__main__": + test_main() +# Test the support for SSL and sockets + +import sys +import unittest +from test import test_support +import socket +import errno +import threading +import subprocess +import time +import os +import pprint +import urllib +import shutil +import string +import traceback + +# Optionally test SSL support, if we have it in the tested platform +skip_expected = False +try: + import ssl +except ImportError: + skip_expected = True + +CERTFILE = None +GMAIL_POP_CERTFILE = None + +class BasicTests(unittest.TestCase): + + def testRudeShutdown(self): + # Some random port to connect to. + PORT = [9934] + + listener_ready = threading.Event() + listener_gone = threading.Event() + + # `listener` runs in a thread. It opens a socket listening on + # PORT, and sits in an accept() until the main thread connects. + # Then it rudely closes the socket, and sets Event `listener_gone` + # to let the main thread know the socket is gone. + def listener(): + s = socket.socket() + PORT[0] = test_support.bind_port(s, '', PORT[0]) + s.listen(5) + listener_ready.set() + s.accept() + s = None # reclaim the socket object, which also closes it + listener_gone.set() + + def connector(): + listener_ready.wait() + s = socket.socket() + s.connect(('localhost', PORT[0])) + listener_gone.wait() + try: + ssl_sock = socket.ssl(s) + except socket.sslerror: + pass + else: + raise test_support.TestFailed( + 'connecting to closed SSL socket should have failed') + + t = threading.Thread(target=listener) + t.start() + connector() + t.join() + + def testSSLconnect(self): + import os + with test_support.transient_internet(): + s = ssl.sslsocket(socket.socket(socket.AF_INET), cert_reqs=ssl.CERT_NONE) + s.connect(("pop.gmail.com", 995)) + c = s.getpeercert() + if c: + raise test_support.TestFailed("Peer cert %s shouldn't be here!") + s.close() + + # this should fail because we have no verification certs + s = ssl.sslsocket(socket.socket(socket.AF_INET), cert_reqs=ssl.CERT_REQUIRED) + try: + s.connect(("pop.gmail.com", 995)) + except ssl.sslerror: + pass + finally: + s.close() + +class ConnectedTests(unittest.TestCase): + + def testTLSecho (self): + + s1 = socket.socket() + s1.connect(('127.0.0.1', 10024)) + c1 = ssl.sslsocket(s1, ssl_version=ssl.PROTOCOL_TLSv1) + indata = "FOO\n" + c1.write(indata) + outdata = c1.read() + if outdata != indata.lower(): + sys.stderr.write("bad data <<%s>> received\n" % data) + c1.close() + + def testReadCert(self): + + s2 = socket.socket() + s2.connect(('127.0.0.1', 10024)) + c2 = ssl.sslsocket(s2, ssl_version=ssl.PROTOCOL_TLSv1, cert_reqs=ssl.CERT_REQUIRED, ca_certs=CERTFILE) + cert = c2.getpeercert() + if not cert: + raise test_support.TestFailed("Can't get peer certificate.") + elif not cert.has_key('subject'): + raise test_support.TestFailed("No subject field in certificate: %s." % (pprint.pformat(cert),)); + elif not (cert['subject'].has_key('organizationName')): + raise test_support.TestFailed("No 'organizationName' field in certificate subject: %s." % (pprint.pformat(cert),)); + elif (cert['subject']['organizationName'] != "Python Software Foundation"): + raise test_support.TestFailed("Invalid 'organizationName' field '%s' in certificate subject; should be 'Python Software Foundation'."); + c2.close() + + +class threadedEchoServer(threading.Thread): + + class connectionHandler(threading.Thread): + + def __init__(self, server, connsock): + self.server = server + self.running = False + self.sock = connsock + threading.Thread.__init__(self) + self.setDaemon(True) + + def run (self): + self.running = True + sslconn = ssl.sslsocket(self.sock, server_side=True, certfile=self.server.certificate, + ssl_version=self.server.protocol, cert_reqs=self.server.certreqs) + while self.running: + try: + msg = sslconn.read() + if not msg: + # eof, so quit this handler + self.running = False + sslconn.close() + elif msg.strip() == 'over': + sslconn.close() + self.server.stop() + self.running = False + else: + # print "server:", msg.strip().lower() + sslconn.write(msg.lower()) + except ssl.sslerror: + sys.stderr.write(string.join(traceback.format_exception(*sys.exc_info()))) + sslconn.close() + self.running = False + except: + sys.stderr.write(string.join(traceback.format_exception(*sys.exc_info()))) + + def __init__(self, port, certificate, ssl_version=ssl.PROTOCOL_TLSv1, certreqs=ssl.CERT_NONE, cacerts=None): + self.certificate = certificate + self.protocol = ssl_version + self.certreqs = certreqs + self.cacerts = cacerts + self.sock = socket.socket() + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + self.sock.bind(('127.0.0.1', port)) + self.active = False + threading.Thread.__init__(self) + self.setDaemon(False) + + def run (self): + self.sock.settimeout(0.5) + self.sock.listen(5) + self.active = True + while self.active: + try: + newconn, connaddr = self.sock.accept() + # sys.stderr.write('new connection from ' + str(connaddr)) + handler = self.connectionHandler(self, newconn) + handler.start() + except socket.timeout: + pass + except KeyboardInterrupt: + self.active = False + except: + sys.stderr.write(string.join(traceback.format_exception(*sys.exc_info()))) + + def stop (self): + self.active = False + + +CERTFILE_CONFIG_TEMPLATE = """ +# create RSA certs - Server + +[ req ] +default_bits = 1024 +encrypt_key = yes +distinguished_name = req_dn +x509_extensions = cert_type + +[ req_dn ] +countryName = Country Name (2 letter code) +countryName_default = US +countryName_min = 2 +countryName_max = 2 + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = %(state)s + +localityName = Locality Name (eg, city) +localityName_default = %(city)s + +0.organizationName = Organization Name (eg, company) +0.organizationName_default = %(organization)s + +organizationalUnitName = Organizational Unit Name (eg, section) +organizationalUnitName_default = %(unit)s + +0.commonName = Common Name (FQDN of your server) +0.commonName_default = %(common-name)s + +# To create a certificate for more than one name uncomment: +# 1.commonName = DNS alias of your server +# 2.commonName = DNS alias of your server +# ... +# See http://home.netscape.com/eng/security/ssl_2.0_certificate.html +# to see how Netscape understands commonName. + +[ cert_type ] +nsCertType = server +""" + +def create_cert_files(): + + import tempfile, socket, os + d = tempfile.mkdtemp() + # now create a configuration file for the CA signing cert + fqdn = socket.getfqdn() + crtfile = os.path.join(d, "cert.pem") + conffile = os.path.join(d, "ca.conf") + fp = open(conffile, "w") + fp.write(CERTFILE_CONFIG_TEMPLATE % { 'state' : "Delaware", + 'city' : "Wilmington", + 'organization' : "Python Software Foundation", + 'unit' : "SSL", + 'common-name' : fqdn }) + fp.close() + os.system("openssl req -batch -new -x509 -days 10 -nodes -config %s -keyout \"%s\" -out \"%s\" > /dev/null < /dev/null 2>&1" + % (conffile, crtfile, crtfile)) + # now we have a self-signed server cert in crtfile + os.unlink(conffile) + #sf_certfile = os.path.join(d, "sourceforge-imap.pem") + #sf_cert = ssl.fetch_server_certificate('pop.gmail.com', 995) + #open(sf_certfile, 'w').write(sf_cert) + #return d, crtfile, sf_certfile + # sys.stderr.write(open(crtfile, 'r').read() + '\n') + return d, crtfile + +def test_main(): + if skip_expected: + raise test_support.TestSkipped("socket module has no ssl support") + + global CERTFILE + tdir, CERTFILE = create_cert_files() + + tests = [BasicTests] + + server = None + if test_support.is_resource_enabled('network'): + server = threadedEchoServer(10024, CERTFILE) + server.start() + time.sleep(1) + tests.append(ConnectedTests) + + thread_info = test_support.threading_setup() + + try: + test_support.run_unittest(*tests) + finally: + if server is not None and server.active: + server.stop() + # wait for it to stop + server.join() + + shutil.rmtree(tdir) + test_support.threading_cleanup(*thread_info) + +if __name__ == "__main__": + test_main() Property changes on: Lib/test/test_ssl.py ___________________________________________________________________ Name: svn:keywords + Id Name: svn:eol-style + native Index: Modules/_ssl.c =================================================================== --- Modules/_ssl.c (revision 57412) +++ Modules/_ssl.c (working copy) @@ -5,9 +5,12 @@ This module is imported by socket.py. It should *not* be used directly. + TODO: look up C indent standard for Python and teach Emacs to do it + */ #include "Python.h" + enum py_ssl_error { /* these mirror ssl.h */ PY_SSL_ERROR_NONE, @@ -23,6 +26,24 @@ PY_SSL_ERROR_INVALID_ERROR_CODE }; +enum py_ssl_server_or_client { + PY_SSL_CLIENT, + PY_SSL_SERVER +}; + +enum py_ssl_cert_requirements { + PY_SSL_CERT_NONE, + PY_SSL_CERT_OPTIONAL, + PY_SSL_CERT_REQUIRED +}; + +enum py_ssl_version { + PY_SSL_VERSION_SSL2, + PY_SSL_VERSION_SSL3, + PY_SSL_VERSION_SSL23, + PY_SSL_VERSION_TLS1, +}; + /* Include symbols from _socket module */ #include "socketmodule.h" @@ -60,7 +81,7 @@ PySocketSockObject *Socket; /* Socket on which we're layered */ SSL_CTX* ctx; SSL* ssl; - X509* server_cert; + X509* peer_cert; char server[X509_NAME_MAXLEN]; char issuer[X509_NAME_MAXLEN]; @@ -71,7 +92,9 @@ static PyObject *PySSL_SSLread(PySSLObject *self, PyObject *args); static int check_socket_and_wait_for_timeout(PySocketSockObject *s, int writing); +static PyObject *PySSL_peercert(PySSLObject *self); + #define PySSLObject_Check(v) (Py_Type(v) == &PySSL_Type) typedef enum { @@ -83,15 +106,21 @@ SOCKET_OPERATION_OK } timeout_state; +/* Wrap error strings with filename and line # */ +#define STRINGIFY1(x) #x +#define STRINGIFY2(x) STRINGIFY1(x) +#define ERRSTR1(x,y,z) (x ":" y ": " z) +#define ERRSTR(x) ERRSTR1("_ssl.c", STRINGIFY2(__LINE__), x) + /* XXX It might be helpful to augment the error message generated below with the name of the SSL function that generated the error. I expect it's obvious most of the time. */ static PyObject * -PySSL_SetError(PySSLObject *obj, int ret) +PySSL_SetError(PySSLObject *obj, int ret, char *filename, int lineno) { - PyObject *v, *n, *s; + PyObject *v; char *errstr; int err; enum py_ssl_error p; @@ -158,29 +187,23 @@ p = PY_SSL_ERROR_INVALID_ERROR_CODE; errstr = "Invalid error code"; } - n = PyInt_FromLong((long) p); - if (n == NULL) - return NULL; - v = PyTuple_New(2); - if (v == NULL) { - Py_DECREF(n); - return NULL; - } - s = PyString_FromString(errstr); - if (s == NULL) { + char buf[2048]; + PyOS_snprintf(buf, sizeof(buf), "_ssl.c:%d: %s", lineno, errstr); + v = Py_BuildValue("(is)", p, buf); + if (v != NULL) { + PyErr_SetObject(PySSLErrorObject, v); Py_DECREF(v); - Py_DECREF(n); } - PyTuple_SET_ITEM(v, 0, n); - PyTuple_SET_ITEM(v, 1, s); - PyErr_SetObject(PySSLErrorObject, v); - Py_DECREF(v); return NULL; } static PySSLObject * -newPySSLObject(PySocketSockObject *Sock, char *key_file, char *cert_file) +newPySSLObject(PySocketSockObject *Sock, char *key_file, char *cert_file, + enum py_ssl_server_or_client socket_type, + enum py_ssl_cert_requirements certreq, + enum py_ssl_version proto_version, + char *cacerts_file) { PySSLObject *self; char *errstr = NULL; @@ -193,31 +216,59 @@ return NULL; memset(self->server, '\0', sizeof(char) * X509_NAME_MAXLEN); memset(self->issuer, '\0', sizeof(char) * X509_NAME_MAXLEN); - self->server_cert = NULL; + self->peer_cert = NULL; self->ssl = NULL; self->ctx = NULL; self->Socket = NULL; if ((key_file && !cert_file) || (!key_file && cert_file)) { - errstr = "Both the key & certificate files must be specified"; + errstr = ERRSTR("Both the key & certificate files must be specified"); goto fail; } + if ((socket_type == PY_SSL_SERVER) && + ((key_file == NULL) || (cert_file == NULL))) { + errstr = ERRSTR("Both the key & certificate files must be specified for server-side operation"); + goto fail; + } + Py_BEGIN_ALLOW_THREADS + if (proto_version == PY_SSL_VERSION_TLS1) + self->ctx = SSL_CTX_new(TLSv1_method()); /* Set up context */ + else if (proto_version == PY_SSL_VERSION_SSL3) + self->ctx = SSL_CTX_new(SSLv3_method()); /* Set up context */ + else if (proto_version == PY_SSL_VERSION_SSL2) + self->ctx = SSL_CTX_new(SSLv2_method()); /* Set up context */ + else self->ctx = SSL_CTX_new(SSLv23_method()); /* Set up context */ Py_END_ALLOW_THREADS + if (self->ctx == NULL) { - errstr = "SSL_CTX_new error"; + errstr = ERRSTR("Invalid SSL protocol variant specified."); goto fail; } + if (certreq != PY_SSL_CERT_NONE) { + if (cacerts_file == NULL) { + errstr = ERRSTR("No root certificates specified for verification of other-side certificates."); + goto fail; + } else { + Py_BEGIN_ALLOW_THREADS + ret = SSL_CTX_load_verify_locations(self->ctx, cacerts_file, NULL); + Py_END_ALLOW_THREADS + if (ret < 1) { + errstr = ERRSTR("SSL_CTX_load_verify_locations"); + goto fail; + } + } + } if (key_file) { Py_BEGIN_ALLOW_THREADS ret = SSL_CTX_use_PrivateKey_file(self->ctx, key_file, SSL_FILETYPE_PEM); Py_END_ALLOW_THREADS if (ret < 1) { - errstr = "SSL_CTX_use_PrivateKey_file error"; + errstr = ERRSTR("SSL_CTX_use_PrivateKey_file error"); goto fail; } @@ -225,16 +276,21 @@ ret = SSL_CTX_use_certificate_chain_file(self->ctx, cert_file); Py_END_ALLOW_THREADS - SSL_CTX_set_options(self->ctx, SSL_OP_ALL); /* ssl compatibility */ if (ret < 1) { - errstr = "SSL_CTX_use_certificate_chain_file error"; + errstr = ERRSTR("SSL_CTX_use_certificate_chain_file error") ; goto fail; } + SSL_CTX_set_options(self->ctx, SSL_OP_ALL); /* ssl compatibility */ } + int verification_mode = SSL_VERIFY_NONE; + if (certreq == PY_SSL_CERT_OPTIONAL) + verification_mode = SSL_VERIFY_PEER; + else if (certreq == PY_SSL_CERT_REQUIRED) + verification_mode = (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT); + SSL_CTX_set_verify(self->ctx, verification_mode, NULL); /* set verify lvl */ + Py_BEGIN_ALLOW_THREADS - SSL_CTX_set_verify(self->ctx, - SSL_VERIFY_NONE, NULL); /* set verify lvl */ self->ssl = SSL_new(self->ctx); /* New ssl struct */ Py_END_ALLOW_THREADS SSL_set_fd(self->ssl, Sock->sock_fd); /* Set the socket for SSL */ @@ -249,7 +305,10 @@ } Py_BEGIN_ALLOW_THREADS - SSL_set_connect_state(self->ssl); + if (socket_type == PY_SSL_CLIENT) + SSL_set_connect_state(self->ssl); + else + SSL_set_accept_state(self->ssl); Py_END_ALLOW_THREADS /* Actually negotiate SSL connection */ @@ -257,7 +316,10 @@ sockstate = 0; do { Py_BEGIN_ALLOW_THREADS - ret = SSL_connect(self->ssl); + if (socket_type == PY_SSL_CLIENT) + ret = SSL_connect(self->ssl); + else + ret = SSL_accept(self->ssl); err = SSL_get_error(self->ssl, ret); Py_END_ALLOW_THREADS if(PyErr_CheckSignals()) { @@ -271,29 +333,29 @@ sockstate = SOCKET_OPERATION_OK; } if (sockstate == SOCKET_HAS_TIMED_OUT) { - PyErr_SetString(PySSLErrorObject, "The connect operation timed out"); + PyErr_SetString(PySSLErrorObject, ERRSTR("The connect operation timed out")); goto fail; } else if (sockstate == SOCKET_HAS_BEEN_CLOSED) { - PyErr_SetString(PySSLErrorObject, "Underlying socket has been closed."); + PyErr_SetString(PySSLErrorObject, ERRSTR("Underlying socket has been closed.")); goto fail; } else if (sockstate == SOCKET_TOO_LARGE_FOR_SELECT) { - PyErr_SetString(PySSLErrorObject, "Underlying socket too large for select()."); + PyErr_SetString(PySSLErrorObject, ERRSTR("Underlying socket too large for select().")); goto fail; } else if (sockstate == SOCKET_IS_NONBLOCKING) { break; } } while (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE); - if (ret <= 0) { - PySSL_SetError(self, ret); + if (ret < 1) { + PySSL_SetError(self, ret, __FILE__, __LINE__); goto fail; } self->ssl->debug = 1; Py_BEGIN_ALLOW_THREADS - if ((self->server_cert = SSL_get_peer_certificate(self->ssl))) { - X509_NAME_oneline(X509_get_subject_name(self->server_cert), + if ((self->peer_cert = SSL_get_peer_certificate(self->ssl))) { + X509_NAME_oneline(X509_get_subject_name(self->peer_cert), self->server, X509_NAME_MAXLEN); - X509_NAME_oneline(X509_get_issuer_name(self->server_cert), + X509_NAME_oneline(X509_get_issuer_name(self->peer_cert), self->issuer, X509_NAME_MAXLEN); } Py_END_ALLOW_THREADS @@ -310,25 +372,34 @@ static PyObject * PySocket_ssl(PyObject *self, PyObject *args) { - PySSLObject *rv; PySocketSockObject *Sock; + int server_side = 0; + int verification_mode = PY_SSL_CERT_NONE; + int protocol = PY_SSL_VERSION_SSL23; char *key_file = NULL; char *cert_file = NULL; + char *cacerts_file = NULL; - if (!PyArg_ParseTuple(args, "O!|zz:ssl", + if (!PyArg_ParseTuple(args, "O!i|zziiz:sslwrap", PySocketModule.Sock_Type, &Sock, - &key_file, &cert_file)) + &server_side, + &key_file, &cert_file, + &verification_mode, &protocol, + &cacerts_file)) return NULL; - rv = newPySSLObject(Sock, key_file, cert_file); - if (rv == NULL) - return NULL; - return (PyObject *)rv; + /* + fprintf(stderr, "server_side is %d, keyfile %p, certfile %p, verify_mode %d, protocol %d, certs %p\n", + server_side, key_file, cert_file, verification_mode, protocol, cacerts_file); + */ + + return (PyObject *) newPySSLObject(Sock, key_file, cert_file, server_side, + verification_mode, protocol, cacerts_file); } PyDoc_STRVAR(ssl_doc, -"ssl(socket, [keyfile, certfile]) -> sslobject"); +"sslwrap(socket, server_side, [keyfile, certfile, certs_mode, protocol, cacertsfile]) -> sslobject"); /* SSL object methods */ @@ -344,11 +415,148 @@ return PyString_FromString(self->issuer); } +static PyObject * +_create_dict_for_X509_NAME (X509_NAME *xname) { + PyObject *pd = PyDict_New(); + int index_counter; + for (index_counter = 0; index_counter < X509_NAME_entry_count(xname); index_counter++) { + char namebuf[X509_NAME_MAXLEN]; + int buflen; + + X509_NAME_ENTRY *entry = X509_NAME_get_entry(xname, index_counter); + + ASN1_OBJECT *name = X509_NAME_ENTRY_get_object(entry); + buflen = OBJ_obj2txt(namebuf, sizeof(namebuf), name, 0); + if (buflen < 0) + goto fail0; + PyObject *name_obj = PyString_FromStringAndSize(namebuf, buflen); + if (name_obj == NULL) + goto fail0; + + ASN1_STRING *value = X509_NAME_ENTRY_get_data(entry); + unsigned char *valuebuf = NULL; + buflen = ASN1_STRING_to_UTF8(&valuebuf, value); + if (buflen < 0) { + Py_DECREF(name_obj); + goto fail0; + } + PyObject *value_obj = PyUnicode_DecodeUTF8((char *) valuebuf, buflen, "strict"); + OPENSSL_free(valuebuf); + if (value_obj == NULL) { + Py_DECREF(name_obj); + goto fail0; + } + if (PyDict_SetItem(pd, name_obj, value_obj) < 0) { + Py_DECREF(name_obj); + Py_DECREF(value_obj); + goto fail0; + } + Py_DECREF(name_obj); + Py_DECREF(value_obj); + } + return pd; + + fail0: + Py_XDECREF(pd); + return NULL; +} + +static PyObject * +PySSL_peercert(PySSLObject *self) { + + PyObject *retval = NULL; + BIO *biobuf = NULL; + + if (!self->peer_cert) + Py_RETURN_NONE; + + { + retval = PyDict_New(); + if (retval == NULL) + return NULL; + + int verification = SSL_CTX_get_verify_mode(self->ctx); + if ((verification & SSL_VERIFY_PEER) == 0) + return retval; + { + PyObject *peer = _create_dict_for_X509_NAME(X509_get_subject_name(self->peer_cert)); + if (peer == NULL) + goto fail0; + if (PyDict_SetItemString(retval, (const char *) "subject", peer) < 0) { + Py_DECREF(peer); + goto fail0; + } + Py_DECREF(peer); + + PyObject *issuer = _create_dict_for_X509_NAME(X509_get_issuer_name(self->peer_cert)); + if (issuer == NULL) + goto fail0; + if (PyDict_SetItemString(retval, (const char *) "issuer", issuer) < 0) { + Py_DECREF(issuer); + goto fail0; + } + Py_DECREF(issuer); + + PyObject *version = PyInt_FromLong(X509_get_version(self->peer_cert)); + if (PyDict_SetItemString(retval, "version", version) < 0) { + Py_DECREF(version); + goto fail0; + } + Py_DECREF(version); + + char buf[2048]; + int len; + + /* get a memory buffer */ + biobuf = BIO_new(BIO_s_mem()); + + ASN1_TIME *notBefore = X509_get_notBefore(self->peer_cert); + ASN1_TIME_print(biobuf, notBefore); + len = BIO_gets(biobuf, buf, sizeof(buf)-1); + PyObject *pnotBefore = PyString_FromStringAndSize(buf, len); + if (pnotBefore == NULL) + goto fail1; + if (PyDict_SetItemString(retval, "notBefore", pnotBefore) < 0) { + Py_DECREF(pnotBefore); + goto fail1; + } + Py_DECREF(pnotBefore); + + BIO_reset(biobuf); + ASN1_TIME *notAfter = X509_get_notAfter(self->peer_cert); + ASN1_TIME_print(biobuf, notAfter); + len = BIO_gets(biobuf, buf, sizeof(buf)-1); + BIO_free(biobuf); + PyObject *pnotAfter = PyString_FromStringAndSize(buf, len); + if (pnotAfter == NULL) + goto fail0; + if (PyDict_SetItemString(retval, "notAfter", pnotAfter) < 0) { + Py_DECREF(pnotAfter); + goto fail0; + } + Py_DECREF(pnotAfter); + + } + + } + return retval; + + fail1: + if (biobuf != NULL) { + BIO_free(biobuf); + } + fail0: + if (retval != NULL) { + Py_DECREF(retval); + } + return NULL; +} + static void PySSL_dealloc(PySSLObject *self) { - if (self->server_cert) /* Possible not to have one? */ - X509_free (self->server_cert); + if (self->peer_cert) /* Possible not to have one? */ + X509_free (self->peer_cert); if (self->ssl) SSL_free(self->ssl); if (self->ctx) @@ -476,7 +684,7 @@ if (len > 0) return PyInt_FromLong(len); else - return PySSL_SetError(self, len); + return PySSL_SetError(self, len, __FILE__, __LINE__); } PyDoc_STRVAR(PySSL_SSLwrite_doc, @@ -512,7 +720,18 @@ return NULL; } else if (sockstate == SOCKET_TOO_LARGE_FOR_SELECT) { PyErr_SetString(PySSLErrorObject, "Underlying socket too large for select()."); - return NULL; + Py_DECREF(buf); + return NULL; + } else if (sockstate == SOCKET_HAS_BEEN_CLOSED) { + if (SSL_get_shutdown(self->ssl) != SSL_RECEIVED_SHUTDOWN) { + Py_DECREF(buf); + PyErr_SetString(PySSLErrorObject, "Socket closed without SSL shutdown handshake"); + return NULL; + } else { + /* should contain a zero-length string */ + _PyString_Resize(&buf, 0); + return buf; + } } } do { @@ -529,6 +748,10 @@ sockstate = check_socket_and_wait_for_timeout(self->Socket, 0); } else if (err == SSL_ERROR_WANT_WRITE) { sockstate = check_socket_and_wait_for_timeout(self->Socket, 1); + } else if ((err == SSL_ERROR_ZERO_RETURN) && + (SSL_get_shutdown(self->ssl) == SSL_RECEIVED_SHUTDOWN)) { + _PyString_Resize(&buf, 0); + return buf; } else { sockstate = SOCKET_OPERATION_OK; } @@ -542,7 +765,7 @@ } while (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE); if (count <= 0) { Py_DECREF(buf); - return PySSL_SetError(self, count); + return PySSL_SetError(self, count, __FILE__, __LINE__); } if (count != len) _PyString_Resize(&buf, count); @@ -554,6 +777,38 @@ \n\ Read up to len bytes from the SSL socket."); +static PyObject *PySSL_SSLshutdown(PySSLObject *self, PyObject *args) +{ + int err; + + /* Guard against closed socket */ + if (self->Socket->sock_fd < 0) { + PyErr_SetString(PySSLErrorObject, "Underlying socket has been closed."); + return NULL; + } + + Py_BEGIN_ALLOW_THREADS + err = SSL_shutdown(self->ssl); + if (err == 0) { + /* we need to call it again to finish the shutdown */ + err = SSL_shutdown(self->ssl); + } + Py_END_ALLOW_THREADS + + if (err < 0) + return PySSL_SetError(self, err, __FILE__, __LINE__); + else { + Py_INCREF(self->Socket); + return (PyObject *) (self->Socket); + } +} + +PyDoc_STRVAR(PySSL_SSLshutdown_doc, +"shutdown(s) -> socket\n\ +\n\ +Does the SSL shutdown handshake with the remote end, and returns\n\ +the underlying socket object."); + static PyMethodDef PySSLMethods[] = { {"write", (PyCFunction)PySSL_SSLwrite, METH_VARARGS, PySSL_SSLwrite_doc}, @@ -561,6 +816,8 @@ PySSL_SSLread_doc}, {"server", (PyCFunction)PySSL_server, METH_NOARGS}, {"issuer", (PyCFunction)PySSL_issuer, METH_NOARGS}, + {"peer_certificate", (PyCFunction)PySSL_peercert, METH_NOARGS}, + {"shutdown", (PyCFunction)PySSL_SSLshutdown, METH_NOARGS, PySSL_SSLshutdown_doc}, {NULL, NULL} }; @@ -654,7 +911,7 @@ /* List of functions exported by this module. */ static PyMethodDef PySSL_methods[] = { - {"ssl", PySocket_ssl, + {"sslwrap", PySocket_ssl, METH_VARARGS, ssl_doc}, #ifdef HAVE_OPENSSL_RAND {"RAND_add", PySSL_RAND_add, METH_VARARGS, @@ -699,6 +956,7 @@ if (PySSLErrorObject == NULL) return; PyDict_SetItemString(d, "sslerror", PySSLErrorObject); + if (PyDict_SetItemString(d, "SSLType", (PyObject *)&PySSL_Type) != 0) return; @@ -721,5 +979,21 @@ PY_SSL_ERROR_EOF); PyModule_AddIntConstant(m, "SSL_ERROR_INVALID_ERROR_CODE", PY_SSL_ERROR_INVALID_ERROR_CODE); + /* cert requirements */ + PyModule_AddIntConstant(m, "CERT_NONE", + PY_SSL_CERT_NONE); + PyModule_AddIntConstant(m, "CERT_OPTIONAL", + PY_SSL_CERT_OPTIONAL); + PyModule_AddIntConstant(m, "CERT_REQUIRED", + PY_SSL_CERT_REQUIRED); + /* protocol versions */ + PyModule_AddIntConstant(m, "PROTOCOL_SSLv2", + PY_SSL_VERSION_SSL2); + PyModule_AddIntConstant(m, "PROTOCOL_SSLv3", + PY_SSL_VERSION_SSL3); + PyModule_AddIntConstant(m, "PROTOCOL_SSLv23", + PY_SSL_VERSION_SSL23); + PyModule_AddIntConstant(m, "PROTOCOL_TLSv1", + PY_SSL_VERSION_TLS1); }