diff -r ce29bd44ea7c Doc/library/ssl.rst --- a/Doc/library/ssl.rst Wed May 05 18:31:07 2010 +0200 +++ b/Doc/library/ssl.rst Wed May 05 18:45:43 2010 +0200 @@ -36,6 +36,11 @@ additional :meth:`read` and :meth:`write connection, and a method, :meth:`cipher`, to retrieve the cipher being used for the secure connection. +For more sophisticated applications, the :class:`ssl.SSLContext` class +helps manage settings and certificates, which can then be inherited +by SSL sockets created through the :meth:`SSLContext.wrap_socket` method. + + Functions, Constants, and Exceptions ------------------------------------ @@ -64,19 +69,6 @@ Functions, Constants, and Exceptions connection. See the discussion of :ref:`ssl-certificates` for more information on how the certificate is stored in the ``certfile``. - Often the private key is stored in the same file as the certificate; in this - case, only the ``certfile`` parameter need be passed. If the private key is - stored in a separate file, both parameters must be used. If the private key - is stored in the ``certfile``, it should come before the first certificate in - the certificate chain:: - - -----BEGIN RSA PRIVATE KEY----- - ... (private key in base64 encoding) ... - -----END RSA PRIVATE KEY----- - -----BEGIN CERTIFICATE----- - ... (certificate in base64 PEM encoding) ... - -----END CERTIFICATE----- - The parameter ``server_side`` is a boolean which identifies whether server-side or client-side behavior is desired from this socket. @@ -208,13 +200,13 @@ Functions, Constants, and Exceptions .. data:: CERT_NONE - Value to pass to the ``cert_reqs`` parameter to :func:`sslobject` when no + Value to pass to the ``cert_reqs`` parameter to :func:`wrap_socket` when no certificates will be required or validated from the other side of the socket connection. .. data:: CERT_OPTIONAL - Value to pass to the ``cert_reqs`` parameter to :func:`sslobject` when no + Value to pass to the ``cert_reqs`` parameter to :func:`wrap_socket` when no certificates will be required from the other side of the socket connection, but if they are provided, will be validated. Note that use of this setting requires a valid certificate validation file also be passed as a value of the @@ -222,7 +214,7 @@ Functions, Constants, and Exceptions .. data:: CERT_REQUIRED - Value to pass to the ``cert_reqs`` parameter to :func:`sslobject` when + Value to pass to the ``cert_reqs`` parameter to :func:`wrap_socket` when certificates will be required from the other side of the socket connection. Note that use of this setting requires a valid certificate validation file also be passed as a value of the ``ca_certs`` parameter. @@ -280,8 +272,8 @@ Functions, Constants, and Exceptions .. versionadded:: 3.2 -SSLSocket Objects ------------------ +SSL Sockets +----------- .. method:: SSLSocket.read(nbytes=1024, buffer=None) @@ -367,6 +359,83 @@ SSLSocket Objects returned socket should always be used for further communication with the other side of the connection, rather than the original socket. + +SSL Contexts +------------ + +.. class:: SSLContext(protocol) + + An object holding various data longer-lived than single SSL connections, + such as SSL configuration options, certificate(s) and private key(s). + You must pass *protocol* which must be one of the ``PROTOCOL_*`` constants + defined in this module. :data:`PROTOCOL_SSLv23` is recommended for + maximum interoperability. + +:class:`SSLContext` objects have the following methods and attributes: + +.. method:: SSLContext.load_cert_chain(certfile, keyfile=None) + + Load a private key and the corresponding certificate. The *certfile* + string must be the path to a single file in PEM format containing the + certificate as well as any number of CA certificates needed to establish + the certificate's authenticity. The *keyfile* string, if present, must + point to a file containing the private key in. Otherwise the private + key will be taken from *certfile* as well. See the discussion of + :ref:`ssl-certificates` for more information on how the certificate + is stored in the *certfile*. + + An :class:`SSLError` is raised if the private key doesn't + match with the certificate. + +.. method:: SSLContext.load_verify_locations(cafile=None, capath=None) + + Load a set of "certification authority" (CA) certificates used to validate + other peers' certificates when :data:`verify_mode` is other than + :data:`CERT_NONE`. At least one of *cafile* or *capath* must be specified. + + The *cafile* string, if present, is the path to a file of concatenated + CA certificates in PEM format. See the discussion of + :ref:`ssl-certificates` for more information about how to arrange the + certificates in this file. + + The *capath* string, if present, is + the path to a directory containing several CA certificates in PEM format, + following an `OpenSSL specific layout + `_. + +.. method:: SSLContext.set_ciphers(ciphers) + + Set the available ciphers for sockets created with this context. + It should be a string in the `OpenSSL cipher list format + `_. + If no cipher can be selected (because compile-time options or other + configuration forbids use of all the specified ciphers), an + :class:`SSLError` will be raised. + + .. note:: + when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will + give the currently selected cipher. + +.. method:: SSLContext.wrap_socket(sock, server_side=False, do_handshake_on_connect=True, suppress_ragged_eofs=True) + + Wrap an existing Python socket *sock* and return an :class:`SSLSocket` + object. The SSL socket is tied to the context, its settings and + certificates. The parameters *server_side*, *do_handshake_on_connect* + and *suppress_ragged_eofs* have the same meaning as in the top-level + :func:`wrap_socket` function. + +.. attribute:: SSLContext.protocol + + The protocol version chosen when constructing the context. This attribute + is read-only. + +.. attribute:: SSLContext.verify_mode + + Whether to try to verify other peers' certificates and how to behave + if verification fails. This attribute must be one of + :data:`CERT_NONE`, :data:`CERT_OPTIONAL` or :data:`CERT_REQUIRED`. + + .. index:: single: certificates .. index:: single: X509 certificate @@ -412,6 +481,9 @@ and a footer line:: ... (certificate in base64 PEM encoding) ... -----END CERTIFICATE----- +Certificate chains +^^^^^^^^^^^^^^^^^^ + The Python files which contain certificates can contain a sequence of certificates, sometimes called a *certificate chain*. This chain should start with the specific certificate for the principal who "is" the client or server, @@ -435,6 +507,9 @@ certification authority's certificate:: ... (the root certificate for the CA's issuer)... -----END CERTIFICATE----- +CA certificates +^^^^^^^^^^^^^^^ + If you are going to require validation of the other side of the connection's certificate, you need to provide a "CA certs" file, filled with the certificate chains for each issuer you are willing to trust. Again, this file just contains @@ -454,6 +529,25 @@ peer is supposed to furnish the other ce certificate to a root certificate. See :rfc:`4158` for more discussion of the way in which certification chains can be built. +Combined key and certificate +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Often the private key is stored in the same file as the certificate; in this +case, only the ``certfile`` parameter to :meth:`SSLContext.load_cert_chain` +and :func:`wrap_socket` needs to be passed. If the private key is stored +with the certificate, it should come before the first certificate in +the certificate chain:: + + -----BEGIN RSA PRIVATE KEY----- + ... (private key in base64 encoding) ... + -----END RSA PRIVATE KEY----- + -----BEGIN CERTIFICATE----- + ... (certificate in base64 PEM encoding) ... + -----END CERTIFICATE----- + +Self-signed certificates +^^^^^^^^^^^^^^^^^^^^^^^^ + If you are going to create a server that provides SSL-encrypted connection services, you will need to acquire a certificate for that service. There are many ways of acquiring appropriate certificates, such as buying one from a diff -r ce29bd44ea7c Lib/ssl.py --- a/Lib/ssl.py Wed May 05 18:31:07 2010 +0200 +++ b/Lib/ssl.py Wed May 05 18:45:43 2010 +0200 @@ -59,7 +59,7 @@ import textwrap import _ssl # if we can't import it, let the error propagate from _ssl import OPENSSL_VERSION_NUMBER, OPENSSL_VERSION_INFO, OPENSSL_VERSION -from _ssl import SSLError +from _ssl import _SSLContext, SSLError from _ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED from _ssl import (PROTOCOL_SSLv2, PROTOCOL_SSLv3, PROTOCOL_SSLv23, PROTOCOL_TLSv1) @@ -84,6 +84,25 @@ import base64 # for DER-to-PEM tr import traceback import errno + +class SSLContext(_SSLContext): + __slots__ = ('protocol',) + + def __new__(cls, protocol, *args, **kwargs): + return _SSLContext.__new__(cls, protocol) + + def __init__(self, protocol): + self.protocol = protocol + + def wrap_socket(self, sock, server_side=False, + do_handshake_on_connect=True, + suppress_ragged_eofs=True): + return SSLSocket(sock=sock, server_side=server_side, + do_handshake_on_connect=do_handshake_on_connect, + suppress_ragged_eofs=suppress_ragged_eofs, + _context=self) + + class SSLSocket(socket): """This class implements a subtype of socket.socket that wraps @@ -95,8 +114,31 @@ class SSLSocket(socket): ssl_version=PROTOCOL_SSLv23, ca_certs=None, do_handshake_on_connect=True, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None, - suppress_ragged_eofs=True, ciphers=None): + suppress_ragged_eofs=True, ciphers=None, + _context=None): + if _context: + self.context = _context + else: + if certfile and not keyfile: + keyfile = certfile + self.context = SSLContext(ssl_version) + self.context.verify_mode = cert_reqs + if ca_certs: + self.context.load_verify_locations(ca_certs) + if certfile: + self.context.load_cert_chain(certfile, keyfile) + if ciphers: + self.context.set_ciphers(ciphers) + self.keyfile = keyfile + self.certfile = certfile + self.cert_reqs = cert_reqs + self.ssl_version = ssl_version + self.ca_certs = ca_certs + self.ciphers = ciphers + + self.do_handshake_on_connect = do_handshake_on_connect + self.suppress_ragged_eofs = suppress_ragged_eofs connected = False if sock is not None: socket.__init__(self, @@ -119,18 +161,12 @@ class SSLSocket(socket): else: socket.__init__(self, family=family, type=type, proto=proto) - if certfile and not keyfile: - keyfile = certfile - self._closed = False self._sslobj = None if connected: # create the SSL object try: - self._sslobj = _ssl.sslwrap(self, server_side, - keyfile, certfile, - cert_reqs, ssl_version, ca_certs, - ciphers) + self._sslobj = self.context._wrap_socket(self, server_side) if do_handshake_on_connect: timeout = self.gettimeout() if timeout == 0.0: @@ -142,15 +178,6 @@ class SSLSocket(socket): self.close() raise x - self.keyfile = keyfile - self.certfile = certfile - self.cert_reqs = cert_reqs - self.ssl_version = ssl_version - self.ca_certs = ca_certs - self.ciphers = ciphers - self.do_handshake_on_connect = do_handshake_on_connect - self.suppress_ragged_eofs = suppress_ragged_eofs - def dup(self): raise NotImplemented("Can't dup() %s instances" % self.__class__.__name__) @@ -331,9 +358,7 @@ class SSLSocket(socket): if self._sslobj: raise ValueError("attempt to connect already-connected SSLSocket!") socket.connect(self, addr) - self._sslobj = _ssl.sslwrap(self, False, self.keyfile, self.certfile, - self.cert_reqs, self.ssl_version, - self.ca_certs, self.ciphers) + self._sslobj = self.context._wrap_socket(self, False) try: if self.do_handshake_on_connect: self.do_handshake() diff -r ce29bd44ea7c Lib/test/test_ssl.py --- a/Lib/test/test_ssl.py Wed May 05 18:31:07 2010 +0200 +++ b/Lib/test/test_ssl.py Wed May 05 18:45:43 2010 +0200 @@ -10,6 +10,7 @@ import gc import os import errno import pprint +import tempfile import urllib.parse, urllib.request import traceback import asyncore @@ -25,8 +26,28 @@ except ImportError: skip_expected = True HOST = support.HOST -CERTFILE = None -SVN_PYTHON_ORG_ROOT_CERT = None +PROTOCOLS = [ + ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv3, + ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_TLSv1 +] + +data_file = lambda name: os.path.join(os.path.dirname(__file__), name) +fsencode = lambda name: name.encode(sys.getfilesystemencoding(), "surrogateescape") + +CERTFILE = data_file("keycert.pem") +BYTES_CERTFILE = fsencode(CERTFILE) +ONLYCERT = data_file("ssl_cert.pem") +ONLYKEY = data_file("ssl_key.pem") +BYTES_ONLYCERT = fsencode(ONLYCERT) +BYTES_ONLYKEY = fsencode(ONLYKEY) + +SVN_PYTHON_ORG_ROOT_CERT = data_file("https_svn_python_org_root.pem") + +EMPTYCERT = data_file("nullcert.pem") +BADCERT = data_file("badcert.pem") +WRONGCERT = data_file("XXXnonexisting.pem") +BADKEY = data_file("badkey.pem") + def handle_error(prefix): exc_format = ' '.join(traceback.format_exception(*sys.exc_info())) @@ -34,7 +55,7 @@ def handle_error(prefix): sys.stdout.write(prefix + exc_format) -class BasicTests(unittest.TestCase): +class BasicSocketTests(unittest.TestCase): def test_constants(self): ssl.PROTOCOL_SSLv2 @@ -116,11 +137,10 @@ class BasicTests(unittest.TestCase): s = ssl.wrap_socket(socket.socket(socket.AF_INET), cert_reqs=ssl.CERT_NONE, ciphers="DEFAULT") s.connect(remote) - # Error checking occurs when connecting, because the SSL context - # isn't created before. - s = ssl.wrap_socket(socket.socket(socket.AF_INET), - cert_reqs=ssl.CERT_NONE, ciphers="^$:,;?*'dorothyx") + # Error checking can happen at instantiation or when connecting with self.assertRaisesRegexp(ssl.SSLError, "No cipher can be selected"): + s = ssl.wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_NONE, ciphers="^$:,;?*'dorothyx") s.connect(remote) @support.cpython_only @@ -143,16 +163,101 @@ class BasicTests(unittest.TestCase): self.assertEqual(timeout, ss.gettimeout()) +class ContextTests(unittest.TestCase): + + def test_constructor(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv2) + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv3) + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + self.assertRaises(TypeError, ssl.SSLContext) + self.assertRaises(ValueError, ssl.SSLContext, -1) + self.assertRaises(ValueError, ssl.SSLContext, 42) + + def test_protocol(self): + for proto in PROTOCOLS: + ctx = ssl.SSLContext(proto) + self.assertEqual(ctx.protocol, proto) + + def test_ciphers(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + ctx.set_ciphers("ALL") + ctx.set_ciphers("DEFAULT") + with self.assertRaisesRegexp(ssl.SSLError, "No cipher can be selected"): + ctx.set_ciphers("^$:,;?*'dorothyx") + + def test_verify(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + # Default value + self.assertEqual(ctx.verify_mode, ssl.CERT_NONE) + ctx.verify_mode = ssl.CERT_OPTIONAL + self.assertEqual(ctx.verify_mode, ssl.CERT_OPTIONAL) + ctx.verify_mode = ssl.CERT_REQUIRED + self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED) + ctx.verify_mode = ssl.CERT_NONE + self.assertEqual(ctx.verify_mode, ssl.CERT_NONE) + with self.assertRaises(TypeError): + ctx.verify_mode = None + with self.assertRaises(ValueError): + ctx.verify_mode = 42 + + def test_load_cert_chain(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + # Combined key and cert in a single file + ctx.load_cert_chain(CERTFILE) + ctx.load_cert_chain(CERTFILE, keyfile=CERTFILE) + self.assertRaises(TypeError, ctx.load_cert_chain, keyfile=CERTFILE) + with self.assertRaisesRegexp(ssl.SSLError, "system lib"): + ctx.load_cert_chain(WRONGCERT) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_cert_chain(BADCERT) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_cert_chain(EMPTYCERT) + # Separate key and cert + ctx.load_cert_chain(ONLYCERT, ONLYKEY) + ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY) + ctx.load_cert_chain(certfile=BYTES_ONLYCERT, keyfile=BYTES_ONLYKEY) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_cert_chain(ONLYCERT) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_cert_chain(ONLYKEY) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_cert_chain(certfile=ONLYKEY, keyfile=ONLYCERT) + # Mismatching key and cert + with self.assertRaisesRegexp(ssl.SSLError, "key values mismatch"): + ctx.load_cert_chain(CERTFILE, ONLYKEY) + + def test_load_verify_locations(self): + # XXX capath is mostly untested + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + ctx.load_verify_locations(CERTFILE) + ctx.load_verify_locations(cafile=CERTFILE, capath=None) + ctx.load_verify_locations(BYTES_CERTFILE) + ctx.load_verify_locations(cafile=BYTES_CERTFILE, capath=None) + self.assertRaises(TypeError, ctx.load_verify_locations) + self.assertRaises(TypeError, ctx.load_verify_locations, None, None) + with self.assertRaisesRegexp(ssl.SSLError, "system lib"): + ctx.load_verify_locations(WRONGCERT) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_verify_locations(BADCERT) + # With an empty capath directory + emptydir = tempfile.mkdtemp() + try: + ctx.load_verify_locations(CERTFILE, emptydir) + finally: + os.rmdir(emptydir) + + class NetworkedTests(unittest.TestCase): def test_connect(self): s = ssl.wrap_socket(socket.socket(socket.AF_INET), cert_reqs=ssl.CERT_NONE) - s.connect(("svn.python.org", 443)) - c = s.getpeercert() - if c: - self.fail("Peer cert %s shouldn't be here!") - s.close() + try: + s.connect(("svn.python.org", 443)) + self.assertEqual({}, s.getpeercert()) + finally: + s.close() # this should fail because we have no verification certs s = ssl.wrap_socket(socket.socket(socket.AF_INET), @@ -170,6 +275,35 @@ class NetworkedTests(unittest.TestCase): ca_certs=SVN_PYTHON_ORG_ROOT_CERT) try: s.connect(("svn.python.org", 443)) + self.assertTrue(s.getpeercert()) + finally: + s.close() + + def test_connect_with_context(self): + # Same as test_connect, but with a separately created context + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + s = ctx.wrap_socket(socket.socket(socket.AF_INET)) + try: + s.connect(("svn.python.org", 443)) + self.assertEqual({}, s.getpeercert()) + finally: + s.close() + # This should fail because we have no verification certs + ctx.verify_mode = ssl.CERT_REQUIRED + s = ctx.wrap_socket(socket.socket(socket.AF_INET)) + try: + s.connect(("svn.python.org", 443)) + except ssl.SSLError: + pass + finally: + s.close() + # This should succeed because we specify the root cert + ctx.load_verify_locations(SVN_PYTHON_ORG_ROOT_CERT) + s = ctx.wrap_socket(socket.socket(socket.AF_INET)) + try: + s.connect(("svn.python.org", 443)) + cert = s.getpeercert() + self.assertTrue(cert) finally: s.close() @@ -1227,18 +1361,14 @@ def test_main(verbose=False): if skip_expected: raise unittest.SkipTest("No SSL support") - global CERTFILE, SVN_PYTHON_ORG_ROOT_CERT - CERTFILE = os.path.join(os.path.dirname(__file__) or os.curdir, - "keycert.pem") - SVN_PYTHON_ORG_ROOT_CERT = os.path.join( - os.path.dirname(__file__) or os.curdir, - "https_svn_python_org_root.pem") + for filename in [ + CERTFILE, SVN_PYTHON_ORG_ROOT_CERT, BYTES_CERTFILE, + ONLYCERT, ONLYKEY, BYTES_ONLYCERT, BYTES_ONLYKEY, + BADCERT, BADKEY, EMPTYCERT]: + if not os.path.exists(filename): + raise support.TestFailed("Can't read certificate file %r" % filename) - if (not os.path.exists(CERTFILE) or - not os.path.exists(SVN_PYTHON_ORG_ROOT_CERT)): - raise support.TestFailed("Can't read certificate files!") - - tests = [BasicTests] + tests = [ContextTests, BasicSocketTests] if support.is_resource_enabled('network'): tests.append(NetworkedTests) diff -r ce29bd44ea7c Modules/_ssl.c --- a/Modules/_ssl.c Wed May 05 18:31:07 2010 +0200 +++ b/Modules/_ssl.c Wed May 05 18:45:43 2010 +0200 @@ -115,23 +115,29 @@ static unsigned int _ssl_locks_count = 0 typedef struct { PyObject_HEAD - PyObject *Socket; /* weakref to socket on which we're layered */ - SSL_CTX* ctx; - SSL* ssl; - X509* peer_cert; - int shutdown_seen_zero; + SSL_CTX *ctx; +} PySSLContext; -} PySSLObject; +typedef struct { + PyObject_HEAD + PyObject *Socket; /* weakref to socket on which we're layered */ + SSL *ssl; + X509 *peer_cert; + int shutdown_seen_zero; +} PySSLSocket; -static PyTypeObject PySSL_Type; -static PyObject *PySSL_SSLwrite(PySSLObject *self, PyObject *args); -static PyObject *PySSL_SSLread(PySSLObject *self, PyObject *args); +static PyTypeObject PySSLContext_Type; +static PyTypeObject PySSLSocket_Type; + +static PyObject *PySSL_SSLwrite(PySSLSocket *self, PyObject *args); +static PyObject *PySSL_SSLread(PySSLSocket *self, PyObject *args); static int check_socket_and_wait_for_timeout(PySocketSockObject *s, int writing); -static PyObject *PySSL_peercert(PySSLObject *self, PyObject *args); -static PyObject *PySSL_cipher(PySSLObject *self); +static PyObject *PySSL_peercert(PySSLSocket *self, PyObject *args); +static PyObject *PySSL_cipher(PySSLSocket *self); -#define PySSLObject_Check(v) (Py_TYPE(v) == &PySSL_Type) +#define PySSLContext_Check(v) (Py_TYPE(v) == &PySSLContext_Type) +#define PySSLSocket_Check(v) (Py_TYPE(v) == &PySSLSocket_Type) typedef enum { SOCKET_IS_NONBLOCKING, @@ -154,7 +160,7 @@ typedef enum { */ static PyObject * -PySSL_SetError(PySSLObject *obj, int ret, char *filename, int lineno) +PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno) { PyObject *v; char buf[2048]; @@ -261,126 +267,39 @@ _setSSLError (char *errstr, int errcode, return NULL; } -static PySSLObject * -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, char *ciphers) +static PySSLSocket * +newPySSLSocket(SSL_CTX *ctx, PySocketSockObject *sock, + enum py_ssl_server_or_client socket_type) { - PySSLObject *self; - char *errstr = NULL; - int ret; - int verification_mode; + PySSLSocket *self; + const char *errstr = NULL; - self = PyObject_New(PySSLObject, &PySSL_Type); /* Create new object */ + self = PyObject_New(PySSLSocket, &PySSLSocket_Type); if (self == NULL) return NULL; + self->peer_cert = NULL; self->ssl = NULL; - self->ctx = NULL; self->Socket = NULL; /* Make sure the SSL error state is initialized */ (void) ERR_get_state(); ERR_clear_error(); - if ((key_file && !cert_file) || (!key_file && cert_file)) { - errstr = ERRSTR("Both the key & certificate files " - "must be specified"); - goto fail; - } - + /* XXX */ + #if 0 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; } + #endif PySSL_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 if (proto_version == PY_SSL_VERSION_SSL23) - self->ctx = SSL_CTX_new(SSLv23_method()); /* Set up context */ + self->ssl = SSL_new(ctx); PySSL_END_ALLOW_THREADS - - if (self->ctx == NULL) { - errstr = ERRSTR("Invalid SSL protocol variant specified."); - goto fail; - } - - if (ciphers != NULL) { - ret = SSL_CTX_set_cipher_list(self->ctx, ciphers); - if (ret == 0) { - errstr = ERRSTR("No cipher can be selected."); - 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 { - PySSL_BEGIN_ALLOW_THREADS - ret = SSL_CTX_load_verify_locations(self->ctx, - cacerts_file, - NULL); - PySSL_END_ALLOW_THREADS - if (ret != 1) { - _setSSLError(NULL, 0, __FILE__, __LINE__); - goto fail; - } - } - } - if (key_file) { - PySSL_BEGIN_ALLOW_THREADS - ret = SSL_CTX_use_PrivateKey_file(self->ctx, key_file, - SSL_FILETYPE_PEM); - PySSL_END_ALLOW_THREADS - if (ret != 1) { - _setSSLError(NULL, ret, __FILE__, __LINE__); - goto fail; - } - - PySSL_BEGIN_ALLOW_THREADS - ret = SSL_CTX_use_certificate_chain_file(self->ctx, - cert_file); - PySSL_END_ALLOW_THREADS - if (ret != 1) { - /* - fprintf(stderr, "ret is %d, errcode is %lu, %lu, with file \"%s\"\n", - ret, ERR_peek_error(), ERR_peek_last_error(), cert_file); - */ - if (ERR_peek_last_error() != 0) { - _setSSLError(NULL, ret, __FILE__, __LINE__); - goto fail; - } - } - } - - /* ssl compatibility */ - SSL_CTX_set_options(self->ctx, SSL_OP_ALL); - - 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 */ - - PySSL_BEGIN_ALLOW_THREADS - self->ssl = SSL_new(self->ctx); /* New ssl struct */ - PySSL_END_ALLOW_THREADS - SSL_set_fd(self->ssl, Sock->sock_fd); /* Set the socket for SSL */ + SSL_set_fd(self->ssl, sock->sock_fd); #ifdef SSL_MODE_AUTO_RETRY SSL_set_mode(self->ssl, SSL_MODE_AUTO_RETRY); #endif @@ -388,8 +307,7 @@ newPySSLObject(PySocketSockObject *Sock, /* If the socket is in non-blocking mode or timeout mode, set the BIO * to non-blocking mode (blocking is the default) */ - if (Sock->sock_timeout >= 0.0) { - /* Set both the read and write BIO's to non-blocking mode */ + if (sock->sock_timeout >= 0.0) { BIO_set_nbio(SSL_get_rbio(self->ssl), 1); BIO_set_nbio(SSL_get_wbio(self->ssl), 1); } @@ -401,57 +319,19 @@ newPySSLObject(PySocketSockObject *Sock, SSL_set_accept_state(self->ssl); PySSL_END_ALLOW_THREADS - self->Socket = PyWeakref_NewRef((PyObject *) Sock, Py_None); + self->Socket = PyWeakref_NewRef((PyObject *) sock, NULL); return self; - fail: + +/*fail: if (errstr) PyErr_SetString(PySSLErrorObject, errstr); Py_DECREF(self); - return NULL; + return NULL;*/ } -static PyObject * -PySSL_sslwrap(PyObject *self, PyObject *args) -{ - 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; - char *ciphers = NULL; - - if (!PyArg_ParseTuple(args, "O!i|zziizz:sslwrap", - PySocketModule.Sock_Type, - &Sock, - &server_side, - &key_file, &cert_file, - &verification_mode, &protocol, - &cacerts_file, &ciphers)) - return NULL; - - /* - 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, - ciphers); -} - -PyDoc_STRVAR(ssl_doc, -"sslwrap(socket, server_side, [keyfile, certfile, certs_mode, protocol,\n" -" cacertsfile, ciphers]) -> sslobject"); - /* SSL object methods */ -static PyObject *PySSL_SSLdo_handshake(PySSLObject *self) +static PyObject *PySSL_SSLdo_handshake(PySSLSocket *self) { int ret; int err; @@ -989,7 +869,7 @@ PySSL_test_decode_certificate (PyObject static PyObject * -PySSL_peercert(PySSLObject *self, PyObject *args) +PySSL_peercert(PySSLSocket *self, PyObject *args) { PyObject *retval = NULL; int len; @@ -1020,8 +900,7 @@ PySSL_peercert(PySSLObject *self, PyObje return retval; } else { - - verification = SSL_CTX_get_verify_mode(self->ctx); + verification = SSL_CTX_get_verify_mode(SSL_get_SSL_CTX(self->ssl)); if ((verification & SSL_VERIFY_PEER) == 0) return PyDict_New(); else @@ -1041,7 +920,7 @@ If the optional argument is True, return peer certificate, or None if no certificate was provided. This will\n\ return the certificate even if it wasn't validated."); -static PyObject *PySSL_cipher (PySSLObject *self) { +static PyObject *PySSL_cipher (PySSLSocket *self) { PyObject *retval, *v; SSL_CIPHER *current; @@ -1087,14 +966,12 @@ static PyObject *PySSL_cipher (PySSLObje return NULL; } -static void PySSL_dealloc(PySSLObject *self) +static void PySSL_dealloc(PySSLSocket *self) { if (self->peer_cert) /* Possible not to have one? */ X509_free (self->peer_cert); if (self->ssl) SSL_free(self->ssl); - if (self->ctx) - SSL_CTX_free(self->ctx); Py_XDECREF(self->Socket); PyObject_Del(self); } @@ -1169,7 +1046,7 @@ normal_return: return rc == 0 ? SOCKET_HAS_TIMED_OUT : SOCKET_OPERATION_OK; } -static PyObject *PySSL_SSLwrite(PySSLObject *self, PyObject *args) +static PyObject *PySSL_SSLwrite(PySSLSocket *self, PyObject *args) { Py_buffer buf; int len; @@ -1255,7 +1132,7 @@ PyDoc_STRVAR(PySSL_SSLwrite_doc, Writes the string s into the SSL object. Returns the number\n\ of bytes written."); -static PyObject *PySSL_SSLpending(PySSLObject *self) +static PyObject *PySSL_SSLpending(PySSLSocket *self) { int count = 0; @@ -1274,7 +1151,7 @@ PyDoc_STRVAR(PySSL_SSLpending_doc, Returns the number of already decrypted bytes available for read,\n\ pending on the connection.\n"); -static PyObject *PySSL_SSLread(PySSLObject *self, PyObject *args) +static PyObject *PySSL_SSLread(PySSLSocket *self, PyObject *args) { PyObject *dest = NULL; Py_buffer buf; @@ -1399,7 +1276,7 @@ PyDoc_STRVAR(PySSL_SSLread_doc, \n\ Read up to len bytes from the SSL socket."); -static PyObject *PySSL_SSLshutdown(PySSLObject *self) +static PyObject *PySSL_SSLshutdown(PySSLSocket *self) { int err, ssl_err, sockstate, nonblocking; int zeros = 0; @@ -1504,10 +1381,10 @@ static PyMethodDef PySSLMethods[] = { {NULL, NULL} }; -static PyTypeObject PySSL_Type = { +static PyTypeObject PySSLSocket_Type = { PyVarObject_HEAD_INIT(NULL, 0) - "ssl.SSLContext", /*tp_name*/ - sizeof(PySSLObject), /*tp_basicsize*/ + "_ssl._SSLSocket", /*tp_name*/ + sizeof(PySSLSocket), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)PySSL_dealloc, /*tp_dealloc*/ @@ -1536,6 +1413,306 @@ static PyTypeObject PySSL_Type = { PySSLMethods, /*tp_methods*/ }; + +/* + * _SSLContext objects + */ + +static PyObject * +context_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + char *kwlist[] = {"protocol", NULL}; + PySSLContext *self; + int proto_version = PY_SSL_VERSION_SSL23; + SSL_CTX *ctx = NULL; + + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "i:_SSLContext", kwlist, + &proto_version)) + return NULL; + + PySSL_BEGIN_ALLOW_THREADS + if (proto_version == PY_SSL_VERSION_TLS1) + ctx = SSL_CTX_new(TLSv1_method()); + else if (proto_version == PY_SSL_VERSION_SSL3) + ctx = SSL_CTX_new(SSLv3_method()); + else if (proto_version == PY_SSL_VERSION_SSL2) + ctx = SSL_CTX_new(SSLv2_method()); + else if (proto_version == PY_SSL_VERSION_SSL23) + ctx = SSL_CTX_new(SSLv23_method()); + else + proto_version = -1; + PySSL_END_ALLOW_THREADS + + if (proto_version == -1) { + PyErr_SetString(PyExc_ValueError, + "invalid protocol version"); + return NULL; + } + if (ctx == NULL) { + PyErr_SetString(PySSLErrorObject, + "failed to allocate SSL context"); + return NULL; + } + + assert(type != NULL && type->tp_alloc != NULL); + self = (PySSLContext *) type->tp_alloc(type, 0); + if (self == NULL) { + SSL_CTX_free(ctx); + return NULL; + } + self->ctx = ctx; + /* Defaults */ + SSL_CTX_set_verify(self->ctx, SSL_VERIFY_NONE, NULL); + SSL_CTX_set_options(self->ctx, SSL_OP_ALL); + + return (PyObject *)self; +} + +static void +context_dealloc(PySSLContext *self) +{ + SSL_CTX_free(self->ctx); + Py_TYPE(self)->tp_free(self); +} + +static PyObject * +set_ciphers(PySSLContext *self, PyObject *args) +{ + int ret; + const char *cipherlist; + + if (!PyArg_ParseTuple(args, "s:set_ciphers", &cipherlist)) + return NULL; + ret = SSL_CTX_set_cipher_list(self->ctx, cipherlist); + if (ret == 0) { + PyErr_SetString(PySSLErrorObject, + "No cipher can be selected."); + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +get_verify_mode(PySSLContext *self, void *c) +{ + switch (SSL_CTX_get_verify_mode(self->ctx)) { + case SSL_VERIFY_NONE: + return PyLong_FromLong(PY_SSL_CERT_NONE); + case SSL_VERIFY_PEER: + return PyLong_FromLong(PY_SSL_CERT_OPTIONAL); + case SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT: + return PyLong_FromLong(PY_SSL_CERT_REQUIRED); + } + PyErr_SetString(PySSLErrorObject, + "invalid return value from SSL_CTX_get_verify_mode"); + return NULL; +} + +static int +set_verify_mode(PySSLContext *self, PyObject *arg, void *c) +{ + int n, mode; + if (!PyArg_Parse(arg, "i", &n)) + return -1; + if (n == PY_SSL_CERT_NONE) + mode = SSL_VERIFY_NONE; + else if (n == PY_SSL_CERT_OPTIONAL) + mode = SSL_VERIFY_PEER; + else if (n == PY_SSL_CERT_REQUIRED) + mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + else { + PyErr_SetString(PyExc_ValueError, + "invalid value for verify_mode"); + return -1; + } + SSL_CTX_set_verify(self->ctx, mode, NULL); + return 0; +} + +static PyObject * +load_cert_chain(PySSLContext *self, PyObject *args, PyObject *kwds) +{ + char *kwlist[] = {"certfile", "keyfile", NULL}; + PyObject *certfile, *keyfile = NULL; + PyObject *certfile_bytes = NULL, *keyfile_bytes = NULL; + int r; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O|O:load_cert_chain", kwlist, + &certfile, &keyfile)) + return NULL; + if (keyfile == Py_None) + keyfile = NULL; + if (!PyUnicode_FSConverter(certfile, &certfile_bytes)) { + PyErr_SetString(PyExc_TypeError, + "certfile should be a valid filesystem path"); + return NULL; + } + if (keyfile && !PyUnicode_FSConverter(keyfile, &keyfile_bytes)) { + PyErr_SetString(PyExc_TypeError, + "keyfile should be a valid filesystem path"); + goto error; + } + PySSL_BEGIN_ALLOW_THREADS + r = SSL_CTX_use_certificate_chain_file(self->ctx, + PyBytes_AS_STRING(certfile_bytes)); + PySSL_END_ALLOW_THREADS + if (r != 1) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + goto error; + } + PySSL_BEGIN_ALLOW_THREADS + r = SSL_CTX_use_RSAPrivateKey_file(self->ctx, + PyBytes_AS_STRING(keyfile ? keyfile_bytes : certfile_bytes), + SSL_FILETYPE_PEM); + PySSL_END_ALLOW_THREADS + Py_XDECREF(keyfile_bytes); + Py_XDECREF(certfile_bytes); + if (r != 1) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + return NULL; + } + PySSL_BEGIN_ALLOW_THREADS + r = SSL_CTX_check_private_key(self->ctx); + PySSL_END_ALLOW_THREADS + if (r != 1) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + return NULL; + } + Py_RETURN_NONE; + +error: + Py_XDECREF(keyfile_bytes); + Py_XDECREF(certfile_bytes); + return NULL; +} + +static PyObject * +load_verify_locations(PySSLContext *self, PyObject *args, PyObject *kwds) +{ + char *kwlist[] = {"cafile", "capath", NULL}; + PyObject *cafile = NULL, *capath = NULL; + PyObject *cafile_bytes = NULL, *capath_bytes = NULL; + const char *cafile_buf = NULL, *capath_buf = NULL; + int r; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "|OO:load_verify_locations", kwlist, + &cafile, &capath)) + return NULL; + if (cafile == Py_None) + cafile = NULL; + if (capath == Py_None) + capath = NULL; + if (cafile == NULL && capath == NULL) { + PyErr_SetString(PyExc_TypeError, + "cafile and capath cannot be both omitted"); + return NULL; + } + if (cafile && !PyUnicode_FSConverter(cafile, &cafile_bytes)) { + PyErr_SetString(PyExc_TypeError, + "cafile should be a valid filesystem path"); + return NULL; + } + if (capath && !PyUnicode_FSConverter(capath, &capath_bytes)) { + Py_DECREF(cafile_bytes); + PyErr_SetString(PyExc_TypeError, + "capath should be a valid filesystem path"); + return NULL; + } + if (cafile) + cafile_buf = PyBytes_AS_STRING(cafile_bytes); + if (capath) + capath_buf = PyBytes_AS_STRING(capath_bytes); + PySSL_BEGIN_ALLOW_THREADS + r = SSL_CTX_load_verify_locations(self->ctx, cafile_buf, capath_buf); + PySSL_END_ALLOW_THREADS + Py_XDECREF(cafile_bytes); + Py_XDECREF(capath_bytes); + if (r != 1) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +context_wrap_socket(PySSLContext *self, PyObject *args, PyObject *kwds) +{ + char *kwlist[] = {"sock", "server_side", NULL}; + PySocketSockObject *sock; + int server_side = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!i:_wrap_socket", kwlist, + PySocketModule.Sock_Type, + &sock, &server_side)) + return NULL; + + return (PyObject *) newPySSLSocket(self->ctx, sock, server_side); +} + +static PyGetSetDef context_getsetlist[] = { + {"verify_mode", (getter) get_verify_mode, + (setter) set_verify_mode, NULL}, + {NULL}, /* sentinel */ +}; + +static struct PyMethodDef context_methods[] = { + {"_wrap_socket", (PyCFunction) context_wrap_socket, + METH_VARARGS | METH_KEYWORDS, NULL}, + {"set_ciphers", (PyCFunction) set_ciphers, + METH_VARARGS, NULL}, + {"load_cert_chain", (PyCFunction) load_cert_chain, + METH_VARARGS | METH_KEYWORDS, NULL}, + {"load_verify_locations", (PyCFunction) load_verify_locations, + METH_VARARGS | METH_KEYWORDS, NULL}, + {NULL, NULL} /* sentinel */ +}; + +static PyTypeObject PySSLContext_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "_ssl._SSLContext", /*tp_name*/ + sizeof(PySSLContext), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)context_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_reserved*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + context_methods, /*tp_methods*/ + 0, /*tp_members*/ + context_getsetlist, /*tp_getset*/ + 0, /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc*/ + context_new, /*tp_new*/ +}; + + + #ifdef HAVE_OPENSSL_RAND /* helper routines for seeding the SSL PRNG */ @@ -1605,8 +1782,6 @@ fails or if it does provide enough data /* List of functions exported by this module. */ static PyMethodDef PySSL_methods[] = { - {"sslwrap", PySSL_sslwrap, - METH_VARARGS, ssl_doc}, {"_test_decode_cert", PySSL_test_decode_certificate, METH_VARARGS}, #ifdef HAVE_OPENSSL_RAND @@ -1715,7 +1890,9 @@ PyInit__ssl(void) unsigned int major, minor, fix, patch, status; PySocketModule_APIObject *socket_api; - if (PyType_Ready(&PySSL_Type) < 0) + if (PyType_Ready(&PySSLContext_Type) < 0) + return NULL; + if (PyType_Ready(&PySSLSocket_Type) < 0) return NULL; m = PyModule_Create(&_sslmodule); @@ -1748,8 +1925,11 @@ PyInit__ssl(void) return NULL; if (PyDict_SetItemString(d, "SSLError", PySSLErrorObject) != 0) return NULL; - if (PyDict_SetItemString(d, "SSLType", - (PyObject *)&PySSL_Type) != 0) + if (PyDict_SetItemString(d, "_SSLContext", + (PyObject *)&PySSLContext_Type) != 0) + return NULL; + if (PyDict_SetItemString(d, "_SSLSocket", + (PyObject *)&PySSLSocket_Type) != 0) return NULL; PyModule_AddIntConstant(m, "SSL_ERROR_ZERO_RETURN", PY_SSL_ERROR_ZERO_RETURN);