Skip to content
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

gh-63284: Add support for TLS-PSK (pre-shared key) to the ssl module #103181

Merged
merged 22 commits into from Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9d0f6dc
gh-63284: Add support for TLS-PSK (pre-shared key) to the ssl module
grantramsay Mar 29, 2023
cfddf7b
Fix TLS-PSK for TLS 1.3
grantramsay Apr 3, 2023
603eeef
Merge branch 'main' into fix-issue-63284
arhadthedev May 14, 2023
18302e3
Decode TLS-PSK identities as UTF-8 rather than ASCII
grantramsay Jun 4, 2023
2efc876
Change TLS-PSK version added from 3.12 to 3.13
grantramsay Jun 4, 2023
a56784c
ReSTify NEWS.
gpshead Jun 5, 2023
7a1963d
Merge branch 'main' into fix-issue-63284
gpshead Jul 14, 2023
6f4100c
Set python exceptions raised during C callbacks as unraisable
grantramsay Jul 15, 2023
e4a97ec
Add NULL check for defensive coding
grantramsay Jul 15, 2023
3d75982
Get single value using PyBytes_AsStringAndSize
grantramsay Jul 15, 2023
539ed1f
Do not raise a decode exception if remote side sends invalid UTF-8 du…
grantramsay Jul 16, 2023
4634fc3
Free TLS-PSK callbacks when SSL context is deallocated
grantramsay Jul 16, 2023
d21c322
Use Py_XINCREF and Py_XDECREF for tidier NULL checking
grantramsay Jul 16, 2023
03fac4d
Check return value of SSL_CTX_use_psk_identity_hint
grantramsay Jul 16, 2023
3db3921
Update TLS-PSK documentation
grantramsay Jul 16, 2023
a75f3a7
Regenerate news entry to update the timestamp
grantramsay Jul 16, 2023
851a1e5
Merge branch 'main' into fix-issue-63284
gpshead Nov 26, 2023
4c68974
Update example key strings in the doc.
gpshead Nov 26, 2023
0fb8a3e
Update TLS-PSK documentation
grantramsay Nov 26, 2023
7788589
Regenerate news entry to update the timestamp
grantramsay Nov 26, 2023
11735b9
Code defensively around `z#` vs NULL and 0 length.
gpshead Nov 27, 2023
ad8b249
Merge branch 'fix-issue-63284' of https://github.com/grantramsay/cpyt…
gpshead Nov 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
88 changes: 88 additions & 0 deletions Doc/library/ssl.rst
Expand Up @@ -2006,6 +2006,94 @@ to speed up repeated connections from the same clients.
>>> ssl.create_default_context().verify_mode # doctest: +SKIP
<VerifyMode.CERT_REQUIRED: 2>

.. method:: SSLContext.set_psk_client_callback(callback)
grantramsay marked this conversation as resolved.
Show resolved Hide resolved

Enables TLS-PSK (pre-shared key) authentication on a client-side connection.

In general, certificate based authentication should be preferred over this method.

The parameter ``callback`` is a callable object with the signature:
``def callback(hint: str | None) -> tuple[str | None, bytes]``.
grantramsay marked this conversation as resolved.
Show resolved Hide resolved
The ``hint`` parameter is an optional identity hint sent by the server.
The return value is a tuple in the form (client-identity, psk).
Client-identity is an optional string which may be used by the server to
select a corresponding PSK for the client. The string must be less than or
equal to ``256`` octets when UTF-8 encoded. PSK is a
:term:`bytes-like object` representing the pre-shared key. Return a zero
length PSK to reject the connection.

Setting ``callback`` to :const:`None` removes any existing callback.

.. note::
When using TLS 1.3:

- the ``hint`` parameter is always :const:`None`.
- client-identity must be a non-empty string.

Example usage::

context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
context.maximum_version = ssl.TLSVersion.TLSv1_2
context.set_ciphers('PSK')

# A simple lambda:
psk = bytes.fromhex('c0ffee')
context.set_psk_client_callback(lambda hint: (None, psk))

# A table using the hint from the server:
psk_table = { 'ServerId_1': bytes.fromhex('c0ffee'),
'ServerId_2': bytes.fromhex('facade')
}
def callback(hint):
return 'ClientId_1', psk_table.get(hint, b'')
context.set_psk_client_callback(callback)

.. versionadded:: 3.13

.. method:: SSLContext.set_psk_server_callback(callback, identity_hint=None)

Enables TLS-PSK (pre-shared key) authentication on a server-side connection.

In general, certificate based authentication should be preferred over this method.

The parameter ``callback`` is a callable object with the signature:
``def callback(identity: str | None) -> bytes``.
The ``identity`` parameter is an optional identity sent by the client which can
be used to select a corresponding PSK.
The return value is a :term:`bytes-like object` representing the pre-shared key.
Return a zero length PSK to reject the connection.

Setting ``callback`` to :const:`None` removes any existing callback.

The parameter ``identity_hint`` is an optional identity hint string sent to
the client. The string must be less than or equal to ``256`` octets when
UTF-8 encoded.

.. note::
When using TLS 1.3 the ``identity_hint`` parameter is not sent to the client.

Example usage::

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.maximum_version = ssl.TLSVersion.TLSv1_2
context.set_ciphers('PSK')

# A simple lambda:
psk = bytes.fromhex('c0ffee')
context.set_psk_server_callback(lambda identity: psk)

# A table using the identity of the client:
psk_table = { 'ClientId_1': bytes.fromhex('c0ffee'),
'ClientId_2': bytes.fromhex('facade')
}
def callback(identity):
return psk_table.get(identity, b'')
context.set_psk_server_callback(callback, 'ServerId_1')

.. versionadded:: 3.13

.. index:: single: certificates

.. index:: single: X509 certificate
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

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

2 changes: 2 additions & 0 deletions Include/internal/pycore_global_strings.h
Expand Up @@ -315,6 +315,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(call)
STRUCT_FOR_ID(call_exception_handler)
STRUCT_FOR_ID(call_soon)
STRUCT_FOR_ID(callback)
STRUCT_FOR_ID(cancel)
STRUCT_FOR_ID(capath)
STRUCT_FOR_ID(category)
Expand Down Expand Up @@ -460,6 +461,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(hook)
STRUCT_FOR_ID(id)
STRUCT_FOR_ID(ident)
STRUCT_FOR_ID(identity_hint)
STRUCT_FOR_ID(ignore)
STRUCT_FOR_ID(imag)
STRUCT_FOR_ID(importlib)
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_runtime_init_generated.h

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

6 changes: 6 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

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

99 changes: 99 additions & 0 deletions Lib/test/test_ssl.py
Expand Up @@ -4236,6 +4236,105 @@ def test_session_handling(self):
self.assertEqual(str(e.exception),
'Session refers to a different SSLContext.')

@requires_tls_version('TLSv1_2')
def test_psk(self):
psk = bytes.fromhex('deadbeef')

client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.check_hostname = False
client_context.verify_mode = ssl.CERT_NONE
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
client_context.set_ciphers('PSK')
client_context.set_psk_client_callback(lambda hint: (None, psk))

server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.maximum_version = ssl.TLSVersion.TLSv1_2
server_context.set_ciphers('PSK')
server_context.set_psk_server_callback(lambda identity: psk)

# correct PSK should connect
server = ThreadedEchoServer(context=server_context)
with server:
with client_context.wrap_socket(socket.socket()) as s:
s.connect((HOST, server.port))

# incorrect PSK should fail
incorrect_psk = bytes.fromhex('cafebabe')
client_context.set_psk_client_callback(lambda hint: (None, incorrect_psk))
server = ThreadedEchoServer(context=server_context)
with server:
with client_context.wrap_socket(socket.socket()) as s:
with self.assertRaises(ssl.SSLError):
s.connect((HOST, server.port))

# identity_hint and client_identity should be sent to the other side
identity_hint = 'identity-hint'
client_identity = 'client-identity'

def client_callback(hint):
self.assertEqual(hint, identity_hint)
return client_identity, psk

def server_callback(identity):
self.assertEqual(identity, client_identity)
return psk

client_context.set_psk_client_callback(client_callback)
server_context.set_psk_server_callback(server_callback, identity_hint)
server = ThreadedEchoServer(context=server_context)
with server:
with client_context.wrap_socket(socket.socket()) as s:
s.connect((HOST, server.port))

# adding client callback to server or vice versa raises an exception
with self.assertRaisesRegex(ssl.SSLError, 'Cannot add PSK server callback'):
client_context.set_psk_server_callback(server_callback, identity_hint)
with self.assertRaisesRegex(ssl.SSLError, 'Cannot add PSK client callback'):
server_context.set_psk_client_callback(client_callback)

# test with UTF-8 identities
identity_hint = '身份暗示' # Translation: "Identity hint"
client_identity = '客户身份' # Translation: "Customer identity"

client_context.set_psk_client_callback(client_callback)
server_context.set_psk_server_callback(server_callback, identity_hint)
server = ThreadedEchoServer(context=server_context)
with server:
with client_context.wrap_socket(socket.socket()) as s:
s.connect((HOST, server.port))

@requires_tls_version('TLSv1_3')
def test_psk_tls1_3(self):
psk = bytes.fromhex('deadbeef')
identity_hint = 'identity-hint'
client_identity = 'client-identity'

def client_callback(hint):
# identity_hint is not sent to the client in TLS 1.3
self.assertIsNone(hint)
return client_identity, psk

def server_callback(identity):
self.assertEqual(identity, client_identity)
return psk

client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.check_hostname = False
client_context.verify_mode = ssl.CERT_NONE
client_context.minimum_version = ssl.TLSVersion.TLSv1_3
client_context.set_ciphers('PSK')
client_context.set_psk_client_callback(client_callback)

server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
server_context.set_ciphers('PSK')
server_context.set_psk_server_callback(server_callback, identity_hint)

server = ThreadedEchoServer(context=server_context)
with server:
with client_context.wrap_socket(socket.socket()) as s:
s.connect((HOST, server.port))


@unittest.skipUnless(has_tls_version('TLSv1_3'), "Test needs TLS 1.3")
class TestPostHandshakeAuth(unittest.TestCase):
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Expand Up @@ -1482,6 +1482,7 @@ Ajith Ramachandran
Dhushyanth Ramasamy
Ashwin Ramaswami
Jeff Ramnani
Grant Ramsay
Bayard Randel
Varpu Rantala
Brodie Rao
Expand Down
@@ -0,0 +1 @@
Added support for TLS-PSK (pre-shared key) mode to the :mod:`ssl` module.