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

File tree

10 files changed

+561
-1
lines changed

10 files changed

+561
-1
lines changed

Doc/library/ssl.rst

Lines changed: 88 additions & 0 deletions
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

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
@@ -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

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: 99 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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
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) mode to the :mod:`ssl` module.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy