Skip to content

Commit ce6d7bd

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 08ec8e6 commit ce6d7bd

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed

src/OpenSSL/SSL.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,7 @@ def __init__(self, method: int) -> None:
905905
self._info_callback = None
906906
self._keylog_callback = None
907907
self._tlsext_servername_callback = None
908+
self._client_hello_callback = None
908909
self._app_data = None
909910
self._alpn_select_helper: _ALPNSelectHelper | None = None
910911
self._alpn_select_callback: _ALPNSelectCallback | None = None
@@ -1762,6 +1763,30 @@ def wrapper(ssl, alert, arg): # type: ignore[no-untyped-def]
17621763
self._context, self._tlsext_servername_callback
17631764
)
17641765

1766+
@_require_not_used
1767+
def set_ssl_ctx_client_hello_callback(self, callback) -> None:
1768+
"""
1769+
Specify a callback function to be called when the ClientHello
1770+
is received.
1771+
1772+
:param callback: The callback function. It will be invoked with one
1773+
argument, the Connection instance.
1774+
1775+
.. versionadded:: 0.13
1776+
"""
1777+
1778+
@wraps(callback)
1779+
def wrapper(ssl, alert, arg): # type: ignore[no-untyped-def]
1780+
callback(Connection._reverse_mapping[ssl])
1781+
return 1
1782+
1783+
self._client_hello_callback = _ffi.callback(
1784+
"int (*)(SSL *, int *, void *)", wrapper
1785+
)
1786+
_lib.SSL_CTX_set_client_hello_cb(
1787+
self._context, self._client_hello_callback, _ffi.NULL
1788+
)
1789+
17651790
@_require_not_used
17661791
def set_tlsext_use_srtp(self, profiles: bytes) -> None:
17671792
"""
@@ -3262,3 +3287,48 @@ def wrapper(ssl, where, return_code): # type: ignore[no-untyped-def]
32623287
"void (*)(const SSL *, int, int)", wrapper
32633288
)
32643289
_lib.SSL_set_info_callback(self._ssl, self._info_callback)
3290+
3291+
def get_client_hello_extension(self, type: int) -> bytes:
3292+
"""
3293+
Returns the client extension with the specified type. If the extensions
3294+
cannot be found an empty byte string is returned.
3295+
3296+
:param type: The type of extension to retrieve as integer.
3297+
:return: A byte array containing the extension or an empty byte array
3298+
if the extension is absent.
3299+
"""
3300+
out = _ffi.new("const unsigned char **")
3301+
outlen = _ffi.new("size_t *")
3302+
_lib.SSL_client_hello_get0_ext(self._ssl, type, out, outlen)
3303+
3304+
if not outlen:
3305+
return b""
3306+
3307+
return _ffi.buffer(out[0], outlen[0])[:]
3308+
3309+
def get_client_hello_extensions_present(self) -> list[bytes]:
3310+
"""
3311+
Returns a list of the types of the client hello extensions
3312+
that are present in the ClientHello message.
3313+
"""
3314+
# SSL_client_hello_get1_extensions_present returns a new array
3315+
# allocated by OpenSSL_malloc
3316+
data = _ffi.new("int **")
3317+
data_len = _ffi.new("size_t *")
3318+
rc = _lib.SSL_client_hello_get1_extensions_present(
3319+
self._ssl, data, data_len
3320+
)
3321+
3322+
_openssl_assert(rc == 1)
3323+
3324+
if not data_len:
3325+
return []
3326+
3327+
# OpenSSL returns the number of items and FFI wants the numbers of
3328+
# types, so multiply it by the size of each item (int)
3329+
data_gc = _ffi.gc(data[0], _lib.OPENSSL_free)
3330+
3331+
buf = _ffi.buffer(data_gc, data_len[0] * _ffi.sizeof("int"))
3332+
retarray = _ffi.from_buffer("int[]", buf)
3333+
3334+
return list(retarray)

tests/test_ssl.py

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

23172367
class TestSession:
23182368
"""

0 commit comments

Comments
 (0)