From 9084802820a18f10099002aea3bbd9f579d68642 Mon Sep 17 00:00:00 2001 From: Tom Tan Date: Fri, 10 Apr 2026 10:48:35 -0700 Subject: [PATCH 1/3] Enable configurable SSL certificate verification for libcurl HTTP client --- lib/api/LogManagerImpl.cpp | 16 +-- lib/config/RuntimeConfig_Default.hpp | 6 +- lib/http/HttpClient_Curl.cpp | 23 +++- lib/http/HttpClient_Curl.hpp | 18 ++- lib/http/HttpClient_WinInet.cpp | 5 + lib/http/HttpClient_WinInet.hpp | 2 + lib/include/public/IHttpClient.hpp | 9 ++ lib/include/public/ILogConfiguration.hpp | 10 ++ tests/unittests/CMakeLists.txt | 1 + tests/unittests/HttpClientCurlTests.cpp | 160 +++++++++++++++++++++++ 10 files changed, 231 insertions(+), 19 deletions(-) create mode 100644 tests/unittests/HttpClientCurlTests.cpp diff --git a/lib/api/LogManagerImpl.cpp b/lib/api/LogManagerImpl.cpp index fac8fdbd5..2f0e8933d 100644 --- a/lib/api/LogManagerImpl.cpp +++ b/lib/api/LogManagerImpl.cpp @@ -289,13 +289,7 @@ namespace MAT_NS_BEGIN if (m_httpClient == nullptr) { m_httpClient = HttpClientFactory::Create(); -#ifdef HAVE_MAT_WININET_HTTP_CLIENT - HttpClient_WinInet* client = static_cast(m_httpClient.get()); - if (client != nullptr) - { - client->SetMsRootCheck(m_logConfiguration[CFG_MAP_HTTP][CFG_BOOL_HTTP_MS_ROOT_CHECK]); - } -#endif + m_httpClient->ApplySettings(m_logConfiguration); } else { @@ -366,14 +360,10 @@ namespace MAT_NS_BEGIN /// void LogManagerImpl::Configure() { - // TODO: [maxgolov] - add other config params. -#ifdef HAVE_MAT_WININET_HTTP_CLIENT - HttpClient_WinInet* client = static_cast(m_httpClient.get()); - if (client != nullptr) + if (m_httpClient != nullptr) { - client->SetMsRootCheck(m_logConfiguration[CFG_MAP_HTTP][CFG_BOOL_HTTP_MS_ROOT_CHECK]); + m_httpClient->ApplySettings(m_logConfiguration); } -#endif } LogManagerImpl::~LogManagerImpl() noexcept diff --git a/lib/config/RuntimeConfig_Default.hpp b/lib/config/RuntimeConfig_Default.hpp index d911ff9fd..504aeefe3 100644 --- a/lib/config/RuntimeConfig_Default.hpp +++ b/lib/config/RuntimeConfig_Default.hpp @@ -60,7 +60,11 @@ namespace MAT_NS_BEGIN , {"contentEncoding", "deflate"}, /* Optional parameter to require Microsoft Root CA */ - {CFG_BOOL_HTTP_MS_ROOT_CHECK, false}}}, + {CFG_BOOL_HTTP_MS_ROOT_CHECK, false}, + /* Optional parameter for SSL certificate verification (curl) */ + {CFG_BOOL_HTTP_SSL_VERIFY, true}, + /* Optional CA bundle path for OpenSSL-backed curl */ + {CFG_STR_HTTP_SSL_CAINFO, ""}}}, {CFG_MAP_TPM, { {CFG_INT_TPM_MAX_BLOB_BYTES, 2097152}, diff --git a/lib/http/HttpClient_Curl.cpp b/lib/http/HttpClient_Curl.cpp index 18ddabce9..b910cdf28 100644 --- a/lib/http/HttpClient_Curl.cpp +++ b/lib/http/HttpClient_Curl.cpp @@ -14,6 +14,7 @@ #include "utils/Utils.hpp" #include "HttpClient_Curl.hpp" +#include "ILogConfiguration.hpp" namespace MAT_NS_BEGIN { @@ -74,7 +75,13 @@ namespace MAT_NS_BEGIN { requestHeaders[header.first] = header.second; } - auto curlOperation = std::make_shared(curlRequest->m_method, curlRequest->m_url, callback, requestHeaders, curlRequest->m_body); + std::string sslCaInfo; + { + std::lock_guard lock(m_requestsMtx); + sslCaInfo = m_sslCaInfo; + } + + auto curlOperation = std::make_shared(curlRequest->m_method, curlRequest->m_url, callback, requestHeaders, curlRequest->m_body, false, HTTP_CONN_TIMEOUT, m_sslVerify, sslCaInfo); curlRequest->SetOperation(curlOperation); // The lifetime of curlOperation is guarnteed by the call to result.wait() in the d'tor. @@ -125,6 +132,20 @@ namespace MAT_NS_BEGIN { } } + void HttpClient_Curl::ApplySettings(ILogConfiguration& config) + { + SetSslVerification( + config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY], + (const char *)config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO]); + } + + void HttpClient_Curl::SetSslVerification(bool sslVerify, const std::string& caInfo) + { + m_sslVerify = sslVerify; + std::lock_guard lock(m_requestsMtx); + m_sslCaInfo = caInfo; + } + void HttpClient_Curl::EraseRequest(std::string const& id) { std::lock_guard lock(m_requestsMtx); diff --git a/lib/http/HttpClient_Curl.hpp b/lib/http/HttpClient_Curl.hpp index cb89ec7e6..d2f5e2b50 100644 --- a/lib/http/HttpClient_Curl.hpp +++ b/lib/http/HttpClient_Curl.hpp @@ -55,12 +55,17 @@ class HttpClient_Curl : public IHttpClient { virtual void SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback) override; virtual void CancelRequestAsync(std::string const& id) override; + virtual void ApplySettings(ILogConfiguration& config) override; + void SetSslVerification(bool sslVerify, const std::string& caInfo = ""); + private: void EraseRequest(std::string const& id); void AddRequest(IHttpRequest* request); std::mutex m_requestsMtx; std::map m_requests; + std::atomic m_sslVerify { true }; + std::string m_sslCaInfo; }; class CurlHttpOperation { @@ -91,7 +96,10 @@ class CurlHttpOperation { const std::vector& requestBody = std::vector(), // Default connectivity and response size options bool rawResponse = false, - size_t httpConnTimeout = HTTP_CONN_TIMEOUT) : + size_t httpConnTimeout = HTTP_CONN_TIMEOUT, + // SSL certificate verification options + bool sslVerify = true, + const std::string& sslCaInfo = "") : // Optional connection params rawResponse(rawResponse), @@ -129,9 +137,11 @@ class CurlHttpOperation { // Specify target URL curl_easy_setopt(curl, CURLOPT_URL, m_url.c_str()); - // TODO: expose SSL cert verification opts via ILogConfiguration - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); // 1L - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); // 2L + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, sslVerify ? 1L : 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, sslVerify ? 2L : 0L); + if (!sslCaInfo.empty()) { + curl_easy_setopt(curl, CURLOPT_CAINFO, sslCaInfo.c_str()); + } // HTTP/2 please, fallback to HTTP/1.1 if not supported curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); diff --git a/lib/http/HttpClient_WinInet.cpp b/lib/http/HttpClient_WinInet.cpp index 637b10778..eaefb2318 100644 --- a/lib/http/HttpClient_WinInet.cpp +++ b/lib/http/HttpClient_WinInet.cpp @@ -546,6 +546,11 @@ void HttpClient_WinInet::CancelAllRequests() /// Enforces MS-root server certificate check. /// /// if set to true [enforce verification that server cert is MS-Rooted]. +void HttpClient_WinInet::ApplySettings(ILogConfiguration& config) +{ + SetMsRootCheck(config[CFG_MAP_HTTP][CFG_BOOL_HTTP_MS_ROOT_CHECK]); +} + void HttpClient_WinInet::SetMsRootCheck(bool enforceMsRoot) { m_msRootCheck = enforceMsRoot; diff --git a/lib/http/HttpClient_WinInet.hpp b/lib/http/HttpClient_WinInet.hpp index e5936fcc3..7e9379ded 100644 --- a/lib/http/HttpClient_WinInet.hpp +++ b/lib/http/HttpClient_WinInet.hpp @@ -30,6 +30,8 @@ class HttpClient_WinInet : public IHttpClient { virtual void CancelRequestAsync(std::string const& id) final; virtual void CancelAllRequests() final; + virtual void ApplySettings(ILogConfiguration& config) override; + // Methods unique to WinInet implementation. void SetMsRootCheck(bool enforceMsRoot); bool IsMsRootCheckRequired(); diff --git a/lib/include/public/IHttpClient.hpp b/lib/include/public/IHttpClient.hpp index e9a71a210..89e5e6cf0 100644 --- a/lib/include/public/IHttpClient.hpp +++ b/lib/include/public/IHttpClient.hpp @@ -18,6 +18,7 @@ ///@cond INTERNAL_DOCS namespace MAT_NS_BEGIN { + class ILogConfiguration; /// /// The HttpHeaders class contains a set of HTTP headers. /// @@ -543,6 +544,14 @@ namespace MAT_NS_BEGIN virtual void CancelRequestAsync(std::string const& id) = 0; virtual void CancelAllRequests() {} + + /// + /// Apply HTTP settings from the log configuration. + /// Subclasses override to handle platform-specific options. + /// Default implementation is a no-op. + /// + /// The log configuration to read settings from. + virtual void ApplySettings(ILogConfiguration& /*config*/) {} }; /// @endcond diff --git a/lib/include/public/ILogConfiguration.hpp b/lib/include/public/ILogConfiguration.hpp index 952bc2651..1cb8103b8 100644 --- a/lib/include/public/ILogConfiguration.hpp +++ b/lib/include/public/ILogConfiguration.hpp @@ -361,6 +361,16 @@ namespace MAT_NS_BEGIN /// static constexpr const char* const CFG_BOOL_HTTP_COMPRESSION = "compress"; + /// + /// HTTP configuration: SSL certificate verification (peer + host) + /// + static constexpr const char* const CFG_BOOL_HTTP_SSL_VERIFY = "sslVerify"; + + /// + /// HTTP configuration: SSL CA bundle file path (for libcurl/OpenSSL) + /// + static constexpr const char* const CFG_STR_HTTP_SSL_CAINFO = "sslCaInfo"; + /// /// TPM configuration map /// diff --git a/tests/unittests/CMakeLists.txt b/tests/unittests/CMakeLists.txt index 07f1887ca..e453df619 100644 --- a/tests/unittests/CMakeLists.txt +++ b/tests/unittests/CMakeLists.txt @@ -19,6 +19,7 @@ set(SRCS EventPropertiesTests.cpp GuidTests.cpp HttpClientCAPITests.cpp + HttpClientCurlTests.cpp HttpClientManagerTests.cpp HttpClientTests.cpp HttpDeflateCompressionTests.cpp diff --git a/tests/unittests/HttpClientCurlTests.cpp b/tests/unittests/HttpClientCurlTests.cpp new file mode 100644 index 000000000..451c8d4eb --- /dev/null +++ b/tests/unittests/HttpClientCurlTests.cpp @@ -0,0 +1,160 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +#include "mat/config.h" + +// These tests only apply to the curl HTTP client path (non-MSVC, non-Windows) +#if defined(MATSDK_PAL_CPP11) && !defined(_MSC_VER) && defined(HAVE_MAT_DEFAULT_HTTP_CLIENT) + +#include "common/Common.hpp" +#include "http/HttpClient_Curl.hpp" + +using namespace testing; +using namespace MAT; + +class HttpClientCurlTests : public ::testing::Test +{ +protected: + HttpClient_Curl m_client; +}; + +// --- SetSslVerification wiring --- + +TEST_F(HttpClientCurlTests, SslVerification_DefaultsToTrue) +{ + // CurlHttpOperation default parameter is sslVerify=true. + // Construct an operation with defaults and verify CURLOPT values. + CurlHttpOperation op("GET", "https://example.com", nullptr); + CURL* handle = op.GetHandle(); + ASSERT_NE(handle, nullptr); + + long verifyPeer = 0; + long verifyHost = 0; + curl_easy_getinfo(handle, CURLINFO_SSL_VERIFYRESULT, &verifyPeer); + // We can't directly read back CURLOPT values via getinfo for VERIFYPEER/HOST, + // but we can verify the handle is valid and the operation was constructed. + // The real verification is that the config path sets the opts correctly. + SUCCEED(); +} + +TEST_F(HttpClientCurlTests, SetSslVerification_PropagatesVerifyTrue) +{ + m_client.SetSslVerification(true, ""); + + // Create a request and verify the operation receives the SSL settings. + // We exercise the path by constructing a CurlHttpOperation with verify=true. + CurlHttpOperation op("GET", "https://example.com", nullptr, + std::map(), std::vector(), + false, 5, true, ""); + CURL* handle = op.GetHandle(); + ASSERT_NE(handle, nullptr); +} + +TEST_F(HttpClientCurlTests, SetSslVerification_PropagatesVerifyFalse) +{ + m_client.SetSslVerification(false, ""); + + CurlHttpOperation op("GET", "https://example.com", nullptr, + std::map(), std::vector(), + false, 5, false, ""); + CURL* handle = op.GetHandle(); + ASSERT_NE(handle, nullptr); +} + +TEST_F(HttpClientCurlTests, SetSslVerification_CaInfoPassedToOperation) +{ + const std::string caPath = "/etc/ssl/certs/ca-certificates.crt"; + m_client.SetSslVerification(true, caPath); + + CurlHttpOperation op("GET", "https://example.com", nullptr, + std::map(), std::vector(), + false, 5, true, caPath); + CURL* handle = op.GetHandle(); + ASSERT_NE(handle, nullptr); +} + +TEST_F(HttpClientCurlTests, SetSslVerification_EmptyCaInfoNoFailure) +{ + m_client.SetSslVerification(true, ""); + + CurlHttpOperation op("GET", "https://example.com", nullptr, + std::map(), std::vector(), + false, 5, true, ""); + CURL* handle = op.GetHandle(); + ASSERT_NE(handle, nullptr); +} + +// --- ILogConfiguration integration --- + +TEST(HttpClientCurlConfigTests, LogConfiguration_SslVerify_DefaultIsTrue) +{ + ILogConfiguration config; + // The default config should have sslVerify = true + bool sslVerify = config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY]; + EXPECT_TRUE(sslVerify); +} + +TEST(HttpClientCurlConfigTests, LogConfiguration_SslCaInfo_DefaultIsEmpty) +{ + ILogConfiguration config; + const char* caInfo = config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO]; + EXPECT_STREQ(caInfo, ""); +} + +TEST(HttpClientCurlConfigTests, LogConfiguration_SslVerify_CanBeDisabled) +{ + ILogConfiguration config; + config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY] = false; + bool sslVerify = config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY]; + EXPECT_FALSE(sslVerify); +} + +TEST(HttpClientCurlConfigTests, LogConfiguration_SslCaInfo_CanBeSet) +{ + ILogConfiguration config; + config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO] = "/custom/ca-bundle.crt"; + const char* caInfo = config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO]; + EXPECT_STREQ(caInfo, "/custom/ca-bundle.crt"); +} + +// --- ApplySettings integration --- + +TEST_F(HttpClientCurlTests, ApplySettings_ReadsSslConfigFromLogConfiguration) +{ + ILogConfiguration config; + config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY] = false; + config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO] = "/custom/ca.pem"; + m_client.ApplySettings(config); + // Verify indirectly -- constructing an operation should not fail + SUCCEED(); +} + +TEST_F(HttpClientCurlTests, ApplySettings_DefaultConfigEnablesVerification) +{ + ILogConfiguration config; + m_client.ApplySettings(config); + SUCCEED(); +} + +// --- Thread safety: SetSslVerification concurrent with reads --- + +TEST_F(HttpClientCurlTests, SetSslVerification_ConcurrentCallsNoRace) +{ + // Exercise the atomic + mutex path under contention. + // No assertions on output -- this is a sanitizer/TSAN target. + std::vector> futures; + for (int i = 0; i < 10; ++i) + { + futures.push_back(std::async(std::launch::async, [this, i]() { + m_client.SetSslVerification(i % 2 == 0, (i % 2 == 0) ? "/some/path" : ""); + })); + } + for (auto& f : futures) + { + f.get(); + } + SUCCEED(); +} + +#endif // MATSDK_PAL_CPP11 && !_MSC_VER && HAVE_MAT_DEFAULT_HTTP_CLIENT From bee95e75378d6b8ff794889f69c5f679d4308f09 Mon Sep 17 00:00:00 2001 From: Tom Tan Date: Fri, 10 Apr 2026 15:32:22 -0700 Subject: [PATCH 2/3] Test cleanup --- tests/unittests/HttpClientCurlTests.cpp | 55 +++++-------------------- 1 file changed, 11 insertions(+), 44 deletions(-) diff --git a/tests/unittests/HttpClientCurlTests.cpp b/tests/unittests/HttpClientCurlTests.cpp index 451c8d4eb..6f1e3384e 100644 --- a/tests/unittests/HttpClientCurlTests.cpp +++ b/tests/unittests/HttpClientCurlTests.cpp @@ -4,8 +4,9 @@ // #include "mat/config.h" -// These tests only apply to the curl HTTP client path (non-MSVC, non-Windows) -#if defined(MATSDK_PAL_CPP11) && !defined(_MSC_VER) && defined(HAVE_MAT_DEFAULT_HTTP_CLIENT) +// These tests only apply to the curl HTTP client path (Linux, non-Apple, non-Android) +#if defined(MATSDK_PAL_CPP11) && !defined(_MSC_VER) && defined(HAVE_MAT_DEFAULT_HTTP_CLIENT) \ + && !defined(__APPLE__) && !defined(ANDROID) #include "common/Common.hpp" #include "http/HttpClient_Curl.hpp" @@ -23,66 +24,32 @@ class HttpClientCurlTests : public ::testing::Test TEST_F(HttpClientCurlTests, SslVerification_DefaultsToTrue) { - // CurlHttpOperation default parameter is sslVerify=true. - // Construct an operation with defaults and verify CURLOPT values. CurlHttpOperation op("GET", "https://example.com", nullptr); - CURL* handle = op.GetHandle(); - ASSERT_NE(handle, nullptr); - - long verifyPeer = 0; - long verifyHost = 0; - curl_easy_getinfo(handle, CURLINFO_SSL_VERIFYRESULT, &verifyPeer); - // We can't directly read back CURLOPT values via getinfo for VERIFYPEER/HOST, - // but we can verify the handle is valid and the operation was constructed. - // The real verification is that the config path sets the opts correctly. - SUCCEED(); + ASSERT_NE(op.GetHandle(), nullptr); } -TEST_F(HttpClientCurlTests, SetSslVerification_PropagatesVerifyTrue) +TEST_F(HttpClientCurlTests, CurlHttpOperation_ConstructsWithVerifyTrue) { - m_client.SetSslVerification(true, ""); - - // Create a request and verify the operation receives the SSL settings. - // We exercise the path by constructing a CurlHttpOperation with verify=true. CurlHttpOperation op("GET", "https://example.com", nullptr, std::map(), std::vector(), false, 5, true, ""); - CURL* handle = op.GetHandle(); - ASSERT_NE(handle, nullptr); + ASSERT_NE(op.GetHandle(), nullptr); } -TEST_F(HttpClientCurlTests, SetSslVerification_PropagatesVerifyFalse) +TEST_F(HttpClientCurlTests, CurlHttpOperation_ConstructsWithVerifyFalse) { - m_client.SetSslVerification(false, ""); - CurlHttpOperation op("GET", "https://example.com", nullptr, std::map(), std::vector(), false, 5, false, ""); - CURL* handle = op.GetHandle(); - ASSERT_NE(handle, nullptr); + ASSERT_NE(op.GetHandle(), nullptr); } -TEST_F(HttpClientCurlTests, SetSslVerification_CaInfoPassedToOperation) +TEST_F(HttpClientCurlTests, CurlHttpOperation_ConstructsWithCaInfo) { - const std::string caPath = "/etc/ssl/certs/ca-certificates.crt"; - m_client.SetSslVerification(true, caPath); - CurlHttpOperation op("GET", "https://example.com", nullptr, std::map(), std::vector(), - false, 5, true, caPath); - CURL* handle = op.GetHandle(); - ASSERT_NE(handle, nullptr); -} - -TEST_F(HttpClientCurlTests, SetSslVerification_EmptyCaInfoNoFailure) -{ - m_client.SetSslVerification(true, ""); - - CurlHttpOperation op("GET", "https://example.com", nullptr, - std::map(), std::vector(), - false, 5, true, ""); - CURL* handle = op.GetHandle(); - ASSERT_NE(handle, nullptr); + false, 5, true, "/etc/ssl/certs/ca-certificates.crt"); + ASSERT_NE(op.GetHandle(), nullptr); } // --- ILogConfiguration integration --- From 38cd0d3abdfc8a00dcf203ce34e28248102bb4e7 Mon Sep 17 00:00:00 2001 From: Tom Tan Date: Fri, 10 Apr 2026 16:06:22 -0700 Subject: [PATCH 3/3] Fix test --- tests/unittests/HttpClientCurlTests.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/unittests/HttpClientCurlTests.cpp b/tests/unittests/HttpClientCurlTests.cpp index 6f1e3384e..889d2ffe7 100644 --- a/tests/unittests/HttpClientCurlTests.cpp +++ b/tests/unittests/HttpClientCurlTests.cpp @@ -10,6 +10,7 @@ #include "common/Common.hpp" #include "http/HttpClient_Curl.hpp" +#include "config/RuntimeConfig_Default.hpp" using namespace testing; using namespace MAT; @@ -56,16 +57,14 @@ TEST_F(HttpClientCurlTests, CurlHttpOperation_ConstructsWithCaInfo) TEST(HttpClientCurlConfigTests, LogConfiguration_SslVerify_DefaultIsTrue) { - ILogConfiguration config; - // The default config should have sslVerify = true - bool sslVerify = config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY]; + // defaultRuntimeConfig from RuntimeConfig_Default.hpp has the defaults + bool sslVerify = defaultRuntimeConfig[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY]; EXPECT_TRUE(sslVerify); } TEST(HttpClientCurlConfigTests, LogConfiguration_SslCaInfo_DefaultIsEmpty) { - ILogConfiguration config; - const char* caInfo = config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO]; + const char* caInfo = defaultRuntimeConfig[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO]; EXPECT_STREQ(caInfo, ""); }