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/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_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 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/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 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/tests/test_io_error_mapping.py b/tests/test_io_error_mapping.py new file mode 100644 index 0000000..eec6540 --- /dev/null +++ b/tests/test_io_error_mapping.py @@ -0,0 +1,119 @@ +# -*- 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") + + +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) + + +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/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 7a739c3..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") @@ -460,6 +468,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) @@ -574,14 +585,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() - # 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) + # 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)) @@ -590,6 +607,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) @@ -640,8 +660,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: @@ -655,6 +675,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) @@ -675,7 +698,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: @@ -696,6 +720,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) @@ -817,6 +844,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") @@ -877,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): """