Skip to content

Commit fd71c11

Browse files
committed
Verify the server certificate during the handshake on OpenSSL
The OpenSSL client finished the handshake with SSL_VERIFY_NONE and checked the server certificate only afterwards, so it presented its client certificate to a server that then failed verification. With a server certificate verifier that accepted, wolfSSL did the same. Verify the chain, and the host name, during the handshake on every backend, keeping the built-in verdict when a verifier runs alongside it. On OpenSSL a session verifier, which may accept a certificate the built-in check rejects, still decides after the handshake. A certificate that fails the built-in verification now reports CertVerifyFailed, or HostnameMismatch for its name, in ssl_error() on every backend, where OpenSSL left it 0.
1 parent fd5dd79 commit fd71c11

3 files changed

Lines changed: 137 additions & 45 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ cli.enable_server_hostname_verification(false);
108108
109109
When SSL operations fail, cpp-httplib provides detailed error information through `ssl_error()` and `ssl_backend_error()`:
110110
111-
- `ssl_error()` - Returns the TLS-level error as a backend-independent `httplib::tls::ErrorCode` value (e.g., `Fatal`, `CertVerifyFailed`), cast to `int`. `CertVerifyFailed` comes from Mbed TLS and wolfSSL. OpenSSL checks the chain and the host name after the handshake, so a failure there leaves this `0`
111+
- `ssl_error()` - Returns the TLS-level error as a backend-independent `httplib::tls::ErrorCode` value (e.g., `Fatal`, `CertVerifyFailed`), cast to `int`. A server certificate that the built-in verification rejects reports `CertVerifyFailed`, or `HostnameMismatch` when only its name does not match
112112
- `ssl_backend_error()` - Returns the backend-specific error code (e.g., `ERR_get_error()` for OpenSSL/wolfSSL, return value for Mbed TLS). With OpenSSL, a certificate verification failure reports the verify result (`X509_V_ERR_*`) here instead. On Windows, when CryptoAPI verifies the chain, a failure there reports the CryptoAPI error code here
113113
114114
A request that fails after the handshake with `Error::Read` or `Error::Write` also carries the TLS error behind it in both values, such as a TLS 1.3 "certificate required" alert from the server. Both values are reset for each request.

‎httplib.h‎

Lines changed: 83 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11330,6 +11330,17 @@ inline bool setup_client_tls_session(
1133011330
return false;
1133111331
};
1133211332

11333+
// A TLS failure, whose verification codes name the Error too
11334+
auto fail_tls = [&](ErrorCode code, uint64_t backend_error) {
11335+
auto error = Error::SSLConnection;
11336+
if (code == ErrorCode::CertVerifyFailed) {
11337+
error = Error::SSLServerVerification;
11338+
} else if (code == ErrorCode::HostnameMismatch) {
11339+
error = Error::SSLServerHostnameVerification;
11340+
}
11341+
return fail(error, static_cast<int>(code), backend_error);
11342+
};
11343+
1133311344
if (!ctx) {
1133411345
session = nullptr;
1133511346
return fail(Error::SSLConnection, 0, 0);
@@ -11345,45 +11356,42 @@ inline bool setup_client_tls_session(
1134511356
!options.server_certificate_verifier_set;
1134611357
#endif
1134711358

11359+
// The chain, and a host name, are verified during the handshake, so that a
11360+
// server failing them never gets to see a client certificate. Unless
11361+
// Schannel verifies the chain instead, this holds for IP hosts too, whose
11362+
// identity is checked after the handshake by verify_hostname().
11363+
auto verify_in_handshake =
11364+
server_certificate_verification && !windows_verifies_chain;
11365+
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
11366+
// A session verifier may accept a certificate the built-in check rejects,
11367+
// and runs only after the handshake
11368+
verify_in_handshake = verify_in_handshake && !options.session_verifier;
11369+
#endif
11370+
1134811371
{
1134911372
std::unique_lock<std::mutex> guard;
1135011373
if (options.ctx_mutex) {
1135111374
guard = std::unique_lock<std::mutex>(*options.ctx_mutex);
1135211375
}
11353-
#if defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT)
11354-
// Mbed TLS and wolfSSL need the verification mode set explicitly, on the
11355-
// context, so under the same lock; OpenSSL uses SSL_VERIFY_NONE and does
11356-
// all verification post-handshake. Unless Schannel verifies the chain
11357-
// instead, chain verification happens during the handshake even for IP
11358-
// hosts; the certificate identity is verified post-handshake via
11359-
// verify_hostname().
11360-
set_verify_client(ctx, server_certificate_verification &&
11361-
!windows_verifies_chain);
11362-
#endif
11376+
// The mode is set on the context, so under the same lock
11377+
set_verify_client(ctx, verify_in_handshake);
1136311378
session = create_session(ctx, sock);
1136411379
}
1136511380
if (!session) { return fail(Error::SSLConnection, 0, get_error()); }
1136611381

1136711382
// RFC 6066: SNI must not be set for IP addresses; skip it for IP hosts, so
11368-
// their identity is checked post-handshake below instead. On Mbed TLS and
11369-
// wolfSSL, set_sni also drives handshake-time hostname verification, so
11370-
// options.server_hostname_verification is threaded through here.
11383+
// their identity is checked post-handshake below instead.
1137111384
if (!is_ip_address(host)) {
11372-
if (!set_sni(session, host.c_str(), options.server_hostname_verification)) {
11385+
if (!set_sni(session, host.c_str(),
11386+
verify_in_handshake && options.server_hostname_verification)) {
1137311387
return fail(Error::SSLConnection, 0, get_error());
1137411388
}
1137511389
}
1137611390

1137711391
TlsError tls_err;
1137811392
if (!connect_nonblocking(session, sock, timeout_sec, timeout_usec,
1137911393
&tls_err)) {
11380-
auto error = Error::SSLConnection;
11381-
if (tls_err.code == ErrorCode::CertVerifyFailed) {
11382-
error = Error::SSLServerVerification;
11383-
} else if (tls_err.code == ErrorCode::HostnameMismatch) {
11384-
error = Error::SSLServerHostnameVerification;
11385-
}
11386-
return fail(error, static_cast<int>(tls_err.code), tls_err.backend_code);
11394+
return fail_tls(tls_err.code, tls_err.backend_code);
1138711395
}
1138811396

1138911397
auto verification_status = SSLVerifierResponse::NoDecisionMade;
@@ -11400,14 +11408,14 @@ inline bool setup_client_tls_session(
1140011408
if (!windows_verifies_chain) {
1140111409
auto verify_result = get_verify_result(session);
1140211410
if (verify_result != 0) {
11403-
return fail(Error::SSLServerVerification, 0,
11404-
static_cast<uint64_t>(verify_result));
11411+
return fail_tls(ErrorCode::CertVerifyFailed,
11412+
static_cast<uint64_t>(verify_result));
1140511413
}
1140611414
}
1140711415

1140811416
auto server_cert = get_peer_cert(session);
1140911417
if (!server_cert) {
11410-
return fail(Error::SSLServerVerification, 0, get_error());
11418+
return fail_tls(ErrorCode::CertVerifyFailed, get_error());
1141111419
}
1141211420
auto cert_guard = detail::scope_exit([&] { free_cert(server_cert); });
1141311421

@@ -11416,8 +11424,7 @@ inline bool setup_client_tls_session(
1141611424
// hostname is bound during the handshake.
1141711425
if (options.server_hostname_verification) {
1141811426
if (!verify_hostname(server_cert, host.c_str())) {
11419-
return fail(Error::SSLServerHostnameVerification, 0,
11420-
hostname_mismatch_code());
11427+
return fail_tls(ErrorCode::HostnameMismatch, hostname_mismatch_code());
1142111428
}
1142211429
}
1142311430

@@ -11432,7 +11439,7 @@ inline bool setup_client_tls_session(
1143211439
!verify_cert_with_windows_schannel(
1143311440
der, host, options.server_hostname_verification, wincrypt_error,
1143411441
session)) {
11435-
return fail(Error::SSLServerVerification, 0, wincrypt_error);
11442+
return fail_tls(ErrorCode::CertVerifyFailed, wincrypt_error);
1143611443
}
1143711444
}
1143811445
#endif
@@ -19715,6 +19722,30 @@ inline ErrorCode map_ssl_error(int ssl_error, int &out_errno) {
1971519722
}
1971619723
}
1971719724

19725+
// Fills in a failed SSL_connect(). One that the server certificate's
19726+
// verification stopped reports the verify result, as the other backends and
19727+
// a check after the handshake do.
19728+
inline void fill_connect_error(SSL *ssl, int ret, TlsError &err) {
19729+
auto ssl_err = SSL_get_error(ssl, ret);
19730+
auto error = ERR_get_error();
19731+
err.code = map_ssl_error(ssl_err, err.sys_errno);
19732+
err.backend_code = error;
19733+
if (ssl_err != SSL_ERROR_SSL || ERR_GET_LIB(error) != ERR_LIB_SSL ||
19734+
ERR_GET_REASON(error) != SSL_R_CERTIFICATE_VERIFY_FAILED) {
19735+
return;
19736+
}
19737+
// A certificate only a verify callback turned down carries no verdict of
19738+
// its own, and fails as any other handshake does
19739+
auto verify_result = SSL_get_verify_result(ssl);
19740+
if (verify_result == X509_V_OK || verify_result == X509_V_ERR_UNSPECIFIED) {
19741+
return;
19742+
}
19743+
err.code = verify_result == X509_V_ERR_HOSTNAME_MISMATCH
19744+
? ErrorCode::HostnameMismatch
19745+
: ErrorCode::CertVerifyFailed;
19746+
err.backend_code = static_cast<uint64_t>(verify_result);
19747+
}
19748+
1971819749
// Helper: Create client CA list from PEM string
1971919750
// Returns a new STACK_OF(X509_NAME)* or nullptr on failure
1972019751
// Caller takes ownership of returned list
@@ -19790,7 +19821,14 @@ inline int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {
1979019821
verify_ctx.error_string =
1979119822
(error != X509_V_OK) ? X509_verify_cert_error_string(error) : nullptr;
1979219823

19793-
return run_verify_callback(*callback, verify_ctx) ? 1 : 0;
19824+
auto accepted = run_verify_callback(*callback, verify_ctx);
19825+
// A client verifying the server during the handshake, which is what
19826+
// set_verify_client(ctx, true) asks for there, keeps the built-in verdict
19827+
if (!SSL_is_server(ssl) &&
19828+
(SSL_get_verify_mode(ssl) & SSL_VERIFY_FAIL_IF_NO_PEER_CERT)) {
19829+
accepted = accepted && preverify_ok;
19830+
}
19831+
return accepted ? 1 : 0;
1979419832
}
1979519833

1979619834
// Sets the verify mode from whether the peer's certificate is required and
@@ -20066,14 +20104,18 @@ inline void free_session(session_t session) {
2006620104
}
2006720105

2006820106
inline bool set_sni(session_t session, const char *hostname,
20069-
bool /*verify_hostname*/) {
20107+
bool verify_hostname) {
2007020108
if (!session || !hostname) return false;
2007120109

2007220110
auto ssl = static_cast<SSL *>(session);
2007320111

20074-
// Set SNI (Server Name Indication) only - does not enable verification.
20075-
// OpenSSL never binds identity checking to SNI (that happens post-
20076-
// handshake in setup_client_tls_session()), so verify_hostname is unused.
20112+
// The handshake's chain verification then checks the name as well
20113+
if (verify_hostname &&
20114+
X509_VERIFY_PARAM_set1_host(SSL_get0_param(ssl), hostname,
20115+
strlen(hostname)) != 1) {
20116+
return false;
20117+
}
20118+
2007720119
#if defined(OPENSSL_IS_BORINGSSL)
2007820120
return SSL_set_tlsext_host_name(ssl, hostname) == 1;
2007920121
#else
@@ -20093,9 +20135,7 @@ inline TlsError connect(session_t session) {
2009320135
if (ret == 1) {
2009420136
err.code = ErrorCode::Success;
2009520137
} else {
20096-
auto ssl_err = SSL_get_error(ssl, ret);
20097-
err.code = impl::map_ssl_error(ssl_err, err.sys_errno);
20098-
err.backend_code = ERR_get_error();
20138+
impl::fill_connect_error(ssl, ret, err);
2009920139
}
2010020140
return err;
2010120141
}
@@ -20151,10 +20191,7 @@ inline bool connect_nonblocking(session_t session, socket_t sock,
2015120191
break;
2015220192
default: break;
2015320193
}
20154-
if (err) {
20155-
err->code = impl::map_ssl_error(ssl_err, err->sys_errno);
20156-
err->backend_code = ERR_get_error();
20157-
}
20194+
if (err) { impl::fill_connect_error(ssl, res, *err); }
2015820195
return false;
2015920196
}
2016020197
if (err) { err->code = ErrorCode::Success; }
@@ -22296,7 +22333,13 @@ inline int wolfssl_verify_callback(int preverify_ok,
2229622333
verify_ctx.error_string = nullptr;
2229722334
}
2229822335

22299-
bool accepted = run_verify_callback(*callback, verify_ctx);
22336+
auto accepted = run_verify_callback(*callback, verify_ctx);
22337+
// A client verifying the server during the handshake, which is what
22338+
// set_verify_client(ctx, true) asks for there, keeps the built-in verdict
22339+
if (!wolfSSL_is_server(ssl) &&
22340+
(wolfSSL_get_verify_mode(ssl) & WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT)) {
22341+
accepted = accepted && preverify_ok;
22342+
}
2230022343
return accepted ? 1 : 0;
2230122344
}
2230222345

‎test/test.cc‎

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15808,9 +15808,9 @@ TEST(SSLClientTest, ServerCertificateVerificationError_Online) {
1580815808
// OpenSSL: X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT (18)
1580915809
// Mbed TLS: MBEDTLS_X509_BADCERT_NOT_TRUSTED or similar flags
1581015810
EXPECT_NE(0UL, res.ssl_backend_error());
15811+
EXPECT_EQ(static_cast<int>(tls::ErrorCode::CertVerifyFailed),
15812+
res.ssl_error());
1581115813
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
15812-
// For OpenSSL, ssl_error is 0 for verification errors
15813-
EXPECT_EQ(0, res.ssl_error());
1581415814
#if !defined(_WIN32) || \
1581515815
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1581615816
// On non-Windows or when Windows Schannel is disabled, the error comes
@@ -15834,9 +15834,9 @@ TEST(SSLClientTest, ServerHostnameVerificationError_Online) {
1583415834

1583515835
// Verify backend error is captured for hostname verification failure
1583615836
EXPECT_NE(0UL, res.ssl_backend_error());
15837+
EXPECT_EQ(static_cast<int>(tls::ErrorCode::HostnameMismatch),
15838+
res.ssl_error());
1583715839
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
15838-
// For OpenSSL, ssl_error is 0 for verification errors
15839-
EXPECT_EQ(0, res.ssl_error());
1584015840
#if !defined(_WIN32) || \
1584115841
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
1584215842
// On non-Windows or when Windows Schannel is disabled, the error comes
@@ -16252,6 +16252,55 @@ TEST(SSLClientServerTest, ClientCertStillRequiredAfterVerifyCallbackCleared) {
1625216252
});
1625316253
}
1625416254

16255+
// The client verifies the server certificate during the handshake, so a
16256+
// server that fails the check never gets the client certificate, even when a
16257+
// verifier of the client's own accepts it
16258+
TEST(SSLClientServerTest, ClientCertNotSentToUnverifiedServer) {
16259+
struct Case {
16260+
const char *trusted_ca;
16261+
const char *host;
16262+
Error error;
16263+
};
16264+
// cert2.pem names localhost: the first client does not trust it, and the
16265+
// second trusts it but asks for another name
16266+
for (const auto &c :
16267+
{Case{CLIENT_CA_CERT_FILE, "localhost", Error::SSLServerVerification},
16268+
Case{SERVER_CERT2_FILE, "example.com",
16269+
Error::SSLServerHostnameVerification}}) {
16270+
for (auto with_verifier : {false, true}) {
16271+
SCOPED_TRACE(std::string(c.host) + (with_verifier ? " +verifier" : ""));
16272+
SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE,
16273+
CLIENT_CA_CERT_FILE);
16274+
ASSERT_TRUE(svr.is_valid());
16275+
std::atomic<bool> got_client_cert{false};
16276+
ASSERT_TRUE(tls::set_verify_callback(svr.tls_context(),
16277+
[&](const tls::VerifyContext &) {
16278+
got_client_cert = true;
16279+
return true;
16280+
}));
16281+
auto port = svr.bind_to_any_port("127.0.0.1");
16282+
ASSERT_LT(0, port);
16283+
thread t = thread([&] { svr.listen_after_bind(); });
16284+
svr.wait_until_ready();
16285+
16286+
SSLClient cli(c.host, port, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
16287+
cli.set_hostname_addr_map({{c.host, "127.0.0.1"}});
16288+
cli.set_ca_cert_path(c.trusted_ca);
16289+
if (with_verifier) {
16290+
cli.set_server_certificate_verifier(
16291+
[](const tls::VerifyContext &) { return true; });
16292+
}
16293+
auto res = cli.Get("/");
16294+
EXPECT_FALSE(res);
16295+
EXPECT_EQ(c.error, res.error());
16296+
16297+
svr.stop();
16298+
t.join();
16299+
EXPECT_FALSE(got_client_cert);
16300+
}
16301+
}
16302+
}
16303+
1625516304
void UpdateCertsPemWithClientCA(SSLServer &svr) {
1625616305
std::string cert_pem, key_pem, ca_pem;
1625716306
read_file(SERVER_CERT_FILE, cert_pem);

0 commit comments

Comments
 (0)