Skip to content

Commit 51a8a5c

Browse files
committed
bpo-37952: SSL: add support for export_keying_material
Add support for the RFC5705 SSL_export_keying_material function to the Python SSL module.
1 parent e13cdca commit 51a8a5c

File tree

6 files changed

+217
-1
lines changed

6 files changed

+217
-1
lines changed

Doc/library/ssl.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,6 +1330,39 @@ SSL sockets also have the following additional methods and attributes:
13301330

13311331
.. versionadded:: 3.3
13321332

1333+
.. method:: SSLSocket.export_keying_material(label, material_len, context=None)
1334+
1335+
Returns a bytes object with keying material as defined by
1336+
:rfc:`5705` and :rfc:`8446` or None if no keying material is
1337+
available, i.e. before the SSL handshake or after the SSL
1338+
connection is closed.
1339+
1340+
The appliction specific *label* should contain a value from the the
1341+
IANA Exporter Label Registry
1342+
(https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#exporter-labels).
1343+
Labels beginning with "EXPERIMENTAL" can be used without
1344+
registration. *material_len* specifies how many bytes of keying
1345+
material should be returned. The optional appliction specific
1346+
*context* can be used to get multiple distinct keying materials
1347+
from the same application specific label; None means that no
1348+
context will be used.
1349+
1350+
:exc:`ValueError` will be raised if an unsupported channel binding
1351+
type is requested.
1352+
1353+
In TLSv1.2 passing no context (None) will return different keying
1354+
material than a zero length context. In TLSv1.3 not context will
1355+
return the same keying material as a zero length context.
1356+
1357+
If label or context is a string only ASCII is allowed. Convert the
1358+
string to a bytes object using an explicit encoding if you want to
1359+
use non-ASCII data.
1360+
1361+
This method is a wrapper around the SSL_export_keying_material
1362+
function; refer to the OpenSSLfor documentation for more details.
1363+
1364+
.. versionadded:: 3.11
1365+
13331366
.. method:: SSLSocket.get_channel_binding(cb_type="tls-unique")
13341367

13351368
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.
27662799
:rfc:`RFC 5246: The Transport Layer Security (TLS) Protocol Version 1.2 <5246>`
27672800
T. Dierks et. al.
27682801

2802+
:rfc:`RFC 5705: Keying Material Exporters for Transport Layer Security (TLS) <5705>`
2803+
IETF, E. Rescorla
2804+
27692805
:rfc:`RFC 6066: Transport Layer Security (TLS) Extensions <6066>`
27702806
D. Eastlake
27712807

@@ -2775,5 +2811,8 @@ of TLS/SSL. Some new TLS 1.3 features are not yet available.
27752811
:rfc:`RFC 7525: Recommendations for Secure Use of Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS) <7525>`
27762812
IETF
27772813

2814+
:rfc:`RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3 <8446>`
2815+
IETF, E. Rescorla
2816+
27782817
`Mozilla's Server Side TLS recommendations <https://wiki.mozilla.org/Security/Server_Side_TLS>`_
27792818
Mozilla

Lib/ssl.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,32 @@ def verify_client_post_handshake(self):
13321332
else:
13331333
raise ValueError("No SSL wrapper around " + str(self))
13341334

1335+
def export_keying_material(self, label, material_len, context=None):
1336+
"""Export keying material from current connection. Return a bytes
1337+
object with keying material or None if no keying material is
1338+
available, i.e. before the SSL handshake or after the SSL
1339+
connection is closed
1340+
1341+
The application specific `label` should be a bytes object or
1342+
ASCII string. `material_len` specifies how many byts of
1343+
keying material to return. The optional application specific
1344+
"context" should be a bytes object, ASCII string or None for
1345+
no conxtext.
1346+
1347+
"""
1348+
1349+
if isinstance(label, str):
1350+
label = label.encode('ASCII')
1351+
1352+
if isinstance(context, str):
1353+
context = context.encode('ASCII')
1354+
1355+
self._checkClosed()
1356+
self._check_connected()
1357+
if self._sslobj is None:
1358+
return None
1359+
return self._sslobj.export_keying_material(label, material_len, context)
1360+
13351361
def _real_close(self):
13361362
self._sslobj = None
13371363
super()._real_close()

Lib/test/test_ssl.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4436,6 +4436,65 @@ def test_session_handling(self):
44364436
self.assertEqual(str(e.exception),
44374437
'Session refers to a different SSLContext.')
44384438

4439+
def export_keying_material_test(self, tls_version):
4440+
client_context, server_context, hostname = testing_context()
4441+
client_context.minimum_version = tls_version
4442+
client_context.maximum_version = tls_version
4443+
server_context.minimum_version = tls_version
4444+
server_context.maximum_version = tls_version
4445+
4446+
with ThreadedEchoServer(context=server_context,
4447+
chatty=False) as server:
4448+
with client_context.wrap_socket(socket.socket(),
4449+
do_handshake_on_connect=False,
4450+
server_hostname=hostname) as s:
4451+
# can not be used before the connection is open
4452+
with self.assertRaises(OSError) as cm:
4453+
s.export_keying_material('foo', 1)
4454+
self.assertEqual(cm.exception.errno, errno.ENOTCONN)
4455+
s.connect((HOST, server.port))
4456+
# should return None before the handshake is finished
4457+
t = s.export_keying_material('foo', 1)
4458+
self.assertEqual(t, None)
4459+
s.do_handshake()
4460+
# material_len must be positive
4461+
with self.assertRaises(ValueError) as cm:
4462+
s.export_keying_material('foo', 0)
4463+
with self.assertRaises(ValueError) as cm:
4464+
s.export_keying_material('foo', -1)
4465+
with self.assertRaises(ValueError) as cm:
4466+
s.export_keying_material('foo', -13)
4467+
# Strings containing non-ASCII labels are not allowed
4468+
with self.assertRaises(UnicodeEncodeError) as cm:
4469+
s.export_keying_material('\u0394', 1)
4470+
with self.assertRaises(UnicodeEncodeError) as cm:
4471+
s.export_keying_material('foo', 1, '\u0394')
4472+
for args in [
4473+
( 'foo', 32 ),
4474+
( 'foo', 32, None ),
4475+
( 'foo', 32, '' ),
4476+
( 'foo', 32, 'bar' ),
4477+
( b'foo', 32, b'bar' ),
4478+
( b'foo', 32, b'bar' ),
4479+
( b'foo', 1, b'bar' ),
4480+
( b'foo', 128, b'bar' ),
4481+
( b'\x00\x01\0x2\x03\x80\xa1\xc2\xe3', 128,
4482+
b'\x80\xa1\xc2\xe3\x00\x01\0x2\x03' ),
4483+
]:
4484+
t = s.export_keying_material(*args)
4485+
self.assertEqual(len(t), args[1])
4486+
s.close()
4487+
# should return None after the socket has been closed
4488+
t = s.export_keying_material('foo', 1)
4489+
self.assertEqual(t, None)
4490+
4491+
@requires_tls_version('TLSv1_2')
4492+
def test_export_keying_material_tlsv1_2(self):
4493+
self.export_keying_material_test(ssl.TLSVersion.TLSv1_2)
4494+
4495+
@requires_tls_version('TLSv1_3')
4496+
def test_export_keying_material_tlsv1_3(self):
4497+
self.export_keying_material_test(ssl.TLSVersion.TLSv1_3)
44394498

44404499
@unittest.skipUnless(has_tls_version('TLSv1_3'), "Test needs TLS 1.3")
44414500
class TestPostHandshakeAuth(unittest.TestCase):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added support for the RFC5705 :func:`ssl.SSLSocket.export_keying_material`

Modules/_ssl.c

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2749,6 +2749,53 @@ _ssl__SSLSocket_verify_client_post_handshake_impl(PySSLSocket *self)
27492749
#endif
27502750
}
27512751

2752+
/*[clinic input]
2753+
_ssl._SSLSocket.export_keying_material
2754+
label: Py_buffer(accept={buffer, str})
2755+
material_len: int
2756+
context: Py_buffer(accept={buffer, str, NoneType})
2757+
[clinic start generated code]*/
2758+
2759+
static PyObject *
2760+
_ssl__SSLSocket_export_keying_material_impl(PySSLSocket *self,
2761+
Py_buffer *label,
2762+
int material_len,
2763+
Py_buffer *context)
2764+
/*[clinic end generated code: output=17b975255ccb2984 input=57daff6c33809e2e]*/
2765+
{
2766+
PyObject *out = NULL;
2767+
unsigned char *material;
2768+
2769+
if (material_len < 1) {
2770+
PyErr_SetString(PyExc_ValueError, "material_len must be positive");
2771+
return NULL;
2772+
}
2773+
2774+
if (!SSL_is_init_finished(self->ssl)) {
2775+
/* handshake not finished */
2776+
Py_RETURN_NONE;
2777+
}
2778+
2779+
out = PyBytes_FromStringAndSize(NULL, material_len);
2780+
if (out == NULL)
2781+
goto error;
2782+
2783+
material = (unsigned char *)PyBytes_AS_STRING(out);
2784+
2785+
if (!SSL_export_keying_material(self->ssl,
2786+
material, material_len,
2787+
label->buf, label->len,
2788+
context->buf, context->len,
2789+
context->buf != NULL)) {
2790+
Py_CLEAR(out);
2791+
_setSSLError(get_state_ctx(self), NULL, 0, __FILE__, __LINE__);
2792+
goto error;
2793+
}
2794+
2795+
error:
2796+
return out;
2797+
}
2798+
27522799
static SSL_SESSION*
27532800
_ssl_session_dup(SSL_SESSION *session) {
27542801
SSL_SESSION *newsession = NULL;
@@ -2915,6 +2962,7 @@ static PyMethodDef PySSLMethods[] = {
29152962
_SSL__SSLSOCKET_VERIFY_CLIENT_POST_HANDSHAKE_METHODDEF
29162963
_SSL__SSLSOCKET_GET_UNVERIFIED_CHAIN_METHODDEF
29172964
_SSL__SSLSOCKET_GET_VERIFIED_CHAIN_METHODDEF
2965+
_SSL__SSLSOCKET_EXPORT_KEYING_MATERIAL_METHODDEF
29182966
{NULL, NULL}
29192967
};
29202968

Modules/clinic/_ssl.c.h

Lines changed: 44 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)