Skip to content

Commit e84e7b5

Browse files
authored
Expose some DTLS-related features (#1026)
* Expose DTLS_METHOD and friends * Expose OP_NO_RENEGOTIATION * Expose DTLS MTU-related functions * Expose DTLSv1_listen and associated callbacks * Add a basic DTLS test * Cope with old versions of openssl/libressl * blacken * Soothe flake8 * Add temporary hack to skip DTLS test on old cryptography versions * Update for cryptography v35 release * Add changelog entry * Fix versionadded:: * get_cleartext_mtu doesn't exist on decrepit old openssl * Rewrite DTLS test to work around stupid OpenSSL misbehavior * flake8 go away * minor tidying
1 parent ea90b55 commit e84e7b5

File tree

5 files changed

+336
-5
lines changed

5 files changed

+336
-5
lines changed

CHANGELOG.rst

+5
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ Backward-incompatible changes:
1212

1313
- Drop support for Python 2.7.
1414
`#1047 <https://github.com/pyca/pyopenssl/pull/1047>`_
15+
- The minimum ``cryptography`` version is now 35.0.
1516

1617
Deprecations:
1718
^^^^^^^^^^^^^
1819

1920
Changes:
2021
^^^^^^^^
2122

23+
- Expose wrappers for some `DTLS
24+
<https://en.wikipedia.org/wiki/Datagram_Transport_Layer_Security>`_
25+
primitives. `#1026 <https://github.com/pyca/pyopenssl/pull/1026>`_
26+
2227
21.0.0 (2021-09-28)
2328
-------------------
2429

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def find_meta(meta):
9393
package_dir={"": "src"},
9494
install_requires=[
9595
# Fix cryptographyMinimum in tox.ini when changing this!
96-
"cryptography>=3.3",
96+
"cryptography>=35.0",
9797
],
9898
extras_require={
9999
"test": ["flaky", "pretend", "pytest>=3.0.1"],

src/OpenSSL/SSL.py

+134-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
"TLS_METHOD",
4646
"TLS_SERVER_METHOD",
4747
"TLS_CLIENT_METHOD",
48+
"DTLS_METHOD",
49+
"DTLS_SERVER_METHOD",
50+
"DTLS_CLIENT_METHOD",
4851
"SSL3_VERSION",
4952
"TLS1_VERSION",
5053
"TLS1_1_VERSION",
@@ -80,6 +83,7 @@
8083
"OP_NO_QUERY_MTU",
8184
"OP_COOKIE_EXCHANGE",
8285
"OP_NO_TICKET",
86+
"OP_NO_RENEGOTIATION",
8387
"OP_ALL",
8488
"VERIFY_PEER",
8589
"VERIFY_FAIL_IF_NO_PEER_CERT",
@@ -149,6 +153,9 @@ class _buffer(object):
149153
TLS_METHOD = 7
150154
TLS_SERVER_METHOD = 8
151155
TLS_CLIENT_METHOD = 9
156+
DTLS_METHOD = 10
157+
DTLS_SERVER_METHOD = 11
158+
DTLS_CLIENT_METHOD = 12
152159

153160
try:
154161
SSL3_VERSION = _lib.SSL3_VERSION
@@ -206,6 +213,11 @@ class _buffer(object):
206213
OP_COOKIE_EXCHANGE = _lib.SSL_OP_COOKIE_EXCHANGE
207214
OP_NO_TICKET = _lib.SSL_OP_NO_TICKET
208215

216+
try:
217+
OP_NO_RENEGOTIATION = _lib.SSL_OP_NO_RENEGOTIATION
218+
except AttributeError:
219+
pass
220+
209221
OP_ALL = _lib.SSL_OP_ALL
210222

211223
VERIFY_PEER = _lib.SSL_VERIFY_PEER
@@ -547,6 +559,48 @@ def wrapper(ssl, cdata):
547559
self.callback = _ffi.callback("int (*)(SSL *, void *)", wrapper)
548560

549561

562+
class _CookieGenerateCallbackHelper(_CallbackExceptionHelper):
563+
def __init__(self, callback):
564+
_CallbackExceptionHelper.__init__(self)
565+
566+
@wraps(callback)
567+
def wrapper(ssl, out, outlen):
568+
try:
569+
conn = Connection._reverse_mapping[ssl]
570+
cookie = callback(conn)
571+
out[0 : len(cookie)] = cookie
572+
outlen[0] = len(cookie)
573+
return 1
574+
except Exception as e:
575+
self._problems.append(e)
576+
# "a zero return value can be used to abort the handshake"
577+
return 0
578+
579+
self.callback = _ffi.callback(
580+
"int (*)(SSL *, unsigned char *, unsigned int *)",
581+
wrapper,
582+
)
583+
584+
585+
class _CookieVerifyCallbackHelper(_CallbackExceptionHelper):
586+
def __init__(self, callback):
587+
_CallbackExceptionHelper.__init__(self)
588+
589+
@wraps(callback)
590+
def wrapper(ssl, c_cookie, cookie_len):
591+
try:
592+
conn = Connection._reverse_mapping[ssl]
593+
return callback(conn, bytes(c_cookie[0:cookie_len]))
594+
except Exception as e:
595+
self._problems.append(e)
596+
return 0
597+
598+
self.callback = _ffi.callback(
599+
"int (*)(SSL *, unsigned char *, unsigned int)",
600+
wrapper,
601+
)
602+
603+
550604
def _asFileDescriptor(obj):
551605
fd = None
552606
if not isinstance(obj, int):
@@ -628,7 +682,8 @@ class Context(object):
628682
:class:`OpenSSL.SSL.Context` instances define the parameters for setting
629683
up new SSL connections.
630684
631-
:param method: One of TLS_METHOD, TLS_CLIENT_METHOD, or TLS_SERVER_METHOD.
685+
:param method: One of TLS_METHOD, TLS_CLIENT_METHOD, TLS_SERVER_METHOD,
686+
DTLS_METHOD, DTLS_CLIENT_METHOD, or DTLS_SERVER_METHOD.
632687
SSLv23_METHOD, TLSv1_METHOD, etc. are deprecated and should
633688
not be used.
634689
"""
@@ -643,6 +698,9 @@ class Context(object):
643698
TLS_METHOD: "TLS_method",
644699
TLS_SERVER_METHOD: "TLS_server_method",
645700
TLS_CLIENT_METHOD: "TLS_client_method",
701+
DTLS_METHOD: "DTLS_method",
702+
DTLS_SERVER_METHOD: "DTLS_server_method",
703+
DTLS_CLIENT_METHOD: "DTLS_client_method",
646704
}
647705
_methods = dict(
648706
(identifier, getattr(_lib, name))
@@ -687,6 +745,8 @@ def __init__(self, method):
687745
self._ocsp_helper = None
688746
self._ocsp_callback = None
689747
self._ocsp_data = None
748+
self._cookie_generate_helper = None
749+
self._cookie_verify_helper = None
690750

691751
self.set_mode(_lib.SSL_MODE_ENABLE_PARTIAL_WRITE)
692752

@@ -1527,6 +1587,20 @@ def set_ocsp_client_callback(self, callback, data=None):
15271587
helper = _OCSPClientCallbackHelper(callback)
15281588
self._set_ocsp_callback(helper, data)
15291589

1590+
def set_cookie_generate_callback(self, callback):
1591+
self._cookie_generate_helper = _CookieGenerateCallbackHelper(callback)
1592+
_lib.SSL_CTX_set_cookie_generate_cb(
1593+
self._context,
1594+
self._cookie_generate_helper.callback,
1595+
)
1596+
1597+
def set_cookie_verify_callback(self, callback):
1598+
self._cookie_verify_helper = _CookieVerifyCallbackHelper(callback)
1599+
_lib.SSL_CTX_set_cookie_verify_cb(
1600+
self._context,
1601+
self._cookie_verify_helper.callback,
1602+
)
1603+
15301604

15311605
class Connection(object):
15321606
_reverse_mapping = WeakValueDictionary()
@@ -1564,6 +1638,10 @@ def __init__(self, context, socket=None):
15641638
self._verify_helper = context._verify_helper
15651639
self._verify_callback = context._verify_callback
15661640

1641+
# And likewise for the cookie callbacks
1642+
self._cookie_generate_helper = context._cookie_generate_helper
1643+
self._cookie_verify_helper = context._cookie_verify_helper
1644+
15671645
self._reverse_mapping[self._ssl] = self
15681646

15691647
if socket is None:
@@ -1672,6 +1750,35 @@ def get_servername(self):
16721750

16731751
return _ffi.string(name)
16741752

1753+
def set_ciphertext_mtu(self, mtu):
1754+
"""
1755+
For DTLS, set the maximum UDP payload size (*not* including IP/UDP
1756+
overhead).
1757+
1758+
Note that you might have to set :data:`OP_NO_QUERY_MTU` to prevent
1759+
OpenSSL from spontaneously clearing this.
1760+
1761+
:param mtu: An integer giving the maximum transmission unit.
1762+
1763+
.. versionadded:: 21.1
1764+
"""
1765+
_lib.SSL_set_mtu(self._ssl, mtu)
1766+
1767+
def get_cleartext_mtu(self):
1768+
"""
1769+
For DTLS, get the maximum size of unencrypted data you can pass to
1770+
:meth:`write` without exceeding the MTU (as passed to
1771+
:meth:`set_ciphertext_mtu`).
1772+
1773+
:return: The effective MTU as an integer.
1774+
1775+
.. versionadded:: 21.1
1776+
"""
1777+
1778+
if not hasattr(_lib, "DTLS_get_data_mtu"):
1779+
raise NotImplementedError("requires OpenSSL 1.1.1 or better")
1780+
return _lib.DTLS_get_data_mtu(self._ssl)
1781+
16751782
def set_tlsext_host_name(self, name):
16761783
"""
16771784
Set the value of the servername extension to send in the client hello.
@@ -1957,6 +2064,32 @@ def accept(self):
19572064
conn.set_accept_state()
19582065
return (conn, addr)
19592066

2067+
def DTLSv1_listen(self):
2068+
"""
2069+
Call the OpenSSL function DTLSv1_listen on this connection. See the
2070+
OpenSSL manual for more details.
2071+
2072+
:return: None
2073+
"""
2074+
# Possible future extension: return the BIO_ADDR in some form.
2075+
bio_addr = _lib.BIO_ADDR_new()
2076+
try:
2077+
result = _lib.DTLSv1_listen(self._ssl, bio_addr)
2078+
finally:
2079+
_lib.BIO_ADDR_free(bio_addr)
2080+
# DTLSv1_listen is weird. A zero return value means 'didn't find a
2081+
# ClientHello with valid cookie, but keep trying'. So basically
2082+
# WantReadError. But it doesn't work correctly with _raise_ssl_error.
2083+
# So we raise it manually instead.
2084+
if self._cookie_generate_helper is not None:
2085+
self._cookie_generate_helper.raise_if_problem()
2086+
if self._cookie_verify_helper is not None:
2087+
self._cookie_verify_helper.raise_if_problem()
2088+
if result == 0:
2089+
raise WantReadError()
2090+
if result < 0:
2091+
self._raise_ssl_error(self._ssl, result)
2092+
19602093
def bio_shutdown(self):
19612094
"""
19622095
If the Connection was created with a memory BIO, this method can be

0 commit comments

Comments
 (0)