Skip to content

Commit 5d890a0

Browse files
markrwilliamsreaperhulk
authored andcommitted
ALPN: complete handshake without accepting a client's protocols. (pyca#876)
* ALPN: complete handshake without accepting a client's protocols. The callback passed to `SSL_CTX_set_alpn_select_cb` can return `SSL_TLSEXT_ERR_NOACK` to allow the handshake to continue without accepting any of the client's offered protocols. This commit introduces `NO_OVERLAPPING_PROTOCOLS`, which the Python callback passed to `Context.set_alpn_select_callback` can return to achieve the same thing. It does not change the previous meaning of an empty string, which still terminates the handshake. * Update src/OpenSSL/SSL.py Co-Authored-By: Alex Gaynor <[email protected]> * Address @alex's review. * Use recorded value in test, fix lint error. * Cover TypeError branch in _ALPNHelper.callback
1 parent 079c963 commit 5d890a0

File tree

4 files changed

+114
-11
lines changed

4 files changed

+114
-11
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Changes:
3030

3131
- Support ``bytearray`` in ``SSL.Connection.send()`` by using cffi's from_buffer.
3232
`#852 <https://github.com/pyca/pyopenssl/pull/852>`_
33+
- The ``OpenSSL.SSL.Context.set_alpn_select_callback`` can return a new ``NO_OVERLAPPING_PROTOCOLS`` sentinel value
34+
to allow a TLS handshake to complete without an application protocol.
3335

3436

3537
----

doc/api/ssl.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ Context, Connection.
119119
for details.
120120

121121

122+
.. py:data:: NO_OVERLAPPING_PROTOCOLS
123+
124+
A sentinel value that can be returned by the callback passed to
125+
:py:meth:`Context.set_alpn_select_callback` to indicate that
126+
the handshake can continue without a specific application protocol.
127+
128+
.. versionadded:: 19.1
129+
130+
122131
.. autofunction:: SSLeay_version
123132

124133

src/OpenSSL/SSL.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,9 @@ def wrapper(ssl, out, outlen, in_, inlen, arg):
428428
)
429429

430430

431+
NO_OVERLAPPING_PROTOCOLS = object()
432+
433+
431434
class _ALPNSelectHelper(_CallbackExceptionHelper):
432435
"""
433436
Wrap a callback such that it can be used as an ALPN selection callback.
@@ -453,24 +456,32 @@ def wrapper(ssl, out, outlen, in_, inlen, arg):
453456
instr = instr[encoded_len + 1:]
454457

455458
# Call the callback
456-
outstr = callback(conn, protolist)
457-
458-
if not isinstance(outstr, _binary_type):
459-
raise TypeError("ALPN callback must return a bytestring.")
459+
outbytes = callback(conn, protolist)
460+
any_accepted = True
461+
if outbytes is NO_OVERLAPPING_PROTOCOLS:
462+
outbytes = b''
463+
any_accepted = False
464+
elif not isinstance(outbytes, _binary_type):
465+
raise TypeError(
466+
"ALPN callback must return a bytestring or the "
467+
"special NO_OVERLAPPING_PROTOCOLS sentinel value."
468+
)
460469

461470
# Save our callback arguments on the connection object to make
462471
# sure that they don't get freed before OpenSSL can use them.
463472
# Then, return them in the appropriate output parameters.
464473
conn._alpn_select_callback_args = [
465-
_ffi.new("unsigned char *", len(outstr)),
466-
_ffi.new("unsigned char[]", outstr),
474+
_ffi.new("unsigned char *", len(outbytes)),
475+
_ffi.new("unsigned char[]", outbytes),
467476
]
468477
outlen[0] = conn._alpn_select_callback_args[0][0]
469478
out[0] = conn._alpn_select_callback_args[1]
470-
return 0
479+
if not any_accepted:
480+
return _lib.SSL_TLSEXT_ERR_NOACK
481+
return _lib.SSL_TLSEXT_ERR_OK
471482
except Exception as e:
472483
self._problems.append(e)
473-
return 2 # SSL_TLSEXT_ERR_ALERT_FATAL
484+
return _lib.SSL_TLSEXT_ERR_ALERT_FATAL
474485

475486
self.callback = _ffi.callback(
476487
("int (*)(SSL *, unsigned char **, unsigned char *, "
@@ -1476,8 +1487,12 @@ def set_alpn_select_callback(self, callback):
14761487
14771488
:param callback: The callback function. It will be invoked with two
14781489
arguments: the Connection, and a list of offered protocols as
1479-
bytestrings, e.g ``[b'http/1.1', b'spdy/2']``. It should return
1480-
one of those bytestrings, the chosen protocol.
1490+
bytestrings, e.g ``[b'http/1.1', b'spdy/2']``. It can return
1491+
one of those bytestrings to indicate the chosen protocol, the
1492+
empty bytestring to terminate the TLS connection, or the
1493+
:py:obj:`NO_OVERLAPPING_PROTOCOLS` to indicate that no offered
1494+
protocol was selected, but that the connection should not be
1495+
aborted.
14811496
"""
14821497
self._alpn_select_helper = _ALPNSelectHelper(callback)
14831498
self._alpn_select_callback = self._alpn_select_helper.callback

tests/test_ssl.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767

6868
from OpenSSL.SSL import (
6969
OP_NO_QUERY_MTU, OP_COOKIE_EXCHANGE, OP_NO_TICKET, OP_NO_COMPRESSION,
70-
MODE_RELEASE_BUFFERS)
70+
MODE_RELEASE_BUFFERS, NO_OVERLAPPING_PROTOCOLS)
7171

7272
from OpenSSL.SSL import (
7373
SSL_ST_CONNECT, SSL_ST_ACCEPT, SSL_ST_MASK,
@@ -1960,6 +1960,83 @@ def select(conn, options):
19601960

19611961
assert select_args == [(server, [b'http/1.1', b'spdy/2'])]
19621962

1963+
def test_alpn_no_server_overlap(self):
1964+
"""
1965+
A server can allow a TLS handshake to complete without
1966+
agreeing to an application protocol by returning
1967+
``NO_OVERLAPPING_PROTOCOLS``.
1968+
"""
1969+
refusal_args = []
1970+
1971+
def refusal(conn, options):
1972+
refusal_args.append((conn, options))
1973+
return NO_OVERLAPPING_PROTOCOLS
1974+
1975+
client_context = Context(SSLv23_METHOD)
1976+
client_context.set_alpn_protos([b'http/1.1', b'spdy/2'])
1977+
1978+
server_context = Context(SSLv23_METHOD)
1979+
server_context.set_alpn_select_callback(refusal)
1980+
1981+
# Necessary to actually accept the connection
1982+
server_context.use_privatekey(
1983+
load_privatekey(FILETYPE_PEM, server_key_pem))
1984+
server_context.use_certificate(
1985+
load_certificate(FILETYPE_PEM, server_cert_pem))
1986+
1987+
# Do a little connection to trigger the logic
1988+
server = Connection(server_context, None)
1989+
server.set_accept_state()
1990+
1991+
client = Connection(client_context, None)
1992+
client.set_connect_state()
1993+
1994+
# Do the dance.
1995+
interact_in_memory(server, client)
1996+
1997+
assert refusal_args == [(server, [b'http/1.1', b'spdy/2'])]
1998+
1999+
assert client.get_alpn_proto_negotiated() == b''
2000+
2001+
def test_alpn_select_cb_returns_invalid_value(self):
2002+
"""
2003+
If the ALPN selection callback returns anything other than
2004+
a bytestring or ``NO_OVERLAPPING_PROTOCOLS``, a
2005+
:py:exc:`TypeError` is raised.
2006+
"""
2007+
invalid_cb_args = []
2008+
2009+
def invalid_cb(conn, options):
2010+
invalid_cb_args.append((conn, options))
2011+
return u"can't return unicode"
2012+
2013+
client_context = Context(SSLv23_METHOD)
2014+
client_context.set_alpn_protos([b'http/1.1', b'spdy/2'])
2015+
2016+
server_context = Context(SSLv23_METHOD)
2017+
server_context.set_alpn_select_callback(invalid_cb)
2018+
2019+
# Necessary to actually accept the connection
2020+
server_context.use_privatekey(
2021+
load_privatekey(FILETYPE_PEM, server_key_pem))
2022+
server_context.use_certificate(
2023+
load_certificate(FILETYPE_PEM, server_cert_pem))
2024+
2025+
# Do a little connection to trigger the logic
2026+
server = Connection(server_context, None)
2027+
server.set_accept_state()
2028+
2029+
client = Connection(client_context, None)
2030+
client.set_connect_state()
2031+
2032+
# Do the dance.
2033+
with pytest.raises(TypeError):
2034+
interact_in_memory(server, client)
2035+
2036+
assert invalid_cb_args == [(server, [b'http/1.1', b'spdy/2'])]
2037+
2038+
assert client.get_alpn_proto_negotiated() == b''
2039+
19632040
def test_alpn_no_server(self):
19642041
"""
19652042
When clients and servers cannot agree on what protocol to use next

0 commit comments

Comments
 (0)