Skip to content

Commit afa88db

Browse files
committed
Fix #2250
1 parent 08133b5 commit afa88db

File tree

2 files changed

+257
-2
lines changed

2 files changed

+257
-2
lines changed

httplib.h

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,14 +1954,17 @@ class SSLServer : public Server {
19541954
void update_certs(X509 *cert, EVP_PKEY *private_key,
19551955
X509_STORE *client_ca_cert_store = nullptr);
19561956

1957+
int ssl_last_error() const { return last_ssl_error_; }
1958+
19571959
private:
19581960
bool process_and_close_socket(socket_t sock) override;
19591961

1962+
STACK_OF(X509_NAME) * extract_ca_names_from_x509_store(X509_STORE *store);
1963+
19601964
SSL_CTX *ctx_;
19611965
std::mutex ctx_mutex_;
1962-
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
1966+
19631967
int last_ssl_error_ = 0;
1964-
#endif
19651968
};
19661969

19671970
class SSLClient final : public ClientImpl {
@@ -10716,6 +10719,19 @@ inline SSLServer::SSLServer(const char *cert_path, const char *private_key_path,
1071610719
SSL_CTX_load_verify_locations(ctx_, client_ca_cert_file_path,
1071710720
client_ca_cert_dir_path);
1071810721

10722+
// Set client CA list to be sent to clients during TLS handshake
10723+
if (client_ca_cert_file_path) {
10724+
auto ca_list = SSL_load_client_CA_file(client_ca_cert_file_path);
10725+
if (ca_list != nullptr) {
10726+
SSL_CTX_set_client_CA_list(ctx_, ca_list);
10727+
} else {
10728+
// Failed to load client CA list, but we continue since
10729+
// SSL_CTX_load_verify_locations already succeeded and
10730+
// certificate verification will still work
10731+
last_ssl_error_ = static_cast<int>(ERR_get_error());
10732+
}
10733+
}
10734+
1071910735
SSL_CTX_set_verify(
1072010736
ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);
1072110737
}
@@ -10740,6 +10756,15 @@ inline SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key,
1074010756
} else if (client_ca_cert_store) {
1074110757
SSL_CTX_set_cert_store(ctx_, client_ca_cert_store);
1074210758

10759+
// Extract CA names from the store and set them as the client CA list
10760+
auto ca_list = extract_ca_names_from_x509_store(client_ca_cert_store);
10761+
if (ca_list) {
10762+
SSL_CTX_set_client_CA_list(ctx_, ca_list);
10763+
} else {
10764+
// Failed to extract CA names, record the error
10765+
last_ssl_error_ = static_cast<int>(ERR_get_error());
10766+
}
10767+
1074310768
SSL_CTX_set_verify(
1074410769
ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);
1074510770
}
@@ -10820,6 +10845,44 @@ inline bool SSLServer::process_and_close_socket(socket_t sock) {
1082010845
return ret;
1082110846
}
1082210847

10848+
inline STACK_OF(X509_NAME) * SSLServer::extract_ca_names_from_x509_store(
10849+
X509_STORE *store) {
10850+
if (!store) { return nullptr; }
10851+
10852+
auto ca_list = sk_X509_NAME_new_null();
10853+
if (!ca_list) { return nullptr; }
10854+
10855+
// Get all objects from the store
10856+
auto objs = X509_STORE_get0_objects(store);
10857+
if (!objs) {
10858+
sk_X509_NAME_free(ca_list);
10859+
return nullptr;
10860+
}
10861+
10862+
// Iterate through objects and extract certificate subject names
10863+
for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) {
10864+
auto obj = sk_X509_OBJECT_value(objs, i);
10865+
if (X509_OBJECT_get_type(obj) == X509_LU_X509) {
10866+
auto cert = X509_OBJECT_get0_X509(obj);
10867+
if (cert) {
10868+
auto subject = X509_get_subject_name(cert);
10869+
if (subject) {
10870+
auto name_dup = X509_NAME_dup(subject);
10871+
if (name_dup) { sk_X509_NAME_push(ca_list, name_dup); }
10872+
}
10873+
}
10874+
}
10875+
}
10876+
10877+
// If no names were extracted, free the list and return nullptr
10878+
if (sk_X509_NAME_num(ca_list) == 0) {
10879+
sk_X509_NAME_free(ca_list);
10880+
return nullptr;
10881+
}
10882+
10883+
return ca_list;
10884+
}
10885+
1082310886
// SSL HTTP client implementation
1082410887
inline SSLClient::SSLClient(const std::string &host)
1082510888
: SSLClient(host, 443, std::string(), std::string()) {}

test/test.cc

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
#include <atomic>
1515
#include <chrono>
16+
#include <cstdio>
1617
#include <fstream>
1718
#include <future>
1819
#include <limits>
@@ -8721,6 +8722,197 @@ TEST(SSLClientServerTest, CustomizeServerSSLCtx) {
87218722
ASSERT_EQ(StatusCode::OK_200, res->status);
87228723
}
87238724

8725+
TEST(SSLClientServerTest, ClientCAListSentToClient) {
8726+
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE);
8727+
ASSERT_TRUE(svr.is_valid());
8728+
8729+
// Set up a handler to verify client certificate is present
8730+
bool client_cert_verified = false;
8731+
svr.Get("/test", [&](const Request & /*req*/, Response &res) {
8732+
// Verify that client certificate was provided
8733+
client_cert_verified = true;
8734+
res.set_content("success", "text/plain");
8735+
});
8736+
8737+
thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); });
8738+
auto se = detail::scope_exit([&] {
8739+
svr.stop();
8740+
t.join();
8741+
ASSERT_FALSE(svr.is_running());
8742+
});
8743+
8744+
svr.wait_until_ready();
8745+
8746+
// Client with certificate
8747+
SSLClient cli(HOST, PORT, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
8748+
cli.enable_server_certificate_verification(false);
8749+
cli.set_connection_timeout(30);
8750+
8751+
auto res = cli.Get("/test");
8752+
ASSERT_TRUE(res);
8753+
ASSERT_EQ(StatusCode::OK_200, res->status);
8754+
ASSERT_TRUE(client_cert_verified);
8755+
EXPECT_EQ("success", res->body);
8756+
}
8757+
8758+
TEST(SSLClientServerTest, ClientCAListSetInContext) {
8759+
// Test that when client CA cert file is provided,
8760+
// SSL_CTX_set_client_CA_list is called and the CA list is properly set
8761+
8762+
// Create a server with client authentication
8763+
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE);
8764+
ASSERT_TRUE(svr.is_valid());
8765+
8766+
// We can't directly access the SSL_CTX from SSLServer to verify,
8767+
// but we can test that the server properly requests client certificates
8768+
// and accepts valid ones from the specified CA
8769+
8770+
bool handler_called = false;
8771+
svr.Get("/test", [&](const Request &req, Response &res) {
8772+
handler_called = true;
8773+
8774+
// Verify that a client certificate was provided
8775+
auto peer_cert = SSL_get_peer_certificate(req.ssl);
8776+
ASSERT_TRUE(peer_cert != nullptr);
8777+
8778+
// Get the issuer name
8779+
auto issuer_name = X509_get_issuer_name(peer_cert);
8780+
ASSERT_TRUE(issuer_name != nullptr);
8781+
8782+
char issuer_buf[256];
8783+
X509_NAME_oneline(issuer_name, issuer_buf, sizeof(issuer_buf));
8784+
8785+
// The client certificate should be issued by our test CA
8786+
std::string issuer_str(issuer_buf);
8787+
EXPECT_TRUE(issuer_str.find("Root CA Name") != std::string::npos);
8788+
8789+
X509_free(peer_cert);
8790+
res.set_content("authenticated", "text/plain");
8791+
});
8792+
8793+
thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); });
8794+
auto se = detail::scope_exit([&] {
8795+
svr.stop();
8796+
t.join();
8797+
ASSERT_FALSE(svr.is_running());
8798+
});
8799+
8800+
svr.wait_until_ready();
8801+
8802+
// Connect with a client certificate issued by the CA
8803+
SSLClient cli(HOST, PORT, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
8804+
cli.enable_server_certificate_verification(false);
8805+
cli.set_connection_timeout(30);
8806+
8807+
auto res = cli.Get("/test");
8808+
ASSERT_TRUE(res);
8809+
ASSERT_EQ(StatusCode::OK_200, res->status);
8810+
ASSERT_TRUE(handler_called);
8811+
EXPECT_EQ("authenticated", res->body);
8812+
}
8813+
8814+
TEST(SSLClientServerTest, ClientCAListLoadErrorRecorded) {
8815+
// Test 1: Valid CA file - no error should be recorded
8816+
{
8817+
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE,
8818+
CLIENT_CA_CERT_FILE);
8819+
ASSERT_TRUE(svr.is_valid());
8820+
8821+
// With valid setup, last_ssl_error should be 0
8822+
EXPECT_EQ(0, svr.ssl_last_error());
8823+
}
8824+
8825+
// Test 2: Invalid CA file content
8826+
// When SSL_load_client_CA_file fails, last_ssl_error_ should be set
8827+
{
8828+
// Create a temporary file with completely invalid content
8829+
const char *temp_invalid_ca = "./temp_invalid_ca_for_test.txt";
8830+
{
8831+
std::ofstream ofs(temp_invalid_ca);
8832+
ofs << "This is not a certificate file at all\n";
8833+
ofs << "Just plain text content\n";
8834+
}
8835+
8836+
// Create server with invalid CA file
8837+
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, temp_invalid_ca);
8838+
8839+
// Clean up temporary file
8840+
std::remove(temp_invalid_ca);
8841+
8842+
// When there's an SSL error (from either SSL_CTX_load_verify_locations
8843+
// or SSL_load_client_CA_file), last_ssl_error_ should be non-zero
8844+
// Note: SSL_CTX_load_verify_locations typically fails first,
8845+
// but our error handling code path is still exercised
8846+
if (!svr.is_valid()) { EXPECT_NE(0, svr.ssl_last_error()); }
8847+
}
8848+
}
8849+
8850+
TEST(SSLClientServerTest, ClientCAListFromX509Store) {
8851+
// Test SSL server using X509_STORE constructor with client CA certificates
8852+
// This test verifies that Phase 2 implementation correctly extracts CA names
8853+
// from an X509_STORE and sets them in the SSL context
8854+
8855+
// Load the CA certificate into memory
8856+
auto bio = BIO_new_file(CLIENT_CA_CERT_FILE, "r");
8857+
ASSERT_NE(nullptr, bio);
8858+
8859+
auto ca_cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
8860+
BIO_free(bio);
8861+
ASSERT_NE(nullptr, ca_cert);
8862+
8863+
// Create an X509_STORE and add the CA certificate
8864+
auto store = X509_STORE_new();
8865+
ASSERT_NE(nullptr, store);
8866+
ASSERT_EQ(1, X509_STORE_add_cert(store, ca_cert));
8867+
8868+
// Load server certificate and private key
8869+
auto cert_bio = BIO_new_file(SERVER_CERT_FILE, "r");
8870+
ASSERT_NE(nullptr, cert_bio);
8871+
auto server_cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr);
8872+
BIO_free(cert_bio);
8873+
ASSERT_NE(nullptr, server_cert);
8874+
8875+
auto key_bio = BIO_new_file(SERVER_PRIVATE_KEY_FILE, "r");
8876+
ASSERT_NE(nullptr, key_bio);
8877+
auto server_key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr);
8878+
BIO_free(key_bio);
8879+
ASSERT_NE(nullptr, server_key);
8880+
8881+
// Create SSLServer with X509_STORE constructor
8882+
// Note: X509_STORE ownership is transferred to SSL_CTX
8883+
SSLServer svr(server_cert, server_key, store);
8884+
ASSERT_TRUE(svr.is_valid());
8885+
8886+
// No SSL error should be recorded for valid setup
8887+
EXPECT_EQ(0, svr.ssl_last_error());
8888+
8889+
// Set up server endpoints
8890+
svr.Get("/test-x509store", [&](const Request & /*req*/, Response &res) {
8891+
res.set_content("ok", "text/plain");
8892+
});
8893+
8894+
// Start server in a thread
8895+
auto server_thread = thread([&]() { svr.listen(HOST, PORT); });
8896+
svr.wait_until_ready();
8897+
8898+
// Connect with client certificate (using constructor with paths)
8899+
SSLClient cli(HOST, PORT, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
8900+
cli.enable_server_certificate_verification(false);
8901+
8902+
auto res = cli.Get("/test-x509store");
8903+
ASSERT_TRUE(res);
8904+
EXPECT_EQ(200, res->status);
8905+
EXPECT_EQ("ok", res->body);
8906+
8907+
// Clean up
8908+
X509_free(server_cert);
8909+
EVP_PKEY_free(server_key);
8910+
X509_free(ca_cert);
8911+
8912+
svr.stop();
8913+
server_thread.join();
8914+
}
8915+
87248916
// Disabled due to the out-of-memory problem on GitHub Actions Workflows
87258917
TEST(SSLClientServerTest, DISABLED_LargeDataTransfer) {
87268918

0 commit comments

Comments
 (0)