Skip to content

Commit 705d7cf

Browse files
committed
Add methods related to client hello callback
These methods allow a server to switch the context when looking at ALPN and servername together. This is for example require when implementing the ACME tls-alpn/1 protocol. Unfortunately, OpenSSL does not provide any utility function to actually parse ClientHello extensions when using these APIs and the user has to write their own methods. Closes: #1430 Signed-off-by: Arne Schwabe <arne@rfc2549.org>
1 parent a69d45f commit 705d7cf

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

src/OpenSSL/SSL.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,14 @@ def explode(*args, **kwargs): # type: ignore[no-untyped-def]
820820
"Getting group name is not supported by the linked OpenSSL version",
821821
)
822822

823+
_requires_client_hello_cb = _make_requires(
824+
getattr(_lib, "Cryptography_HAS_CLIENT_HELLO_CB", 0),
825+
(
826+
"SSL client hello callback is not supported by the "
827+
"linked cryptographic library"
828+
),
829+
)
830+
823831

824832
class Session:
825833
"""
@@ -905,6 +913,7 @@ def __init__(self, method: int) -> None:
905913
self._info_callback = None
906914
self._keylog_callback = None
907915
self._tlsext_servername_callback = None
916+
self._client_hello_callback = None
908917
self._app_data = None
909918
self._alpn_select_helper: _ALPNSelectHelper | None = None
910919
self._alpn_select_callback: _ALPNSelectCallback | None = None
@@ -1762,6 +1771,33 @@ def wrapper(ssl, alert, arg): # type: ignore[no-untyped-def]
17621771
self._context, self._tlsext_servername_callback
17631772
)
17641773

1774+
@_requires_client_hello_cb
1775+
@_require_not_used
1776+
def set_ssl_ctx_client_hello_callback(
1777+
self, callback: Callable[[Connection], None]
1778+
) -> None:
1779+
"""
1780+
Specify a callback function to be called when the ClientHello
1781+
is received.
1782+
1783+
:param callback: The callback function. It will be invoked with one
1784+
argument, the Connection instance.
1785+
1786+
.. versionadded:: 0.13
1787+
"""
1788+
1789+
@wraps(callback)
1790+
def wrapper(ssl, alert, arg): # type: ignore[no-untyped-def]
1791+
callback(Connection._reverse_mapping[ssl])
1792+
return 1
1793+
1794+
self._client_hello_callback = _ffi.callback(
1795+
"int (*)(SSL *, int *, void *)", wrapper
1796+
)
1797+
_lib.SSL_CTX_set_client_hello_cb(
1798+
self._context, self._client_hello_callback, _ffi.NULL
1799+
)
1800+
17651801
@_require_not_used
17661802
def set_tlsext_use_srtp(self, profiles: bytes) -> None:
17671803
"""
@@ -3262,3 +3298,50 @@ def wrapper(ssl, where, return_code): # type: ignore[no-untyped-def]
32623298
"void (*)(const SSL *, int, int)", wrapper
32633299
)
32643300
_lib.SSL_set_info_callback(self._ssl, self._info_callback)
3301+
3302+
@_requires_client_hello_cb
3303+
def get_client_hello_extension(self, type: int) -> bytes:
3304+
"""
3305+
Returns the client extension with the specified type. If the extensions
3306+
cannot be found an empty byte string is returned.
3307+
3308+
:param type: The type of extension to retrieve as integer.
3309+
:return: A byte array containing the extension or an empty byte array
3310+
if the extension is absent.
3311+
"""
3312+
out = _ffi.new("const unsigned char **")
3313+
outlen = _ffi.new("size_t *")
3314+
_lib.SSL_client_hello_get0_ext(self._ssl, type, out, outlen)
3315+
3316+
if not outlen:
3317+
return b""
3318+
3319+
return _ffi.buffer(out[0], outlen[0])[:]
3320+
3321+
@_requires_client_hello_cb
3322+
def get_client_hello_extensions_present(self) -> list[int]:
3323+
"""
3324+
Returns a list of the types of the client hello extensions
3325+
that are present in the ClientHello message.
3326+
"""
3327+
# SSL_client_hello_get1_extensions_present returns a new array
3328+
# allocated by OpenSSL_malloc
3329+
data = _ffi.new("int **")
3330+
data_len = _ffi.new("size_t *")
3331+
rc = _lib.SSL_client_hello_get1_extensions_present(
3332+
self._ssl, data, data_len
3333+
)
3334+
3335+
_openssl_assert(rc == 1)
3336+
3337+
if not data_len:
3338+
return []
3339+
3340+
# OpenSSL returns the number of items and FFI wants the numbers of
3341+
# types, so multiply it by the size of each item (int)
3342+
data_gc = _ffi.gc(data[0], _lib.OPENSSL_free)
3343+
3344+
buf = _ffi.buffer(data_gc, data_len[0] * _ffi.sizeof("int"))
3345+
retarray = _ffi.from_buffer("int[]", buf)
3346+
3347+
return list(retarray)

tests/test_ssl.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2313,6 +2313,60 @@ def select(conn: Connection, options: list[bytes]) -> bytes:
23132313
interact_in_memory(server, client)
23142314
assert select_args == [(server, [b"http/1.1", b"spdy/2"])]
23152315

2316+
@pytest.mark.skipif(
2317+
not getattr(_lib, "Cryptography_HAS_CLIENT_HELLO_CB", None),
2318+
reason="Client hello callback unavailable in crypto library",
2319+
)
2320+
def test_client_hello_callback(self) -> None:
2321+
"""
2322+
We can handle exceptions in the ALPN select callback.
2323+
"""
2324+
client_hello_extensions = {}
2325+
2326+
def client_hello_callback(conn: Connection) -> None:
2327+
for ext in conn.get_client_hello_extensions_present():
2328+
client_hello_extensions[ext] = conn.get_client_hello_extension(
2329+
ext
2330+
)
2331+
2332+
client_context = Context(SSLv23_METHOD)
2333+
client_context.set_alpn_protos([b"http/1.1", b"spdy/2"])
2334+
2335+
server_context = Context(SSLv23_METHOD)
2336+
server_context.set_ssl_ctx_client_hello_callback(client_hello_callback)
2337+
2338+
# Necessary to actually accept the connection
2339+
server_context.use_privatekey(
2340+
load_privatekey(FILETYPE_PEM, server_key_pem)
2341+
)
2342+
server_context.use_certificate(
2343+
load_certificate(FILETYPE_PEM, server_cert_pem)
2344+
)
2345+
2346+
# Do a little connection to trigger the logic
2347+
server = Connection(server_context, None)
2348+
server.set_accept_state()
2349+
2350+
client = Connection(client_context, None)
2351+
client.set_tlsext_host_name(b"unitest.example.com")
2352+
client.set_connect_state()
2353+
2354+
interact_in_memory(server, client)
2355+
2356+
# Servername indication has extensions number 0
2357+
# ALPN has extension number 16
2358+
assert 0 in client_hello_extensions
2359+
assert 16 in client_hello_extensions
2360+
2361+
# OpenSSL does not expose good APIs to parse hello extensions. Instead
2362+
# of implementing parsing them just for the unit test we hardcode the
2363+
# string we expect to see
2364+
assert (
2365+
client_hello_extensions[0]
2366+
== b"\x00\x16\x00\x00\x13unitest.example.com"
2367+
)
2368+
assert client_hello_extensions[16] == b"\x00\x10\x08http/1.1\x06spdy/2"
2369+
23162370

23172371
class TestSession:
23182372
"""

0 commit comments

Comments
 (0)