Skip to content

Commit e954ac7

Browse files
grantramsayarhadthedevgpshead
authored
gh-63284: Add support for TLS-PSK (pre-shared key) to the ssl module (#103181)
Add support for TLS-PSK (pre-shared key) to the ssl module. --------- Co-authored-by: Oleg Iarygin <oleg@arhadthedev.net> Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent fb202af commit e954ac7

10 files changed

+561
-1
lines changed

Doc/library/ssl.rst

+88
Original file line numberDiff line numberDiff line change
@@ -2006,6 +2006,94 @@ to speed up repeated connections from the same clients.
20062006
>>> ssl.create_default_context().verify_mode # doctest: +SKIP
20072007
<VerifyMode.CERT_REQUIRED: 2>
20082008

2009+
.. method:: SSLContext.set_psk_client_callback(callback)
2010+
2011+
Enables TLS-PSK (pre-shared key) authentication on a client-side connection.
2012+
2013+
In general, certificate based authentication should be preferred over this method.
2014+
2015+
The parameter ``callback`` is a callable object with the signature:
2016+
``def callback(hint: str | None) -> tuple[str | None, bytes]``.
2017+
The ``hint`` parameter is an optional identity hint sent by the server.
2018+
The return value is a tuple in the form (client-identity, psk).
2019+
Client-identity is an optional string which may be used by the server to
2020+
select a corresponding PSK for the client. The string must be less than or
2021+
equal to ``256`` octets when UTF-8 encoded. PSK is a
2022+
:term:`bytes-like object` representing the pre-shared key. Return a zero
2023+
length PSK to reject the connection.
2024+
2025+
Setting ``callback`` to :const:`None` removes any existing callback.
2026+
2027+
.. note::
2028+
When using TLS 1.3:
2029+
2030+
- the ``hint`` parameter is always :const:`None`.
2031+
- client-identity must be a non-empty string.
2032+
2033+
Example usage::
2034+
2035+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
2036+
context.check_hostname = False
2037+
context.verify_mode = ssl.CERT_NONE
2038+
context.maximum_version = ssl.TLSVersion.TLSv1_2
2039+
context.set_ciphers('PSK')
2040+
2041+
# A simple lambda:
2042+
psk = bytes.fromhex('c0ffee')
2043+
context.set_psk_client_callback(lambda hint: (None, psk))
2044+
2045+
# A table using the hint from the server:
2046+
psk_table = { 'ServerId_1': bytes.fromhex('c0ffee'),
2047+
'ServerId_2': bytes.fromhex('facade')
2048+
}
2049+
def callback(hint):
2050+
return 'ClientId_1', psk_table.get(hint, b'')
2051+
context.set_psk_client_callback(callback)
2052+
2053+
.. versionadded:: 3.13
2054+
2055+
.. method:: SSLContext.set_psk_server_callback(callback, identity_hint=None)
2056+
2057+
Enables TLS-PSK (pre-shared key) authentication on a server-side connection.
2058+
2059+
In general, certificate based authentication should be preferred over this method.
2060+
2061+
The parameter ``callback`` is a callable object with the signature:
2062+
``def callback(identity: str | None) -> bytes``.
2063+
The ``identity`` parameter is an optional identity sent by the client which can
2064+
be used to select a corresponding PSK.
2065+
The return value is a :term:`bytes-like object` representing the pre-shared key.
2066+
Return a zero length PSK to reject the connection.
2067+
2068+
Setting ``callback`` to :const:`None` removes any existing callback.
2069+
2070+
The parameter ``identity_hint`` is an optional identity hint string sent to
2071+
the client. The string must be less than or equal to ``256`` octets when
2072+
UTF-8 encoded.
2073+
2074+
.. note::
2075+
When using TLS 1.3 the ``identity_hint`` parameter is not sent to the client.
2076+
2077+
Example usage::
2078+
2079+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2080+
context.maximum_version = ssl.TLSVersion.TLSv1_2
2081+
context.set_ciphers('PSK')
2082+
2083+
# A simple lambda:
2084+
psk = bytes.fromhex('c0ffee')
2085+
context.set_psk_server_callback(lambda identity: psk)
2086+
2087+
# A table using the identity of the client:
2088+
psk_table = { 'ClientId_1': bytes.fromhex('c0ffee'),
2089+
'ClientId_2': bytes.fromhex('facade')
2090+
}
2091+
def callback(identity):
2092+
return psk_table.get(identity, b'')
2093+
context.set_psk_server_callback(callback, 'ServerId_1')
2094+
2095+
.. versionadded:: 3.13
2096+
20092097
.. index:: single: certificates
20102098

20112099
.. index:: single: X509 certificate

Include/internal/pycore_global_objects_fini_generated.h

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

+2
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ struct _Py_global_strings {
315315
STRUCT_FOR_ID(call)
316316
STRUCT_FOR_ID(call_exception_handler)
317317
STRUCT_FOR_ID(call_soon)
318+
STRUCT_FOR_ID(callback)
318319
STRUCT_FOR_ID(cancel)
319320
STRUCT_FOR_ID(capath)
320321
STRUCT_FOR_ID(category)
@@ -460,6 +461,7 @@ struct _Py_global_strings {
460461
STRUCT_FOR_ID(hook)
461462
STRUCT_FOR_ID(id)
462463
STRUCT_FOR_ID(ident)
464+
STRUCT_FOR_ID(identity_hint)
463465
STRUCT_FOR_ID(ignore)
464466
STRUCT_FOR_ID(imag)
465467
STRUCT_FOR_ID(importlib)

Include/internal/pycore_runtime_init_generated.h

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_ssl.py

+99
Original file line numberDiff line numberDiff line change
@@ -4236,6 +4236,105 @@ def test_session_handling(self):
42364236
self.assertEqual(str(e.exception),
42374237
'Session refers to a different SSLContext.')
42384238

4239+
@requires_tls_version('TLSv1_2')
4240+
def test_psk(self):
4241+
psk = bytes.fromhex('deadbeef')
4242+
4243+
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
4244+
client_context.check_hostname = False
4245+
client_context.verify_mode = ssl.CERT_NONE
4246+
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
4247+
client_context.set_ciphers('PSK')
4248+
client_context.set_psk_client_callback(lambda hint: (None, psk))
4249+
4250+
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
4251+
server_context.maximum_version = ssl.TLSVersion.TLSv1_2
4252+
server_context.set_ciphers('PSK')
4253+
server_context.set_psk_server_callback(lambda identity: psk)
4254+
4255+
# correct PSK should connect
4256+
server = ThreadedEchoServer(context=server_context)
4257+
with server:
4258+
with client_context.wrap_socket(socket.socket()) as s:
4259+
s.connect((HOST, server.port))
4260+
4261+
# incorrect PSK should fail
4262+
incorrect_psk = bytes.fromhex('cafebabe')
4263+
client_context.set_psk_client_callback(lambda hint: (None, incorrect_psk))
4264+
server = ThreadedEchoServer(context=server_context)
4265+
with server:
4266+
with client_context.wrap_socket(socket.socket()) as s:
4267+
with self.assertRaises(ssl.SSLError):
4268+
s.connect((HOST, server.port))
4269+
4270+
# identity_hint and client_identity should be sent to the other side
4271+
identity_hint = 'identity-hint'
4272+
client_identity = 'client-identity'
4273+
4274+
def client_callback(hint):
4275+
self.assertEqual(hint, identity_hint)
4276+
return client_identity, psk
4277+
4278+
def server_callback(identity):
4279+
self.assertEqual(identity, client_identity)
4280+
return psk
4281+
4282+
client_context.set_psk_client_callback(client_callback)
4283+
server_context.set_psk_server_callback(server_callback, identity_hint)
4284+
server = ThreadedEchoServer(context=server_context)
4285+
with server:
4286+
with client_context.wrap_socket(socket.socket()) as s:
4287+
s.connect((HOST, server.port))
4288+
4289+
# adding client callback to server or vice versa raises an exception
4290+
with self.assertRaisesRegex(ssl.SSLError, 'Cannot add PSK server callback'):
4291+
client_context.set_psk_server_callback(server_callback, identity_hint)
4292+
with self.assertRaisesRegex(ssl.SSLError, 'Cannot add PSK client callback'):
4293+
server_context.set_psk_client_callback(client_callback)
4294+
4295+
# test with UTF-8 identities
4296+
identity_hint = '身份暗示' # Translation: "Identity hint"
4297+
client_identity = '客户身份' # Translation: "Customer identity"
4298+
4299+
client_context.set_psk_client_callback(client_callback)
4300+
server_context.set_psk_server_callback(server_callback, identity_hint)
4301+
server = ThreadedEchoServer(context=server_context)
4302+
with server:
4303+
with client_context.wrap_socket(socket.socket()) as s:
4304+
s.connect((HOST, server.port))
4305+
4306+
@requires_tls_version('TLSv1_3')
4307+
def test_psk_tls1_3(self):
4308+
psk = bytes.fromhex('deadbeef')
4309+
identity_hint = 'identity-hint'
4310+
client_identity = 'client-identity'
4311+
4312+
def client_callback(hint):
4313+
# identity_hint is not sent to the client in TLS 1.3
4314+
self.assertIsNone(hint)
4315+
return client_identity, psk
4316+
4317+
def server_callback(identity):
4318+
self.assertEqual(identity, client_identity)
4319+
return psk
4320+
4321+
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
4322+
client_context.check_hostname = False
4323+
client_context.verify_mode = ssl.CERT_NONE
4324+
client_context.minimum_version = ssl.TLSVersion.TLSv1_3
4325+
client_context.set_ciphers('PSK')
4326+
client_context.set_psk_client_callback(client_callback)
4327+
4328+
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
4329+
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
4330+
server_context.set_ciphers('PSK')
4331+
server_context.set_psk_server_callback(server_callback, identity_hint)
4332+
4333+
server = ThreadedEchoServer(context=server_context)
4334+
with server:
4335+
with client_context.wrap_socket(socket.socket()) as s:
4336+
s.connect((HOST, server.port))
4337+
42394338

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

Misc/ACKS

+1
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,7 @@ Ajith Ramachandran
14821482
Dhushyanth Ramasamy
14831483
Ashwin Ramaswami
14841484
Jeff Ramnani
1485+
Grant Ramsay
14851486
Bayard Randel
14861487
Varpu Rantala
14871488
Brodie Rao
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added support for TLS-PSK (pre-shared key) mode to the :mod:`ssl` module.

0 commit comments

Comments
 (0)