Skip to content

Commit 370aa28

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 370aa28

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

src/OpenSSL/SSL.py

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