Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions manpages/wolfssl-s_client.1
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
.SH NAME
wolfssl-s_client, s_client \- basic TLS client for testing connections
.SH SYNOPSIS
wolfssl s_client -connect <host>:<port> [-starttls proto] [-CAfile file] [-verify_return_error] [-disable_stdin_check] [-noservername] [-help]
wolfssl s_client -connect <host>:<port> [-starttls proto] [-CAfile file] [-verify_return_error] [-verify_hostname name] [-verify_ip ip] [-disable_stdin_check] [-noservername] [-help]
.SH DESCRIPTION
Opens a TLS connection to a server for testing. Server Name Indication is
sent by default. Certificate verification is disabled by default and can be
enabled with -verify_return_error.
sent by default. Certificate verification is off by default. When enabled
it checks the certificate chain, not that the certificate was issued for
the host being connected to; that is asked for separately.
.SH OPTIONS
-connect host:port address and port to connect to. IPv6 addresses use
.br
Expand All @@ -20,14 +21,24 @@ enabled with -verify_return_error.
.LP
-CAfile file CA certificate file. Has no effect on whether the
.br
connection succeeds or fails unless
connection succeeds or fails unless certificate
.br
\-verify_return_error is also given (see NOTES).
verification is enabled (see NOTES).
.br
.LP
-verify_return_error close the connection on a verification error.
.br
.LP
-verify_hostname name check the peer certificate against the DNS name
.br
\fIname\fR.
.br
.LP
-verify_ip ip check the peer certificate against the IP address
.br
\fIip\fR.
.br
.LP
-disable_stdin_check do not wait for or read input on stdin; useful when
.br
scripting the client.
Expand All @@ -48,6 +59,16 @@ Connect and verify the server's certificate:
wolfssl s_client -connect example.com:443 -CAfile ca-cert.pem -verify_return_error
.RE
.LP
Connect and require the certificate to be issued for the host:
.RS
wolfssl s_client -connect example.com:443 -CAfile ca-cert.pem -verify_hostname example.com
.RE
.LP
Connect and require the certificate to carry the address dialed:
.RS
wolfssl s_client -connect 127.0.0.1:11111 -CAfile ca-cert.pem -verify_ip 127.0.0.1
.RE
.LP
Connect to an SMTP server using STARTTLS:
.RS
wolfssl s_client -connect mail.example.com:25 -starttls smtp
Expand All @@ -59,8 +80,9 @@ Available only when wolfSSL is built with filesystem support.
.LP
The client negotiates the highest TLS version both peers support. CRL
checking is not performed. The server certificate is only verified when
\-verify_return_error is given; without it the connection proceeds
unverified (with a warning) even if \-CAfile is supplied.
\-verify_return_error, \-verify_hostname or \-verify_ip is given; each of
them enables verification on its own. Without one of them the connection
proceeds unverified (with a warning) even if \-CAfile is supplied.
.SH BUGS
No known bugs at this time.
.SH AUTHOR
Expand Down
34 changes: 32 additions & 2 deletions src/client/client.c
Original file line number Diff line number Diff line change
Expand Up @@ -2181,6 +2181,8 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args)
because can't tell if we're really
going there to detect old chacha-poly
*/
const char* checkDomain = NULL;
const char* checkIpAddr = NULL;
#ifndef WOLFSSL_VXWORKS
int ch;
static const struct mygetopt_long_config long_options[] = {
Expand All @@ -2194,6 +2196,8 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args)
{ "pqc", 1, 259 },
#endif
{ "disable_stdin_check", 0, 260 },
{ "verify_hostname", 1, 300 },
{ "verify_ip", 1, 301 },
{ 0, 0, 0 }
};
#endif
Expand All @@ -2214,6 +2218,7 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args)
int dtlsSCTP = 0;
int doMcast = 0;
int matchName = 0;
int matchIpAddr = 0;
int doPeerCheck = 1;
int nonBlocking = 0;
int simulateWantWrite = 0;
Expand Down Expand Up @@ -2404,6 +2409,16 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args)
disable_stdin_chk = 1;
break;

case 300 :
matchName = 1;
checkDomain = myoptarg;
break;

case 301 :
matchIpAddr = 1;
checkIpAddr = myoptarg;
break;

case 'g' :
sendGET = 1;
break;
Expand Down Expand Up @@ -3877,8 +3892,23 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args)
SetupAtomicUser(ctx, ssl);
#endif

if (matchName && doPeerCheck)
wolfSSL_check_domain_name(ssl, domain);
if (matchName && doPeerCheck) {
if (wolfSSL_check_domain_name(ssl,
checkDomain != NULL ? checkDomain : domain)
!= WOLFSSL_SUCCESS) {
wolfSSL_free(ssl); ssl = NULL;
wolfSSL_CTX_free(ctx); ctx = NULL;
err_sys("can't set domain name to check");
}
}

if (matchIpAddr && doPeerCheck) {
if (wolfSSL_check_ip_address(ssl, checkIpAddr) != WOLFSSL_SUCCESS) {
wolfSSL_free(ssl); ssl = NULL;
wolfSSL_CTX_free(ctx); ctx = NULL;
err_sys("can't set IP address to check");
}
}
#ifndef WOLFSSL_CALLBACKS
if (nonBlocking) {
#ifdef WOLFSSL_DTLS
Expand Down
38 changes: 37 additions & 1 deletion src/client/clu_client_setup.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ static const struct option client_options[] = {
{"-verify_return_error", no_argument, 0, WOLFCLU_VERIFY_RETURN_ERROR},
{"-disable_stdin_check", no_argument, 0, WOLFCLU_DISABLE_STDINCHK },
{"-noservername", no_argument, 0, WOLFCLU_NOSERVERNAME },
{"-verify_hostname", required_argument, 0, WOLFCLU_VERIFY_HOSTNAME },
{"-verify_ip", required_argument, 0, WOLFCLU_VERIFY_IP },
{"-help", no_argument, 0, WOLFCLU_HELP },
{"-h", no_argument, 0, WOLFCLU_HELP },

Expand All @@ -58,6 +60,11 @@ static void wolfCLU_ClientHelp(void)
WOLFCLU_LOG(WOLFCLU_L0, "\t-verify_return_error close connection on verification error");
WOLFCLU_LOG(WOLFCLU_L0, "\t-disable_stdin_check ");
WOLFCLU_LOG(WOLFCLU_L0, "\t-noservername do not send Server Name Indication");
WOLFCLU_LOG(WOLFCLU_L0, "\t-verify_hostname <name> check the peer certificate"
" against <name>");
WOLFCLU_LOG(WOLFCLU_L0, "\t-verify_ip <ip> check the peer certificate against"
" <ip>");
WOLFCLU_LOG(WOLFCLU_L0, "\t\teither one turns on peer verification");
}

static const char hostFlag[] = "-h";
Expand All @@ -69,6 +76,8 @@ static const char noClientCert[] = "-x";
static const char startTLSFlag[] = "-M";
static const char disableCRLFlag[] = "-C";
static const char sniFlag[] = "-S";
static const char verifyHostFlag[] = "--verify_hostname";
static const char verifyIpFlag[] = "--verify_ip";

int myoptind = 0;
char* myoptarg = NULL;
Expand Down Expand Up @@ -105,6 +114,8 @@ int wolfCLU_Client(int argc, char** argv)
int verify = 0;
int noservername = 0;
char* ipv6 = NULL;
char* verifyHost = NULL;
char* verifyIp = NULL;

int clientArgc = 0;
const char* clientArgv[MAX_CLIENT_ARGS];
Expand Down Expand Up @@ -236,6 +247,14 @@ int wolfCLU_Client(int argc, char** argv)
noservername = 1;
break;

case WOLFCLU_VERIFY_HOSTNAME:
verifyHost = optarg;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-verify_hostname/-verify_ip silently ignored when optarg is NULL · Logic errors

wolfCLU_GetOpt leaves optarg NULL when a required_argument option is the last token (clu_funcs.c:1295-1300). Storing it unchecked makes the later verifyHost != NULL / verifyIp != NULL blocks skip, so verify stays 0, -d is emitted, and the run proceeds unverified while printing that no verify option was given. Adjacent to known finding #8069, which is the NULL-optarg crash in the -connect handler of this function; this branch does not crash and instead disables the requested check.

Related known finding #8069 (similar but distinct): Both are in wolfCLU_Client and stem from required-option optarg being NULL. #8069 dereferences optarg in the -connect handler via XSTRSTR and crashes; this finding assigns optarg in the -verify_hostname/-verify_ip handlers, causing verification to be skipped. Different switch cases and separate validation patches are required.

Fix: Reject with WOLFCLU_FATAL_ERROR and a usage message when optarg is NULL in the WOLFCLU_VERIFY_HOSTNAME and WOLFCLU_VERIFY_IP cases.

break;

case WOLFCLU_VERIFY_IP:
verifyIp = optarg;
break;

case ARG_FOUND_TWICE:
wolfCLU_LogError("Found duplicate argument");
return WOLFCLU_FATAL_ERROR;
Expand Down Expand Up @@ -265,10 +284,27 @@ int wolfCLU_Client(int argc, char** argv)
}
}

if (ret == WOLFCLU_SUCCESS && verifyHost != NULL) {
verify = 1;
ret = _addClientArg(clientArgv, verifyHostFlag, &clientArgc);
if (ret == WOLFCLU_SUCCESS) {
ret = _addClientArg(clientArgv, verifyHost, &clientArgc);
}
}

if (ret == WOLFCLU_SUCCESS && verifyIp != NULL) {
verify = 1;
ret = _addClientArg(clientArgv, verifyIpFlag, &clientArgc);
if (ret == WOLFCLU_SUCCESS) {
ret = _addClientArg(clientArgv, verifyIp, &clientArgc);
}
}

if (ret == WOLFCLU_SUCCESS && !verify) {
ret = _addClientArg(clientArgv, noVerifyFlag, &clientArgc);

WOLFCLU_LOG(WOLFCLU_L0, "\nWarning: -verify_return_error not specified."
WOLFCLU_LOG(WOLFCLU_L0, "\nWarning: none of -verify_return_error,"
" -verify_hostname or -verify_ip specified."
" Defaulting to NOT verifying peer.");
}

Expand Down
4 changes: 4 additions & 0 deletions tests/client/client-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ def test_client_help(self):
r = run_wolfssl("s_client", "-help")
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn("s_client" , r.stderr, "help menu was not printed")
self.assertIn("-verify_hostname", r.stderr,
"-verify_hostname missing from help menu")
self.assertIn("-verify_ip", r.stderr,
"-verify_ip missing from help menu")

class ShellInjectionTest(unittest.TestCase):
"""Regression tests for shell command injection via hostname.
Expand Down
128 changes: 128 additions & 0 deletions tests/server/server-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
from wolfclu_test import WOLFSSL_BIN, CERTS_DIR, test_main, find_free_port


NAME_MISMATCH = b"peer subject name mismatch"
IP_MISMATCH = b"peer ip address mismatch"


class ServerClientTest(unittest.TestCase):

@classmethod
Expand Down Expand Up @@ -89,6 +93,130 @@ def test_server_client(self):
server.kill()
server.wait()

def _run_identity_check(self, extra_args):
"""Start s_server on 127.0.0.1 and connect with the given extra
s_client arguments. Returns the s_client CompletedProcess."""
readyfile = "readyfile_identity"
if os.path.exists(readyfile):
os.remove(readyfile)

port = find_free_port()

server = subprocess.Popen(
[WOLFSSL_BIN, "s_server", "-port", str(port),
"-key", os.path.join(CERTS_DIR, "server-key.pem"),
"-cert", os.path.join(CERTS_DIR, "server-cert.pem"),
"-CAfile", os.path.join(CERTS_DIR, "ca-cert.pem"),
"-version", "3", "-naccept", "1", "-www",
"-noVerify", "-readyFile", readyfile],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL,
)

try:
for _ in range(200):
if os.path.exists(readyfile):
break
time.sleep(0.01)
else:
self.fail("s_server did not become ready")

os.remove(readyfile)

return subprocess.run(
[WOLFSSL_BIN, "s_client", "-connect",
"127.0.0.1:{}".format(port),
"-CAfile", os.path.join(CERTS_DIR, "ca-cert.pem"),
"-disable_stdin_check"] + extra_args,
capture_output=True, stdin=subprocess.DEVNULL, timeout=30,
)
finally:
server.terminate()
try:
server.wait(timeout=5)
except subprocess.TimeoutExpired:
server.kill()
server.wait()

def _assert_rejected(self, r, reason, message):
"""Fail unless s_client rejected the peer for the stated reason.

A non-zero exit alone can come from an unready server or a dropped
connection, which would let a real regression pass.
"""
self.assertNotEqual(r.returncode, 0, message)
output = r.stdout + r.stderr
self.assertIn(reason, output,
f"{message} -- rejected, but not for that reason: "
f"{output}")

def test_verify_hostname_match(self):
"""-verify_hostname accepts a name the certificate carries.

server-cert.pem has SAN DNS:example.com. Neither this nor the
mismatch test passes -verify_return_error, so they also cover
-verify_hostname enabling peer verification on its own.
"""
r = self._run_identity_check(["-verify_hostname", "example.com"])
self.assertEqual(r.returncode, 0,
f"s_client rejected a matching name: {r.stderr}")

def test_verify_hostname_mismatch(self):
"""-verify_hostname rejects a CA-trusted cert issued for another name.

server-cert.pem is signed by ca-cert.pem and so passes chain
verification, but carries no name matching attacker.example.
"""
r = self._run_identity_check(["-verify_hostname", "attacker.example"])
self._assert_rejected(r, NAME_MISMATCH,
"SECURITY FAILURE: s_client accepted a "
"certificate issued for a different host")

def test_verify_ip_match(self):
"""-verify_ip accepts an address the certificate carries.

server-cert.pem has SAN IP:127.0.0.1.
"""
r = self._run_identity_check(["-verify_ip", "127.0.0.1"])
self.assertEqual(r.returncode, 0,
f"s_client rejected a matching IP: {r.stderr}")

def test_verify_ip_mismatch(self):
"""-verify_ip rejects a certificate without that IP SAN."""
r = self._run_identity_check(["-verify_ip", "10.0.0.1"])
self._assert_rejected(r, IP_MISMATCH,
"SECURITY FAILURE: s_client accepted a "
"certificate issued for a different address")

def test_verify_ip_unmatched_values(self):
"""-verify_ip fails closed on an IPv6 address the cert lacks and on
a value that is not an address at all."""
for value in ("::1", "not-an-ip"):
with self.subTest(value=value):
r = self._run_identity_check(["-verify_ip", value])
self._assert_rejected(r, IP_MISMATCH,
"SECURITY FAILURE: s_client accepted a "
f"certificate for -verify_ip {value}")

def test_verify_hostname_and_ip_together(self):
"""Both options at once: each is checked, either one can reject."""
r = self._run_identity_check(["-verify_hostname", "example.com",
"-verify_ip", "127.0.0.1"])
self.assertEqual(r.returncode, 0,
f"s_client rejected a matching name and IP: {r.stderr}")

r = self._run_identity_check(["-verify_hostname", "attacker.example",
"-verify_ip", "127.0.0.1"])
self._assert_rejected(r, NAME_MISMATCH,
"SECURITY FAILURE: a matching -verify_ip masked "
"a mismatched -verify_hostname")

r = self._run_identity_check(["-verify_hostname", "example.com",
"-verify_ip", "10.0.0.1"])
self._assert_rejected(r, IP_MISMATCH,
"SECURITY FAILURE: a matching -verify_hostname "
"masked a mismatched -verify_ip")


if __name__ == "__main__":
test_main()
2 changes: 2 additions & 0 deletions wolfclu/clu_optargs.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ enum {
WOLFCLU_VERIFY_RETURN_ERROR,
WOLFCLU_DISABLE_STDINCHK,
WOLFCLU_NOSERVERNAME,
WOLFCLU_VERIFY_HOSTNAME,
WOLFCLU_VERIFY_IP,
WOLFCLU_NOCRYPT,
WOLFCLU_TOPKCS8,
WOLFCLU_HMAC,
Expand Down
Loading