From 9c26572a417d7b0737053194f52b668e8d355e2b Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 23 Jun 2026 12:46:11 +0000 Subject: [PATCH] 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