From 51a8a5c9f400bb3b5f690b8b45171c58f0776076 Mon Sep 17 00:00:00 2001 From: Christer Weinigel Date: Sun, 21 Jul 2019 17:14:00 +0200 Subject: [PATCH] bpo-37952: SSL: add support for export_keying_material Add support for the RFC5705 SSL_export_keying_material function to the Python SSL module. --- Doc/library/ssl.rst | 39 ++++++++++++ Lib/ssl.py | 26 ++++++++ Lib/test/test_ssl.py | 59 +++++++++++++++++++ .../2021-04-15-08-55-22.bpo-37952.A8pKuh.rst | 1 + Modules/_ssl.c | 48 +++++++++++++++ Modules/clinic/_ssl.c.h | 45 +++++++++++++- 6 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2021-04-15-08-55-22.bpo-37952.A8pKuh.rst diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index eb33d7e1778a7f..83ea2c7442413d 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1330,6 +1330,39 @@ SSL sockets also have the following additional methods and attributes: .. versionadded:: 3.3 +.. method:: SSLSocket.export_keying_material(label, material_len, context=None) + + Returns a bytes object with keying material as defined by + :rfc:`5705` and :rfc:`8446` or None if no keying material is + available, i.e. before the SSL handshake or after the SSL + connection is closed. + + The appliction specific *label* should contain a value from the the + IANA Exporter Label Registry + (https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#exporter-labels). + Labels beginning with "EXPERIMENTAL" can be used without + registration. *material_len* specifies how many bytes of keying + material should be returned. The optional appliction specific + *context* can be used to get multiple distinct keying materials + from the same application specific label; None means that no + context will be used. + + :exc:`ValueError` will be raised if an unsupported channel binding + type is requested. + + In TLSv1.2 passing no context (None) will return different keying + material than a zero length context. In TLSv1.3 not context will + return the same keying material as a zero length context. + + If label or context is a string only ASCII is allowed. Convert the + string to a bytes object using an explicit encoding if you want to + use non-ASCII data. + + This method is a wrapper around the SSL_export_keying_material + function; refer to the OpenSSLfor documentation for more details. + + .. versionadded:: 3.11 + .. method:: SSLSocket.get_channel_binding(cb_type="tls-unique") Get channel binding data for current connection, as a bytes object. Returns @@ -2766,6 +2799,9 @@ of TLS/SSL. Some new TLS 1.3 features are not yet available. :rfc:`RFC 5246: The Transport Layer Security (TLS) Protocol Version 1.2 <5246>` T. Dierks et. al. + :rfc:`RFC 5705: Keying Material Exporters for Transport Layer Security (TLS) <5705>` + IETF, E. Rescorla + :rfc:`RFC 6066: Transport Layer Security (TLS) Extensions <6066>` D. Eastlake @@ -2775,5 +2811,8 @@ of TLS/SSL. Some new TLS 1.3 features are not yet available. :rfc:`RFC 7525: Recommendations for Secure Use of Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS) <7525>` IETF + :rfc:`RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3 <8446>` + IETF, E. Rescorla + `Mozilla's Server Side TLS recommendations `_ Mozilla diff --git a/Lib/ssl.py b/Lib/ssl.py index 207925166efa35..88df8dfd8d57a1 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -1332,6 +1332,32 @@ def verify_client_post_handshake(self): else: raise ValueError("No SSL wrapper around " + str(self)) + def export_keying_material(self, label, material_len, context=None): + """Export keying material from current connection. Return a bytes + object with keying material or None if no keying material is + available, i.e. before the SSL handshake or after the SSL + connection is closed + + The application specific `label` should be a bytes object or + ASCII string. `material_len` specifies how many byts of + keying material to return. The optional application specific + "context" should be a bytes object, ASCII string or None for + no conxtext. + + """ + + if isinstance(label, str): + label = label.encode('ASCII') + + if isinstance(context, str): + context = context.encode('ASCII') + + self._checkClosed() + self._check_connected() + if self._sslobj is None: + return None + return self._sslobj.export_keying_material(label, material_len, context) + def _real_close(self): self._sslobj = None super()._real_close() diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index f99a3e8da95f88..7e011b8753be95 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -4436,6 +4436,65 @@ def test_session_handling(self): self.assertEqual(str(e.exception), 'Session refers to a different SSLContext.') + def export_keying_material_test(self, tls_version): + client_context, server_context, hostname = testing_context() + client_context.minimum_version = tls_version + client_context.maximum_version = tls_version + server_context.minimum_version = tls_version + server_context.maximum_version = tls_version + + with ThreadedEchoServer(context=server_context, + chatty=False) as server: + with client_context.wrap_socket(socket.socket(), + do_handshake_on_connect=False, + server_hostname=hostname) as s: + # can not be used before the connection is open + with self.assertRaises(OSError) as cm: + s.export_keying_material('foo', 1) + self.assertEqual(cm.exception.errno, errno.ENOTCONN) + s.connect((HOST, server.port)) + # should return None before the handshake is finished + t = s.export_keying_material('foo', 1) + self.assertEqual(t, None) + s.do_handshake() + # material_len must be positive + with self.assertRaises(ValueError) as cm: + s.export_keying_material('foo', 0) + with self.assertRaises(ValueError) as cm: + s.export_keying_material('foo', -1) + with self.assertRaises(ValueError) as cm: + s.export_keying_material('foo', -13) + # Strings containing non-ASCII labels are not allowed + with self.assertRaises(UnicodeEncodeError) as cm: + s.export_keying_material('\u0394', 1) + with self.assertRaises(UnicodeEncodeError) as cm: + s.export_keying_material('foo', 1, '\u0394') + for args in [ + ( 'foo', 32 ), + ( 'foo', 32, None ), + ( 'foo', 32, '' ), + ( 'foo', 32, 'bar' ), + ( b'foo', 32, b'bar' ), + ( b'foo', 32, b'bar' ), + ( b'foo', 1, b'bar' ), + ( b'foo', 128, b'bar' ), + ( b'\x00\x01\0x2\x03\x80\xa1\xc2\xe3', 128, + b'\x80\xa1\xc2\xe3\x00\x01\0x2\x03' ), + ]: + t = s.export_keying_material(*args) + self.assertEqual(len(t), args[1]) + s.close() + # should return None after the socket has been closed + t = s.export_keying_material('foo', 1) + self.assertEqual(t, None) + + @requires_tls_version('TLSv1_2') + def test_export_keying_material_tlsv1_2(self): + self.export_keying_material_test(ssl.TLSVersion.TLSv1_2) + + @requires_tls_version('TLSv1_3') + def test_export_keying_material_tlsv1_3(self): + self.export_keying_material_test(ssl.TLSVersion.TLSv1_3) @unittest.skipUnless(has_tls_version('TLSv1_3'), "Test needs TLS 1.3") class TestPostHandshakeAuth(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2021-04-15-08-55-22.bpo-37952.A8pKuh.rst b/Misc/NEWS.d/next/Library/2021-04-15-08-55-22.bpo-37952.A8pKuh.rst new file mode 100644 index 00000000000000..cd5f6b51082e10 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-04-15-08-55-22.bpo-37952.A8pKuh.rst @@ -0,0 +1 @@ +Added support for the RFC5705 :func:`ssl.SSLSocket.export_keying_material` diff --git a/Modules/_ssl.c b/Modules/_ssl.c index b2e241a0a338eb..39e9855051e356 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2749,6 +2749,53 @@ _ssl__SSLSocket_verify_client_post_handshake_impl(PySSLSocket *self) #endif } +/*[clinic input] +_ssl._SSLSocket.export_keying_material + label: Py_buffer(accept={buffer, str}) + material_len: int + context: Py_buffer(accept={buffer, str, NoneType}) +[clinic start generated code]*/ + +static PyObject * +_ssl__SSLSocket_export_keying_material_impl(PySSLSocket *self, + Py_buffer *label, + int material_len, + Py_buffer *context) +/*[clinic end generated code: output=17b975255ccb2984 input=57daff6c33809e2e]*/ +{ + PyObject *out = NULL; + unsigned char *material; + + if (material_len < 1) { + PyErr_SetString(PyExc_ValueError, "material_len must be positive"); + return NULL; + } + + if (!SSL_is_init_finished(self->ssl)) { + /* handshake not finished */ + Py_RETURN_NONE; + } + + out = PyBytes_FromStringAndSize(NULL, material_len); + if (out == NULL) + goto error; + + material = (unsigned char *)PyBytes_AS_STRING(out); + + if (!SSL_export_keying_material(self->ssl, + material, material_len, + label->buf, label->len, + context->buf, context->len, + context->buf != NULL)) { + Py_CLEAR(out); + _setSSLError(get_state_ctx(self), NULL, 0, __FILE__, __LINE__); + goto error; + } + +error: + return out; +} + static SSL_SESSION* _ssl_session_dup(SSL_SESSION *session) { SSL_SESSION *newsession = NULL; @@ -2915,6 +2962,7 @@ static PyMethodDef PySSLMethods[] = { _SSL__SSLSOCKET_VERIFY_CLIENT_POST_HANDSHAKE_METHODDEF _SSL__SSLSOCKET_GET_UNVERIFIED_CHAIN_METHODDEF _SSL__SSLSOCKET_GET_VERIFIED_CHAIN_METHODDEF + _SSL__SSLSOCKET_EXPORT_KEYING_MATERIAL_METHODDEF {NULL, NULL} }; diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index 67eaf3f9609fab..6d140849af0be8 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -399,6 +399,49 @@ _ssl__SSLSocket_verify_client_post_handshake(PySSLSocket *self, PyObject *Py_UNU return _ssl__SSLSocket_verify_client_post_handshake_impl(self); } +PyDoc_STRVAR(_ssl__SSLSocket_export_keying_material__doc__, +"export_keying_material($self, /, label, material_len, context)\n" +"--\n" +"\n"); + +#define _SSL__SSLSOCKET_EXPORT_KEYING_MATERIAL_METHODDEF \ + {"export_keying_material", (PyCFunction)(void(*)(void))_ssl__SSLSocket_export_keying_material, METH_FASTCALL|METH_KEYWORDS, _ssl__SSLSocket_export_keying_material__doc__}, + +static PyObject * +_ssl__SSLSocket_export_keying_material_impl(PySSLSocket *self, + Py_buffer *label, + int material_len, + Py_buffer *context); + +static PyObject * +_ssl__SSLSocket_export_keying_material(PySSLSocket *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + static const char * const _keywords[] = {"label", "material_len", "context", NULL}; + static _PyArg_Parser _parser = {"s*iz*:export_keying_material", _keywords, 0}; + Py_buffer label = {NULL, NULL}; + int material_len; + Py_buffer context = {NULL, NULL}; + + if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser, + &label, &material_len, &context)) { + goto exit; + } + return_value = _ssl__SSLSocket_export_keying_material_impl(self, &label, material_len, &context); + +exit: + /* Cleanup for label */ + if (label.obj) { + PyBuffer_Release(&label); + } + /* Cleanup for context */ + if (context.obj) { + PyBuffer_Release(&context); + } + + return return_value; +} + static PyObject * _ssl__SSLContext_impl(PyTypeObject *type, int proto_version); @@ -1361,4 +1404,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=cd2a53c26eda295e input=a9049054013a1b77]*/ +/*[clinic end generated code: output=6c0899733dfc6649 input=a9049054013a1b77]*/