Skip to content

Commit 9d0f6dc

Browse files
committed
gh-63284: Add support for TLS-PSK (pre-shared key) to the ssl module
1 parent dcd6f22 commit 9d0f6dc

File tree

10 files changed

+482
-1
lines changed

10 files changed

+482
-1
lines changed

Doc/library/ssl.rst

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1987,6 +1987,80 @@ to speed up repeated connections from the same clients.
19871987
>>> ssl.create_default_context().verify_mode # doctest: +SKIP
19881988
<VerifyMode.CERT_REQUIRED: 2>
19891989

1990+
.. method:: SSLContext.set_psk_client_callback(callback)
1991+
1992+
Enables TLS-PSK (pre-shared key) authentication on a client-side connection.
1993+
1994+
In general, certificate based authentication should be preferred over this method.
1995+
1996+
The parameter ``callback`` is a callable object with the signature:
1997+
``def callback(hint: str | None) -> tuple[str | None, bytes]``.
1998+
The ``hint`` parameter is an optional identity hint sent by the server.
1999+
The return value is a tuple in the form (client-identity, psk).
2000+
Client-identity is an optional string which may be used by the server to
2001+
select a corresponding PSK for the client. PSK is a
2002+
:term:`bytes-like object` representing the pre-shared key.
2003+
2004+
Setting ``callback`` to :const:`None` removes any existing callback.
2005+
2006+
Example usage::
2007+
2008+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
2009+
context.check_hostname = False
2010+
context.verify_mode = ssl.CERT_NONE
2011+
context.maximum_version = ssl.TLSVersion.TLSv1_2
2012+
context.set_ciphers('PSK')
2013+
2014+
# A simple lambda:
2015+
psk = bytes.fromhex('deadbeef')
2016+
context.set_psk_client_callback(lambda hint: (None, psk))
2017+
2018+
# A table using the hint from the server:
2019+
psk_table = { 'ServerId_1': bytes.fromhex('deadbeef'),
2020+
'ServerId_2': bytes.fromhex('cafebabe')
2021+
}
2022+
def callback(hint):
2023+
return 'ClientId_1', psk_table[hint]
2024+
context.set_psk_client_callback(callback)
2025+
2026+
.. versionadded:: 3.12
2027+
2028+
.. method:: SSLContext.set_psk_server_callback(callback, identity_hint=None)
2029+
2030+
Enables TLS-PSK (pre-shared key) authentication on a server-side connection.
2031+
2032+
In general, certificate based authentication should be preferred over this method.
2033+
2034+
The parameter ``callback`` is a callable object with the signature:
2035+
``def callback(identity: str | None) -> bytes``.
2036+
The ``identity`` parameter is an optional identity sent by the client which can
2037+
be used to select a corresponding PSK.
2038+
The return value is a :term:`bytes-like object` representing the pre-shared key.
2039+
2040+
Setting ``callback`` to :const:`None` removes any existing callback.
2041+
2042+
The parameter ``identity_hint`` is an optional identity hint sent to the client.
2043+
2044+
Example usage::
2045+
2046+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2047+
context.maximum_version = ssl.TLSVersion.TLSv1_2
2048+
context.set_ciphers('PSK')
2049+
2050+
# A simple lambda:
2051+
psk = bytes.fromhex('deadbeef')
2052+
context.set_psk_server_callback(lambda identity: psk)
2053+
2054+
# A table using the identity of the client:
2055+
psk_table = { 'ClientId_1': bytes.fromhex('deadbeef'),
2056+
'ClientId_2': bytes.fromhex('cafed00d')
2057+
}
2058+
def callback(identity):
2059+
return psk_table[identity]
2060+
context.set_psk_server_callback(callback, 'ServerId_1')
2061+
2062+
.. versionadded:: 3.12
2063+
19902064
.. index:: single: certificates
19912065

19922066
.. index:: single: X509 certificate

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ struct _Py_global_strings {
301301
STRUCT_FOR_ID(call)
302302
STRUCT_FOR_ID(call_exception_handler)
303303
STRUCT_FOR_ID(call_soon)
304+
STRUCT_FOR_ID(callback)
304305
STRUCT_FOR_ID(cancel)
305306
STRUCT_FOR_ID(capath)
306307
STRUCT_FOR_ID(category)
@@ -440,6 +441,7 @@ struct _Py_global_strings {
440441
STRUCT_FOR_ID(hook)
441442
STRUCT_FOR_ID(id)
442443
STRUCT_FOR_ID(ident)
444+
STRUCT_FOR_ID(identity_hint)
443445
STRUCT_FOR_ID(ignore)
444446
STRUCT_FOR_ID(imag)
445447
STRUCT_FOR_ID(importlib)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_ssl.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4203,6 +4203,62 @@ def test_session_handling(self):
42034203
self.assertEqual(str(e.exception),
42044204
'Session refers to a different SSLContext.')
42054205

4206+
@requires_tls_version('TLSv1_2')
4207+
def test_psk(self):
4208+
psk = bytes.fromhex('deadbeef')
4209+
4210+
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
4211+
client_context.check_hostname = False
4212+
client_context.verify_mode = ssl.CERT_NONE
4213+
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
4214+
client_context.set_ciphers('PSK')
4215+
client_context.set_psk_client_callback(lambda hint: (None, psk))
4216+
4217+
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
4218+
server_context.maximum_version = ssl.TLSVersion.TLSv1_2
4219+
server_context.set_ciphers('PSK')
4220+
server_context.set_psk_server_callback(lambda identity: psk)
4221+
4222+
# correct PSK should connect
4223+
server = ThreadedEchoServer(context=server_context)
4224+
with server:
4225+
with client_context.wrap_socket(socket.socket()) as s:
4226+
s.connect((HOST, server.port))
4227+
4228+
# incorrect PSK should fail
4229+
incorrect_psk = bytes.fromhex('cafebabe')
4230+
client_context.set_psk_client_callback(lambda hint: (None, incorrect_psk))
4231+
server = ThreadedEchoServer(context=server_context)
4232+
with server:
4233+
with client_context.wrap_socket(socket.socket()) as s:
4234+
with self.assertRaises(ssl.SSLError):
4235+
s.connect((HOST, server.port))
4236+
4237+
# identity_hint and client_identity should be sent to the other side
4238+
identity_hint = 'identity-hint'
4239+
client_identity = 'client-identity'
4240+
4241+
def client_callback(hint):
4242+
self.assertEqual(hint, identity_hint)
4243+
return client_identity, psk
4244+
4245+
def server_callback(identity):
4246+
self.assertEqual(identity, client_identity)
4247+
return psk
4248+
4249+
client_context.set_psk_client_callback(client_callback)
4250+
server_context.set_psk_server_callback(server_callback, identity_hint)
4251+
server = ThreadedEchoServer(context=server_context)
4252+
with server:
4253+
with client_context.wrap_socket(socket.socket()) as s:
4254+
s.connect((HOST, server.port))
4255+
4256+
# adding client callback to server or vice versa raises an exception
4257+
with self.assertRaisesRegex(ssl.SSLError, 'Cannot add PSK server callback'):
4258+
client_context.set_psk_server_callback(server_callback, identity_hint)
4259+
with self.assertRaisesRegex(ssl.SSLError, 'Cannot add PSK client callback'):
4260+
server_context.set_psk_client_callback(client_callback)
4261+
42064262

42074263
@unittest.skipUnless(has_tls_version('TLSv1_3'), "Test needs TLS 1.3")
42084264
class TestPostHandshakeAuth(unittest.TestCase):

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,6 +1466,7 @@ Ajith Ramachandran
14661466
Dhushyanth Ramasamy
14671467
Ashwin Ramaswami
14681468
Jeff Ramnani
1469+
Grant Ramsay
14691470
Bayard Randel
14701471
Varpu Rantala
14711472
Brodie Rao
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added support for TLS-PSK (pre-shared key) to the ssl module

0 commit comments

Comments
 (0)