Skip to content

bpo-37952: SSL: add support for export_keying_material #25255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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 <https://wiki.mozilla.org/Security/Server_Side_TLS>`_
Mozilla
26 changes: 26 additions & 0 deletions Lib/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
59 changes: 59 additions & 0 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for the RFC5705 :func:`ssl.SSLSocket.export_keying_material`
48 changes: 48 additions & 0 deletions Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}
};

Expand Down
45 changes: 44 additions & 1 deletion Modules/clinic/_ssl.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.