From e2dbe7c38d02ee54290eb1b6d8adfabebbbcd738 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Sun, 4 Sep 2016 16:35:50 +0200 Subject: [PATCH] SSLSession support --- Doc/library/ssl.rst | 36 +++++++++++-- Lib/ssl.py | 63 +++++++++++++++++----- Lib/test/test_ssl.py | 43 ++++++++++++++- Modules/_ssl.c | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 273 insertions(+), 19 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 5792d0d407ba224bc4b42c78af613630f341ff51..05ce4f45834d7c3aee4a7a02028f719c97a1faf2 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -129,6 +129,11 @@ Functions, Constants, and Exceptions hostname). Certificate errors detected by OpenSSL, though, raise an :exc:`SSLError`. +.. class:: SSLSession + + Session object used by :attr:`~SSLSocket.session`. + + .. versionadded:: 3.6 Socket creation ^^^^^^^^^^^^^^^ @@ -1070,6 +1075,19 @@ SSL sockets also have the following additional methods and attributes: .. versionadded:: 3.2 +.. attribute:: SSLSocket.session + + The :class:`SSLSession` for this SSL connection. The session is available + for client and server side sockets after the TLS handshake has been + performed. For client sockets the session can be set before + :method:`~SSLSocket.do_handshake` has been called to reuse a session. + + .. versionadded:: 3.6 + +.. attribute:: SSLSocket.session_reused + + .. versionadded:: 3.6 + SSL Contexts ------------ @@ -1333,7 +1351,7 @@ to speed up repeated connections from the same clients. .. method:: SSLContext.wrap_socket(sock, server_side=False, \ do_handshake_on_connect=True, suppress_ragged_eofs=True, \ - server_hostname=None) + server_hostname=None, session=None) Wrap an existing Python socket *sock* and return an :class:`SSLSocket` object. *sock* must be a :data:`~socket.SOCK_STREAM` socket; other socket @@ -1350,19 +1368,27 @@ to speed up repeated connections from the same clients. quite similarly to HTTP virtual hosts. Specifying *server_hostname* will raise a :exc:`ValueError` if *server_side* is true. + *session*, see :attr:`~SSLSocket.session` + .. versionchanged:: 3.5 Always allow a server_hostname to be passed, even if OpenSSL does not have SNI. + .. versionchanged:: 3.6 + *session* argument was added. + .. method:: SSLContext.wrap_bio(incoming, outgoing, server_side=False, \ - server_hostname=None) + server_hostname=None, session=None) Create a new :class:`SSLObject` instance by wrapping the BIO objects *incoming* and *outgoing*. The SSL routines will read input data from the incoming BIO and write data to the outgoing BIO. - The *server_side* and *server_hostname* parameters have the same meaning as - in :meth:`SSLContext.wrap_socket`. + The *server_side*, *server_hostname* and *session* parameters have the + same meaning as in :meth:`SSLContext.wrap_socket`. + + .. versionchanged:: 3.6 + *session* argument was added. .. method:: SSLContext.session_stats() @@ -1852,6 +1878,8 @@ provided. - :attr:`~SSLSocket.context` - :attr:`~SSLSocket.server_side` - :attr:`~SSLSocket.server_hostname` + - :attr:`~SSLSocket.session` + - :attr:`~SSLSocket.session_reused` - :meth:`~SSLSocket.read` - :meth:`~SSLSocket.write` - :meth:`~SSLSocket.getpeercert` diff --git a/Lib/ssl.py b/Lib/ssl.py index 560cfef77661a0300447f0b102eb060585ab5861..91ce750a341da009056a56d0c4e1f686e847eccb 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -98,7 +98,7 @@ from enum import Enum as _Enum, IntEnum as _IntEnum 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 _SSLContext, MemoryBIO +from _ssl import _SSLContext, MemoryBIO, SSLSession from _ssl import ( SSLError, SSLZeroReturnError, SSLWantReadError, SSLWantWriteError, SSLSyscallError, SSLEOFError, @@ -369,18 +369,18 @@ class SSLContext(_SSLContext): def wrap_socket(self, sock, server_side=False, do_handshake_on_connect=True, suppress_ragged_eofs=True, - server_hostname=None): + server_hostname=None, session=None): return SSLSocket(sock=sock, server_side=server_side, do_handshake_on_connect=do_handshake_on_connect, suppress_ragged_eofs=suppress_ragged_eofs, server_hostname=server_hostname, - _context=self) + _context=self, _session=session) def wrap_bio(self, incoming, outgoing, server_side=False, - server_hostname=None): + server_hostname=None, session=None): sslobj = self._wrap_bio(incoming, outgoing, server_side=server_side, server_hostname=server_hostname) - return SSLObject(sslobj) + return SSLObject(sslobj, session=session) def set_npn_protocols(self, npn_protocols): protos = bytearray() @@ -540,10 +540,12 @@ class SSLObject: * The ``do_handshake_on_connect`` and ``suppress_ragged_eofs`` machinery. """ - def __init__(self, sslobj, owner=None): + def __init__(self, sslobj, owner=None, session=None): self._sslobj = sslobj # Note: _sslobj takes a weak reference to owner self._sslobj.owner = owner or self + if session is not None: + self._sslobj.session = session @property def context(self): @@ -555,6 +557,20 @@ class SSLObject: self._sslobj.context = ctx @property + def session(self): + """The SSLSession for client socket.""" + return self._sslobj.session + + @session.setter + def session(self, session): + self._sslobj.session = session + + @property + def session_reused(self): + """Was the client session reused during handshake""" + return self._sslobj.session_reused + + @property def server_side(self): """Whether this is a server-side socket.""" return self._sslobj.server_side @@ -671,7 +687,7 @@ class SSLSocket(socket): family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None, suppress_ragged_eofs=True, npn_protocols=None, ciphers=None, server_hostname=None, - _context=None): + _context=None, _session=None): if _context: self._context = _context @@ -703,11 +719,16 @@ class SSLSocket(socket): # mixed in. if sock.getsockopt(SOL_SOCKET, SO_TYPE) != SOCK_STREAM: raise NotImplementedError("only stream sockets are supported") - if server_side and server_hostname: - raise ValueError("server_hostname can only be specified " - "in client mode") + if server_side: + if server_hostname: + raise ValueError("server_hostname can only be specified " + "in client mode") + if _session is not None: + raise ValueError("session can only be specified in " + "client mode") if self._context.check_hostname and not server_hostname: raise ValueError("check_hostname requires server_hostname") + self._session = _session self.server_side = server_side self.server_hostname = server_hostname self.do_handshake_on_connect = do_handshake_on_connect @@ -743,7 +764,8 @@ class SSLSocket(socket): try: sslobj = self._context._wrap_socket(self, server_side, server_hostname) - self._sslobj = SSLObject(sslobj, owner=self) + self._sslobj = SSLObject(sslobj, owner=self, + session=self._session) if do_handshake_on_connect: timeout = self.gettimeout() if timeout == 0.0: @@ -764,6 +786,22 @@ class SSLSocket(socket): self._context = ctx self._sslobj.context = ctx + @property + def session(self): + """The SSLSession for client socket.""" + return self._sslobj.session + + @session.setter + def session(self, session): + self._session = session + self._sslobj.session = session + + @property + def session_reused(self): + """Was the client session reused during handshake""" + return self._sslobj.session_reused + + def dup(self): raise NotImplemented("Can't dup() %s instances" % self.__class__.__name__) @@ -996,7 +1034,8 @@ class SSLSocket(socket): if self._connected: raise ValueError("attempt to connect already-connected SSLSocket!") sslobj = self.context._wrap_socket(self, False, self.server_hostname) - self._sslobj = SSLObject(sslobj, owner=self) + self._sslobj = SSLObject(sslobj, owner=self, + session=self._session) try: if connect_ex: rc = socket.connect_ex(self, addr) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 6cd5454ab4d06fdfff0b295b35b90938476aeec9..ecc7d01a9bcacf8fda5b06bd13ae93bef1e541bb 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -2108,7 +2108,8 @@ if _have_threads: self.server.close() def server_params_test(client_context, server_context, indata=b"FOO\n", - chatty=True, connectionchatty=False, sni_name=None): + chatty=True, connectionchatty=False, sni_name=None, + session=None): """ Launch a server, connect a client to it and try various reads and writes. @@ -2119,7 +2120,7 @@ if _have_threads: connectionchatty=False) with server: with client_context.wrap_socket(socket.socket(), - server_hostname=sni_name) as s: + server_hostname=sni_name, session=session) as s: s.connect((HOST, server.port)) for arg in [indata, bytearray(indata), memoryview(indata)]: if connectionchatty: @@ -2147,6 +2148,8 @@ if _have_threads: 'client_alpn_protocol': s.selected_alpn_protocol(), 'client_npn_protocol': s.selected_npn_protocol(), 'version': s.version(), + 'session_reused': s.session_reused, + 'session': s.session, }) s.close() stats['server_alpn_protocols'] = server.selected_alpn_protocols @@ -3335,6 +3338,42 @@ if _have_threads: s.sendfile(file) self.assertEqual(s.recv(1024), TEST_DATA) + def test_session(self): + server_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + server_context.load_cert_chain(SIGNED_CERTFILE) + client_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + client_context.verify_mode = ssl.CERT_REQUIRED + client_context.load_verify_locations(SIGNING_CA) + + # first conncetion without session + stats = server_params_test(client_context, server_context) + session = stats['session'] + self.assertFalse(stats['session_reused']) + sess_stat = server_context.session_stats() + self.assertEqual(sess_stat['accept'], 1) + self.assertEqual(sess_stat['hits'], 0) + + # reuse session + stats = server_params_test(client_context, server_context, session=session) + self.assertTrue(stats['session_reused']) + sess_stat = server_context.session_stats() + self.assertEqual(sess_stat['accept'], 2) + self.assertEqual(sess_stat['hits'], 1) + + # another one without session + stats = server_params_test(client_context, server_context) + self.assertFalse(stats['session_reused']) + sess_stat = server_context.session_stats() + self.assertEqual(sess_stat['accept'], 3) + self.assertEqual(sess_stat['hits'], 1) + + # reuse session again + stats = server_params_test(client_context, server_context, session=session) + self.assertTrue(stats['session_reused']) + sess_stat = server_context.session_stats() + self.assertEqual(sess_stat['accept'], 4) + self.assertEqual(sess_stat['hits'], 2) + def test_main(verbose=False): if support.verbose: diff --git a/Modules/_ssl.c b/Modules/_ssl.c index e2c43869b7d2ec1503ae3f7dd944f40bba88966b..862a687b86927f459251f9afa0bf681526a343d0 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -220,25 +220,36 @@ typedef struct { int eof_written; } PySSLMemoryBIO; +typedef struct { + PyObject_HEAD + SSL_SESSION *session; + PySSLContext *ctx; +} PySSLSession; + static PyTypeObject PySSLContext_Type; static PyTypeObject PySSLSocket_Type; static PyTypeObject PySSLMemoryBIO_Type; +static PyTypeObject PySSLSession_Type; /*[clinic input] module _ssl class _ssl._SSLContext "PySSLContext *" "&PySSLContext_Type" class _ssl._SSLSocket "PySSLSocket *" "&PySSLSocket_Type" class _ssl.MemoryBIO "PySSLMemoryBIO *" "&PySSLMemoryBIO_Type" +class _ssl.SSLSession "PySSLSession *" "&PySSLSession_Type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=7bf7cb832638e2e1]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=bdc67fafeeaa8109]*/ #include "clinic/_ssl.c.h" static int PySSL_select(PySocketSockObject *s, int writing, _PyTime_t timeout); +static PySSLSession *newPySSLSession(PySSLSocket *sock); + #define PySSLContext_Check(v) (Py_TYPE(v) == &PySSLContext_Type) #define PySSLSocket_Check(v) (Py_TYPE(v) == &PySSLSocket_Type) #define PySSLMemoryBIO_Check(v) (Py_TYPE(v) == &PySSLMemoryBIO_Type) +#define PySSLSession_Check(v) (Py_TYPE(v) == &PySSLSession_Type) typedef enum { SOCKET_IS_NONBLOCKING, @@ -2152,6 +2163,68 @@ _ssl__SSLSocket_tls_unique_cb_impl(PySSLSocket *self) return retval; } +static PySSLSession *PySSL_get_session(PySSLSocket *self, void *closure) { + /* get_session does not check for handshake done or client socket but + fails if no session is available. */ + return newPySSLSession(self); +} + +static int PySSL_set_session(PySSLSocket *self, PyObject *value, + void *closure) { + PySSLSession *sess; + int result; + + if (!PySSLSession_Check(value)) { + PyErr_SetString(PyExc_TypeError, "The value must be a SSLSession"); + return -1; + } + sess = (PySSLSession *)value; + + if (self->ctx->ctx != sess->ctx->ctx) { + PyErr_SetString(PyExc_ValueError, + "Socket uses a different SSLContext."); + return -1; + } + if (self->socket_type != PY_SSL_CLIENT) { + PyErr_SetString(PyExc_ValueError, + "set_session is only supported for client connections."); + return -1; + } + if (self->handshake_done) { + PyErr_SetString(PyExc_ValueError, + "handshake done"); + return -1; + } + /* should not happen */ + if (sess->session == NULL) { + PyErr_SetString(PyExc_ValueError, "Invalid session"); + return -1; + } + result = SSL_set_session(self->ssl, sess->session); + if (result == 0) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + return -1; + } + return 0; +} + +PyDoc_STRVAR(PySSL_set_session_doc, +"_setter_session(session)\n\ +\ +Get / set client session"); + +static PyObject * +PySSL_get_session_reused(PySSLSocket *self, void *closure) { + if (SSL_session_reused(self->ssl)) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + +PyDoc_STRVAR(PySSL_get_session_reused_doc, +"Was the client session reused during handshake?"); + static PyGetSetDef ssl_getsetlist[] = { {"context", (getter) PySSL_get_context, (setter) PySSL_set_context, PySSL_set_context_doc}, @@ -2161,6 +2234,10 @@ static PyGetSetDef ssl_getsetlist[] = { PySSL_get_server_hostname_doc}, {"owner", (getter) PySSL_get_owner, (setter) PySSL_set_owner, PySSL_get_owner_doc}, + {"session", (getter) PySSL_get_session, + (setter) PySSL_set_session, PySSL_set_session_doc}, + {"session_reused", (getter) PySSL_get_session_reused, NULL, + PySSL_get_session_reused_doc}, {NULL}, /* sentinel */ }; @@ -3828,6 +3905,70 @@ static PyTypeObject PySSLMemoryBIO_Type = { }; +/* + * SSL Session object + */ + +static PySSLSession * +newPySSLSession(PySSLSocket *sock) +{ + PySSLSession *self; + SSL_SESSION *session; + + session = SSL_get1_session(sock->ssl); + if (session == NULL) { + PyErr_SetString(PyExc_ValueError, + "No session available."); + return NULL; + } + + self = PyObject_New(PySSLSession, &PySSLSession_Type); + if (self == NULL) { + SSL_SESSION_free(session); + return NULL; + } + + assert(sock->ctx); + self->ctx = sock->ctx; + Py_INCREF(self->ctx); + self->session = session; + return self; +} + +static void PySSLSession_dealloc(PySSLSession *self) +{ + Py_XDECREF(self->ctx); + if (self->session != NULL) { + SSL_SESSION_free(self->session); + } + PyObject_Del(self); +} + +static PyTypeObject PySSLSession_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "_ssl.Session", /*tp_name*/ + sizeof(PySSLSession), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)PySSLSession_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, /*tp_flags*/ +}; + + + /* helper routines for seeding the SSL PRNG */ /*[clinic input] _ssl.RAND_add @@ -4511,6 +4652,9 @@ PyInit__ssl(void) return NULL; if (PyType_Ready(&PySSLMemoryBIO_Type) < 0) return NULL; + if (PyType_Ready(&PySSLSession_Type) < 0) + return NULL; + m = PyModule_Create(&_sslmodule); if (m == NULL) @@ -4577,6 +4721,10 @@ PyInit__ssl(void) if (PyDict_SetItemString(d, "MemoryBIO", (PyObject *)&PySSLMemoryBIO_Type) != 0) return NULL; + if (PyDict_SetItemString(d, "SSLSession", + (PyObject *)&PySSLSession_Type) != 0) + return NULL; + PyModule_AddIntConstant(m, "SSL_ERROR_ZERO_RETURN", PY_SSL_ERROR_ZERO_RETURN); PyModule_AddIntConstant(m, "SSL_ERROR_WANT_READ", -- 2.7.4