From c29eb6760bce82e4013e0698751ae519c1e7fdbf Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 23 Jun 2026 11:22:06 +0000 Subject: [PATCH 1/8] Fix DTLS server example consuming the ClientHello before handshake (F-3481) The DTLS branch called bind_socket.recvfrom(1) before creating the context. On UDP that removes the entire first datagram (the client's ClientHello) from the queue and discards everything past the first byte, so wolfSSL_accept() had nothing to consume and the handshake only recovered after the client's retransmit timer. The captured from_addr was also reused for every iteration of the -i loop. Replace it with a peek_peer_address() helper that uses MSG_PEEK to read the source address without consuming the datagram, and move the peek into the accept loop so the address is refreshed per connection. --- examples/server.py | 19 +++++++- tests/test_dtls_server_example.py | 73 +++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/test_dtls_server_example.py diff --git a/examples/server.py b/examples/server.py index 2f060af..1df90dd 100755 --- a/examples/server.py +++ b/examples/server.py @@ -115,6 +115,21 @@ def get_DTLSmethod(index): )[index] +# Large enough to peek a DTLS ClientHello source address. +PEEK_BUFSIZE = 1500 + + +def peek_peer_address(sock): + """ + Return the source address of the next pending datagram without removing + it from the socket queue. MSG_PEEK leaves the datagram (the DTLS + ClientHello) intact so wolfSSL_accept() can consume it during the + handshake. + """ + _, from_addr = sock.recvfrom(PEEK_BUFSIZE, socket.MSG_PEEK) + return from_addr + + def main(): args = build_arg_parser().parse_args() # DTLS connection over UDP @@ -124,7 +139,6 @@ def main(): args.v = 1 bind_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) bind_socket.bind(("" if args.b else "localhost", args.p)) - data, from_addr = bind_socket.recvfrom(1) context = wolfssl.SSLContext(get_DTLSmethod(args.v), server_side=True) # SSL/TLS connection over TCP else: @@ -156,6 +170,9 @@ def main(): try: secure_socket = None if args.u: + # Peek the client's address for this connection without + # consuming the ClientHello datagram needed by the handshake. + from_addr = peek_peer_address(bind_socket) secure_socket = context.wrap_socket(bind_socket) else: new_socket, from_addr = bind_socket.accept() diff --git a/tests/test_dtls_server_example.py b/tests/test_dtls_server_example.py new file mode 100644 index 0000000..5838c05 --- /dev/null +++ b/tests/test_dtls_server_example.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# test_dtls_server_example.py +# +# Copyright (C) 2006-2020 wolfSSL Inc. +# +# This file is part of wolfSSL. (formerly known as CyaSSL) +# +# wolfSSL is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# wolfSSL is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +# pylint: disable=missing-docstring, invalid-name, import-error + +import os +import sys +import socket + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "examples")) +import server as server_example # noqa: E402 + + +@pytest.fixture +def udp_pair(): + srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + srv.bind(("localhost", 0)) + cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + yield srv, cli, srv.getsockname() + finally: + srv.close() + cli.close() + + +def test_peek_peer_address_returns_source(udp_pair): + srv, cli, srv_addr = udp_pair + cli.bind(("localhost", 0)) + cli.sendto(b"clienthello-payload", srv_addr) + + addr = server_example.peek_peer_address(srv) + + assert addr == cli.getsockname() + + +def test_peek_peer_address_does_not_consume_datagram(udp_pair): + """ + Regression test for F-3481: peeking the client's address before the + DTLS handshake must leave the ClientHello datagram intact. The previous + example used recvfrom(1), which consumed the datagram and discarded + everything past the first byte, breaking the handshake. + """ + srv, cli, srv_addr = udp_pair + payload = b"X" * 256 # stand-in for a DTLS ClientHello record + cli.sendto(payload, srv_addr) + + server_example.peek_peer_address(srv) + + # The datagram must still be fully available for wolfSSL_accept(). + srv.settimeout(2) + data, _ = srv.recvfrom(4096) + assert data == payload From 41561e7ba61d71bc81f6ef70545f785341f29910 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 23 Jun 2026 11:24:10 +0000 Subject: [PATCH 2/8] Map WANT_READ from SSLSocket.write() to SSLWantReadError (F-3905) wolfSSL_write can return WOLFSSL_ERROR_WANT_READ (e.g. during a renegotiation that must read a record before progressing; secure renegotiation is enabled by default). write() only handled WANT_WRITE, so WANT_READ fell through to a generic SSLError and non-blocking callers tore the session down. Add a WANT_READ branch raising SSLWantReadError, matching do_handshake(). --- tests/test_io_error_mapping.py | 89 ++++++++++++++++++++++++++++++++++ wolfssl/__init__.py | 3 ++ 2 files changed, 92 insertions(+) create mode 100644 tests/test_io_error_mapping.py diff --git a/tests/test_io_error_mapping.py b/tests/test_io_error_mapping.py new file mode 100644 index 0000000..2932d1d --- /dev/null +++ b/tests/test_io_error_mapping.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# +# test_io_error_mapping.py +# +# Copyright (C) 2006-2020 wolfSSL Inc. +# +# This file is part of wolfSSL. (formerly known as CyaSSL) +# +# wolfSSL is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# wolfSSL is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +# pylint: disable=missing-docstring, invalid-name, import-error +# pylint: disable=protected-access + +""" +These tests exercise the error-code-to-exception mapping in SSLSocket's +read/write/recv_into. wolfSSL_write can return WANT_READ and wolfSSL_read can +return WANT_WRITE during a renegotiation; non-blocking callers rely on these +being surfaced as SSLWantReadError / SSLWantWriteError (matching the stdlib +ssl module) rather than a generic SSLError. + +The renegotiation conditions are awkward to force over a real socket, so the +native wolfSSL_read/wolfSSL_write/wolfSSL_get_error functions are stubbed to +return the relevant codes and the Python-level mapping is verified directly. +""" + +from types import SimpleNamespace + +import pytest +import wolfssl + + +class _FakeLib: + """Stand-in for wolfssl._lib that forces a given I/O return / error.""" + + def __init__(self, io_ret, err): + self._io_ret = io_ret + self._err = err + + def wolfSSL_write(self, ssl, data, length): + return self._io_ret + + def wolfSSL_read(self, ssl, data, length): + return self._io_ret + + def wolfSSL_get_error(self, ssl, ret): + return self._err + + +def _make_socket(): + """A minimal, non-DTLS SSLSocket that skips __init__/native setup.""" + sock = wolfssl.SSLSocket.__new__(wolfssl.SSLSocket) + sock.native_object = object() # non-NULL so _check_closed passes + sock._connected = True # so _check_connected is a no-op + sock._context = SimpleNamespace(protocol=wolfssl.PROTOCOL_TLS) + # The dummy native_object isn't a real cdata pointer, so make __del__ + # a no-op to avoid wolfSSL_free() choking on it during GC. + sock._release_native_object = lambda: None + return sock + + +def _patch_lib(monkeypatch, io_ret, err): + monkeypatch.setattr(wolfssl, "_lib", _FakeLib(io_ret, err)) + + +def test_write_want_read_raises_wantread(monkeypatch): + """F-3905: wolfSSL_write returning WANT_READ -> SSLWantReadError.""" + _patch_lib(monkeypatch, -1, wolfssl._SSL_ERROR_WANT_READ) + sock = _make_socket() + with pytest.raises(wolfssl.SSLWantReadError): + sock.write(b"data") + + +def test_write_want_write_still_raises_wantwrite(monkeypatch): + _patch_lib(monkeypatch, -1, wolfssl._SSL_ERROR_WANT_WRITE) + sock = _make_socket() + with pytest.raises(wolfssl.SSLWantWriteError): + sock.write(b"data") diff --git a/wolfssl/__init__.py b/wolfssl/__init__.py index 7a739c3..c1754fb 100644 --- a/wolfssl/__init__.py +++ b/wolfssl/__init__.py @@ -590,6 +590,9 @@ def write(self, data): self.native_object, 0) if err == _SSL_ERROR_WANT_WRITE: raise SSLWantWriteError() + elif err == _SSL_ERROR_WANT_READ: + # wolfSSL_write can require a read first (e.g. renegotiation). + raise SSLWantReadError() else: raise SSLError( "wolfSSL_write error (%d)" % err) From d0bb56e6f941f6966f7a82fe15841a74d4cc86e4 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 23 Jun 2026 11:24:40 +0000 Subject: [PATCH 3/8] Map WANT_WRITE from SSLSocket.read() to SSLWantWriteError (F-3906) wolfSSL_read can return WOLFSSL_ERROR_WANT_WRITE when the SSL layer must flush a handshake record (e.g. renegotiation) before returning data. read() only handled WANT_READ, raising a generic SSLError otherwise, which stops non-blocking callers from select()-ing on writability. Add a WANT_WRITE branch raising SSLWantWriteError. --- tests/test_io_error_mapping.py | 15 +++++++++++++++ wolfssl/__init__.py | 3 +++ 2 files changed, 18 insertions(+) diff --git a/tests/test_io_error_mapping.py b/tests/test_io_error_mapping.py index 2932d1d..6ebd16d 100644 --- a/tests/test_io_error_mapping.py +++ b/tests/test_io_error_mapping.py @@ -87,3 +87,18 @@ def test_write_want_write_still_raises_wantwrite(monkeypatch): sock = _make_socket() with pytest.raises(wolfssl.SSLWantWriteError): sock.write(b"data") + + +def test_read_want_write_raises_wantwrite(monkeypatch): + """F-3906: wolfSSL_read returning WANT_WRITE -> SSLWantWriteError.""" + _patch_lib(monkeypatch, -1, wolfssl._SSL_ERROR_WANT_WRITE) + sock = _make_socket() + with pytest.raises(wolfssl.SSLWantWriteError): + sock.read(16) + + +def test_read_want_read_still_raises_wantread(monkeypatch): + _patch_lib(monkeypatch, -1, wolfssl._SSL_ERROR_WANT_READ) + sock = _make_socket() + with pytest.raises(wolfssl.SSLWantReadError): + sock.read(16) diff --git a/wolfssl/__init__.py b/wolfssl/__init__.py index c1754fb..d1c0807 100644 --- a/wolfssl/__init__.py +++ b/wolfssl/__init__.py @@ -658,6 +658,9 @@ def read(self, length=1024, buffer=None): err = _lib.wolfSSL_get_error(self.native_object, 0) if err == _SSL_ERROR_WANT_READ: raise SSLWantReadError() + elif err == _SSL_ERROR_WANT_WRITE: + # wolfSSL_read can require a write first (e.g. renegotiation). + raise SSLWantWriteError() else: raise SSLError("wolfSSL_read error (%d)" % err) From 65aaef9750a3262293354dcf5759894cd53446e8 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 23 Jun 2026 11:25:08 +0000 Subject: [PATCH 4/8] Map WANT_WRITE from SSLSocket.recv_into() to SSLWantWriteError (F-3907) recv_into() shares read()'s error-mapping pattern and inherited the same omission: wolfSSL_read returning WOLFSSL_ERROR_WANT_WRITE (during a renegotiation needing a write) was reported as a generic SSLError instead of SSLWantWriteError, breaking non-blocking callers that distinguish readiness directions. Add the WANT_WRITE branch. --- tests/test_io_error_mapping.py | 15 +++++++++++++++ wolfssl/__init__.py | 3 +++ 2 files changed, 18 insertions(+) diff --git a/tests/test_io_error_mapping.py b/tests/test_io_error_mapping.py index 6ebd16d..eec6540 100644 --- a/tests/test_io_error_mapping.py +++ b/tests/test_io_error_mapping.py @@ -102,3 +102,18 @@ def test_read_want_read_still_raises_wantread(monkeypatch): sock = _make_socket() with pytest.raises(wolfssl.SSLWantReadError): sock.read(16) + + +def test_recv_into_want_write_raises_wantwrite(monkeypatch): + """F-3907: wolfSSL_read in recv_into returning WANT_WRITE.""" + _patch_lib(monkeypatch, -1, wolfssl._SSL_ERROR_WANT_WRITE) + sock = _make_socket() + with pytest.raises(wolfssl.SSLWantWriteError): + sock.recv_into(bytearray(16)) + + +def test_recv_into_want_read_still_raises_wantread(monkeypatch): + _patch_lib(monkeypatch, -1, wolfssl._SSL_ERROR_WANT_READ) + sock = _make_socket() + with pytest.raises(wolfssl.SSLWantReadError): + sock.recv_into(bytearray(16)) diff --git a/wolfssl/__init__.py b/wolfssl/__init__.py index d1c0807..5f06397 100644 --- a/wolfssl/__init__.py +++ b/wolfssl/__init__.py @@ -702,6 +702,9 @@ def recv_into(self, buffer, nbytes=None, flags=0): err = _lib.wolfSSL_get_error(self.native_object, 0) if err == _SSL_ERROR_WANT_READ: raise SSLWantReadError() + elif err == _SSL_ERROR_WANT_WRITE: + # wolfSSL_read can require a write first (e.g. renegotiation). + raise SSLWantWriteError() else: raise SSLError("wolfSSL_read error (%d)" % err) From 99a441677191e2f1ffde626e387755d8e81f5166 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 23 Jun 2026 11:33:22 +0000 Subject: [PATCH 5/8] Drive DTLS handshake only until complete in I/O methods (F-4136) For DTLS, write()/read()/recv_into() called do_handshake() on every call. do_handshake() runs wolfSSL_accept/connect, which on a non-blocking socket can raise SSLWantReadError and abort an I/O long after the handshake finished, and made DTLS write-side behaviour inconsistent with TCP. Track completion with a _handshake_complete flag set on a successful do_handshake(), and only drive the handshake from I/O methods while that flag is False. --- tests/test_dtls_handshake_once.py | 105 ++++++++++++++++++++++++++++++ wolfssl/__init__.py | 17 +++-- 2 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 tests/test_dtls_handshake_once.py diff --git a/tests/test_dtls_handshake_once.py b/tests/test_dtls_handshake_once.py new file mode 100644 index 0000000..d53c787 --- /dev/null +++ b/tests/test_dtls_handshake_once.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# +# test_dtls_handshake_once.py +# +# Copyright (C) 2006-2020 wolfSSL Inc. +# +# This file is part of wolfSSL. (formerly known as CyaSSL) +# +# wolfSSL is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# wolfSSL is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +# pylint: disable=missing-docstring, invalid-name, import-error +# pylint: disable=protected-access + +""" +F-4136: for DTLS, write()/read()/recv_into() used to call do_handshake() on +every single call. Once the handshake has completed, re-running it is wasteful +and, on a non-blocking socket, wolfSSL_accept/connect can raise +SSLWantReadError and abort an otherwise valid I/O. The handshake must only be +driven until it completes. +""" + +from types import SimpleNamespace + +import pytest +import wolfssl + + +class _OkLib: + """_lib stub whose I/O calls always succeed.""" + + def wolfSSL_write(self, ssl, data, length): + return length + + def wolfSSL_read(self, ssl, data, length): + return length + + def wolfSSL_get_error(self, ssl, ret): # pragma: no cover + return 0 + + +def _make_dtls_socket(handshake_complete): + sock = wolfssl.SSLSocket.__new__(wolfssl.SSLSocket) + sock.native_object = object() + sock._connected = True + sock._server_side = True + sock._context = SimpleNamespace(protocol=wolfssl.PROTOCOL_DTLSv1_2) + sock._handshake_complete = handshake_complete + sock._release_native_object = lambda: None + return sock + + +@pytest.fixture +def spy_handshake(monkeypatch): + monkeypatch.setattr(wolfssl, "_lib", _OkLib()) + calls = [] + + def _record(sock): + calls.append(True) + sock._handshake_complete = True + + return calls, _record + + +@pytest.mark.parametrize("op", ["write", "read", "recv_into"]) +def test_dtls_io_does_not_redrive_completed_handshake(spy_handshake, op): + calls, record = spy_handshake + sock = _make_dtls_socket(handshake_complete=True) + sock.do_handshake = lambda block=False: record(sock) + + if op == "write": + sock.write(b"payload") + elif op == "read": + sock.read(8) + else: + sock.recv_into(bytearray(8)) + + assert calls == [], "do_handshake() must not run once the handshake is done" + + +@pytest.mark.parametrize("op", ["write", "read", "recv_into"]) +def test_dtls_io_drives_handshake_until_complete(spy_handshake, op): + calls, record = spy_handshake + sock = _make_dtls_socket(handshake_complete=False) + sock.do_handshake = lambda block=False: record(sock) + + if op == "write": + sock.write(b"payload") + elif op == "read": + sock.read(8) + else: + sock.recv_into(bytearray(8)) + + assert calls == [True], "first DTLS I/O must drive the handshake once" diff --git a/wolfssl/__init__.py b/wolfssl/__init__.py index 5f06397..5b8e535 100644 --- a/wolfssl/__init__.py +++ b/wolfssl/__init__.py @@ -460,6 +460,9 @@ def __init__(self, sock=None, keyfile=None, certfile=None, self._closed = False self._connected = connected + # Tracks whether the (DTLS) handshake has completed so I/O methods + # don't re-drive it on every call. + self._handshake_complete = False # create the SSL object self.native_object = _lib.wolfSSL_new(self.context.native_object) @@ -577,8 +580,8 @@ def write(self, data): # Check connected if not DTLS if self._context.protocol < PROTOCOL_DTLSv1: self._check_connected() - # Complete handshake if DTLS connection - else: + # Drive the DTLS handshake only until it has completed. + elif not self._handshake_complete: self.do_handshake() data = t2b(data) @@ -643,8 +646,8 @@ def read(self, length=1024, buffer=None): # Check connected if not DTLS if self._context.protocol < PROTOCOL_DTLSv1: self._check_connected() - # Complete handshake if DTLS connection - else: + # Drive the DTLS handshake only until it has completed. + elif not self._handshake_complete: self.do_handshake() if buffer is not None: @@ -681,7 +684,8 @@ def recv_into(self, buffer, nbytes=None, flags=0): self._check_closed("read") if self._context.protocol < PROTOCOL_DTLSv1: self._check_connected() - else: + # Drive the DTLS handshake only until it has completed. + elif not self._handshake_complete: self.do_handshake() if buffer is None: @@ -826,6 +830,9 @@ def do_handshake(self, block=False): # pylint: disable=unused-argument raise SSLError("do_handshake failed with error %d: %s" % (err, eStr)) + # Reached only on success (every failure path above raises). + self._handshake_complete = True + def _real_connect(self, addr, connect_ex): if self._server_side: raise ValueError("can't connect in server-side mode") From 9c26572a417d7b0737053194f52b668e8d355e2b Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 23 Jun 2026 12:46:11 +0000 Subject: [PATCH 6/8] Enable hostname verification in client example (F-5621) The client example set CERT_REQUIRED and loaded CA roots but never set check_hostname or passed server_hostname to wrap_socket, so wolfSSL validated the chain to a trusted CA without binding the certificate to the requested host. A peer presenting any CA-trusted certificate for a different hostname would be accepted by anyone reusing this as a secure client template. Make verification configure hostname checking by default (via a new configure_verification helper) and add a -n flag to opt out explicitly for IP literals or test certificates. --- examples/client.py | 42 +++++++++++++++--- tests/test_client_example.py | 85 ++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 tests/test_client_example.py diff --git a/examples/client.py b/examples/client.py index d30f351..fccccdc 100755 --- a/examples/client.py +++ b/examples/client.py @@ -89,6 +89,12 @@ def build_arg_parser(): help="Disable client cert check" ) + parser.add_argument( + "-n", action="store_true", + help="Disable server hostname check " + "(needed for IP literals or test certificates)" + ) + parser.add_argument( "-g", action="store_true", help="Send server HTTP GET" @@ -125,6 +131,33 @@ def get_DTLSmethod(index): wolfssl.PROTOCOL_DTLSv1_3 )[index] +def configure_verification(context, args): + """ + Configure peer certificate and hostname verification on the context + according to the parsed arguments. Returns the server_hostname to pass + to wrap_socket() (None when no hostname check should be performed). + + When certificate verification is enabled (the default), hostname + verification is enabled too so that a CA-trusted certificate issued for + a different host is rejected. Pass -n to opt out explicitly (e.g. when + connecting to an IP literal or using test certificates). + """ + if args.d: + context.verify_mode = wolfssl.CERT_NONE + context.check_hostname = False + return None + + context.verify_mode = wolfssl.CERT_REQUIRED + context.load_verify_locations(args.A) + + if args.n: + context.check_hostname = False + return None + + context.check_hostname = True + return args.h + + def main(): args = build_arg_parser().parse_args() @@ -147,18 +180,15 @@ def main(): context.load_cert_chain(args.c, args.k) - if args.d: - context.verify_mode = wolfssl.CERT_NONE - else: - context.verify_mode = wolfssl.CERT_REQUIRED - context.load_verify_locations(args.A) + server_hostname = configure_verification(context, args) if args.l: context.set_ciphers(args.l) secure_socket = None try: - secure_socket = context.wrap_socket(bind_socket) + secure_socket = context.wrap_socket( + bind_socket, server_hostname=server_hostname) if not args.C: secure_socket.enable_crl(1) diff --git a/tests/test_client_example.py b/tests/test_client_example.py new file mode 100644 index 0000000..74c82db --- /dev/null +++ b/tests/test_client_example.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# +# test_client_example.py +# +# Copyright (C) 2006-2020 wolfSSL Inc. +# +# This file is part of wolfSSL. (formerly known as CyaSSL) +# +# wolfSSL is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# wolfSSL is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +# pylint: disable=missing-docstring, invalid-name, import-error + +import os +import sys + +import wolfssl + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "examples")) +import client as client_example # noqa: E402 + + +def _args(argv): + return client_example.build_arg_parser().parse_args(argv) + + +def _ctx(): + return wolfssl.SSLContext(wolfssl.PROTOCOL_TLSv1_2) + + +def test_verification_enables_hostname_check_by_default(): + """ + F-5621: with cert verification on (the default), the client must also + verify the peer's hostname and pass server_hostname to wrap_socket. + """ + args = _args(["-h", "example.com"]) + ctx = _ctx() + + server_hostname = client_example.configure_verification(ctx, args) + + assert ctx.verify_mode == wolfssl.CERT_REQUIRED + assert ctx.check_hostname is True + assert server_hostname == "example.com" + + +def test_disable_cert_check_skips_hostname(): + args = _args(["-d"]) + ctx = _ctx() + # Simulate a reused context that previously had hostname checking on: + # -d must clear it, not leave it dangling against CERT_NONE. + ctx.verify_mode = wolfssl.CERT_REQUIRED + ctx.check_hostname = True + + server_hostname = client_example.configure_verification(ctx, args) + + assert ctx.verify_mode == wolfssl.CERT_NONE + assert ctx.check_hostname is False + assert server_hostname is None + + +def test_hostname_check_can_be_opted_out(): + """An explicit opt-out is provided for IP literals / test certs.""" + args = _args(["-n"]) + ctx = _ctx() + # Reused context with hostname checking previously enabled: -n must + # actively turn it back off. + ctx.verify_mode = wolfssl.CERT_REQUIRED + ctx.check_hostname = True + + server_hostname = client_example.configure_verification(ctx, args) + + assert ctx.verify_mode == wolfssl.CERT_REQUIRED + assert ctx.check_hostname is False + assert server_hostname is None From 3dd1b902e1a19c10525bf277f0b9438d448b7706 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 23 Jun 2026 12:49:01 +0000 Subject: [PATCH 7/8] Send bytes-like data verbatim in SSLSocket.write (F-5622) write() converted data with t2b(), which str()-encodes anything that is not already bytes. Valid bytes-like inputs such as bytearray and memoryview were transmitted as their Python repr ("bytearray(b'...')", "") instead of their contents, corrupting the stream. Convert via the buffer protocol (bytes(memoryview(data))) and raise TypeError for objects that are not bytes-like, matching the stdlib ssl module. --- tests/test_write_bytes.py | 84 +++++++++++++++++++++++++++++++++++++++ wolfssl/__init__.py | 10 ++++- 2 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 tests/test_write_bytes.py diff --git a/tests/test_write_bytes.py b/tests/test_write_bytes.py new file mode 100644 index 0000000..9d9b7ca --- /dev/null +++ b/tests/test_write_bytes.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# +# test_write_bytes.py +# +# Copyright (C) 2006-2020 wolfSSL Inc. +# +# This file is part of wolfSSL. (formerly known as CyaSSL) +# +# wolfSSL is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# wolfSSL is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +# pylint: disable=missing-docstring, invalid-name, import-error +# pylint: disable=protected-access + +""" +F-5622: SSLSocket.write() ran data through t2b(), which str()-encodes anything +that is not already bytes. Valid bytes-like inputs (bytearray, memoryview) +were therefore serialized as their Python repr ("bytearray(b'...')", +"") instead of their actual contents. +""" + +from types import SimpleNamespace + +import wolfssl + + +class _CaptureLib: + def __init__(self): + self.written = None + + def wolfSSL_write(self, ssl, data, length): + self.written = bytes(data[:length]) + return length + + def wolfSSL_get_error(self, ssl, ret): # pragma: no cover + return 0 + + +def _make_socket(monkeypatch): + lib = _CaptureLib() + monkeypatch.setattr(wolfssl, "_lib", lib) + sock = wolfssl.SSLSocket.__new__(wolfssl.SSLSocket) + sock.native_object = object() + sock._connected = True + sock._context = SimpleNamespace(protocol=wolfssl.PROTOCOL_TLS) + sock._release_native_object = lambda: None + return sock, lib + + +def test_write_bytes_unchanged(monkeypatch): + sock, lib = _make_socket(monkeypatch) + sock.write(b"hello") + assert lib.written == b"hello" + + +def test_write_bytearray_sends_contents(monkeypatch): + sock, lib = _make_socket(monkeypatch) + sock.write(bytearray(b"hello")) + assert lib.written == b"hello" + + +def test_write_memoryview_sends_contents(monkeypatch): + sock, lib = _make_socket(monkeypatch) + sock.write(memoryview(b"hello")) + assert lib.written == b"hello" + + +def test_write_str_is_utf8_encoded(monkeypatch): + # Backward compatibility: str is UTF-8 encoded (historical t2b() + # behavior), not rejected. + sock, lib = _make_socket(monkeypatch) + sock.write("héllo") + assert lib.written == "héllo".encode("utf-8") diff --git a/wolfssl/__init__.py b/wolfssl/__init__.py index 5b8e535..cc8a6d0 100644 --- a/wolfssl/__init__.py +++ b/wolfssl/__init__.py @@ -577,14 +577,20 @@ def write(self, data): Returns number of bytes of DATA actually transmitted. """ self._check_closed("write") - # Check connected if not DTLS + # Check connected if not DTLS if self._context.protocol < PROTOCOL_DTLSv1: self._check_connected() # Drive the DTLS handshake only until it has completed. elif not self._handshake_complete: self.do_handshake() - data = t2b(data) + # Send bytes-like objects verbatim; fall back to t2b() for other + # types (e.g. str) to preserve backward compatibility. + if not isinstance(data, bytes): + try: + data = bytes(memoryview(data)) + except TypeError: + data = t2b(data) ret = _lib.wolfSSL_write( self.native_object, data, len(data)) From 93954c9430a7aa15bce57956af17e2a38df7b1ac Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 23 Jun 2026 12:51:31 +0000 Subject: [PATCH 8/8] Return None from getpeercert when peer has no certificate (F-5623) get_peer_x509() checked only whether the session was NULL and then built a WolfSSLX509, whose __init__ called wolfSSL_get_peer_certificate() and raised SSLError on NULL. On a valid connection where the peer presented no certificate (e.g. a server not requesting a client cert), this raised instead of returning None as the stdlib ssl getpeercert() contract requires. Fetch the certificate in get_peer_x509(), return None when it is NULL, and have WolfSSLX509 wrap the already-obtained pointer. --- tests/test_getpeercert.py | 116 ++++++++++++++++++++++++++++++++++++++ wolfssl/__init__.py | 20 +++++-- 2 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 tests/test_getpeercert.py diff --git a/tests/test_getpeercert.py b/tests/test_getpeercert.py new file mode 100644 index 0000000..9e6007e --- /dev/null +++ b/tests/test_getpeercert.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# +# test_getpeercert.py +# +# Copyright (C) 2006-2020 wolfSSL Inc. +# +# This file is part of wolfSSL. (formerly known as CyaSSL) +# +# wolfSSL is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# wolfSSL is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +# pylint: disable=missing-docstring, invalid-name, import-error + +import socket +from contextlib import contextmanager +from threading import Thread + +import wolfssl + + +@contextmanager +def _client_server_session(): + """ + Establish a real TLS connection to a local server that does NOT request a + client certificate. Yields (client_socket, server_result); server_result + is populated (after the block exits) with the server's view of the peer: + {"x509", "cert"} on success or {"error"} if a call raised. + """ + result = {} + + server_ctx = wolfssl.SSLContext(wolfssl.PROTOCOL_TLS, server_side=True) + server_ctx.verify_mode = wolfssl.CERT_NONE + server_ctx.load_cert_chain("certs/server-cert.pem", "certs/server-key.pem") + + listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + listener.bind(("localhost", 0)) + listener.listen(1) + port = listener.getsockname()[1] + + def serve(): + conn, _ = listener.accept() + ssock = server_ctx.wrap_socket(conn, server_side=True) + try: + ssock.read(1024) + # Client sent no certificate: these must not raise. + result["x509"] = ssock.get_peer_x509() + result["cert"] = ssock.getpeercert() + ssock.write(b"ok") + except Exception as exc: # pylint: disable=broad-except + result["error"] = exc + finally: + ssock.close() + + server_thread = Thread(target=serve, daemon=True) + server_thread.start() + + client_ctx = wolfssl.SSLContext(wolfssl.PROTOCOL_TLS) + client_ctx.verify_mode = wolfssl.CERT_NONE + client = client_ctx.wrap_socket( + socket.socket(socket.AF_INET, socket.SOCK_STREAM)) + client.connect(("localhost", port)) + client.write(b"hi") + try: + yield client, result + finally: + try: + client.read(1024) + except Exception: # pylint: disable=broad-except + pass + client.close() + server_thread.join(timeout=10) + listener.close() + + +def test_getpeercert_returns_none_without_peer_cert(): + """ + F-5623: on a valid TLS connection where the peer presented no + certificate (here, a server that does not request a client cert), + getpeercert()/get_peer_x509() must return None instead of raising. + """ + with _client_server_session() as (client, result): + # The peer (server) always presents a certificate. + server_cert = client.getpeercert() + + assert "error" not in result, "getpeercert raised: %r" % result.get("error") + assert result["x509"] is None + assert result["cert"] is None + # Positive path: the server's certificate is still returned to the client. + assert server_cert is not None + + +def test_wolfsslx509_accepts_session_for_backward_compat(): + """ + WolfSSLX509 historically accepted a WOLFSSL* session and fetched the peer + certificate itself. That constructor form must keep working alongside the + new WOLFSSL_X509* form used by get_peer_x509(). + """ + with _client_server_session() as (client, _result): + from_session = wolfssl.WolfSSLX509(client.native_object) + from_helper = client.get_peer_x509() + + # Both forms resolve to the same server certificate. + assert from_session.get_subject_cn() != "" + assert from_session.get_subject_cn() == from_helper.get_subject_cn() diff --git a/wolfssl/__init__.py b/wolfssl/__init__.py index cc8a6d0..a2ee519 100644 --- a/wolfssl/__init__.py +++ b/wolfssl/__init__.py @@ -97,7 +97,15 @@ class WolfSSLX509(object): """ def __init__(self, session): - self.native_object = _lib.wolfSSL_get_peer_certificate(session) + # `session` kept as the original public parameter name. Accept a + # WOLFSSL* session (fetch the peer cert here) or an already-obtained + # WOLFSSL_X509* (used by SSLSocket.get_peer_x509()). + if _ffi.typeof(session).cname == "WOLFSSL *": + x509 = _lib.wolfSSL_get_peer_certificate(session) + else: + x509 = session + + self.native_object = x509 if self.native_object == _ffi.NULL: raise SSLError("Unable to get internal WOLFSSL_X509 from wolfSSL") @@ -899,13 +907,17 @@ def accept(self): def get_peer_x509(self): """ - Returns WolfSSLX509 object representing the peer's certificate, - after making a successful SSL/TLS connection. + Returns a WolfSSLX509 object representing the peer's certificate, + or None if the peer did not present one (or there is no session). """ if self.native_object == _ffi.NULL: return None - return WolfSSLX509(self.native_object) + x509 = _lib.wolfSSL_get_peer_certificate(self.native_object) + if x509 == _ffi.NULL: + return None + + return WolfSSLX509(x509) def getpeercert(self, binary_form=False): """