Skip to content

Commit ff76b4e

Browse files
authored
Enable configurable SSL certificate verification for libcurl HTTP client (#1423)
1 parent a10be7f commit ff76b4e

10 files changed

Lines changed: 197 additions & 19 deletions

lib/api/LogManagerImpl.cpp

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -289,13 +289,7 @@ namespace MAT_NS_BEGIN
289289
if (m_httpClient == nullptr)
290290
{
291291
m_httpClient = HttpClientFactory::Create();
292-
#ifdef HAVE_MAT_WININET_HTTP_CLIENT
293-
HttpClient_WinInet* client = static_cast<HttpClient_WinInet*>(m_httpClient.get());
294-
if (client != nullptr)
295-
{
296-
client->SetMsRootCheck(m_logConfiguration[CFG_MAP_HTTP][CFG_BOOL_HTTP_MS_ROOT_CHECK]);
297-
}
298-
#endif
292+
m_httpClient->ApplySettings(m_logConfiguration);
299293
}
300294
else
301295
{
@@ -366,14 +360,10 @@ namespace MAT_NS_BEGIN
366360
/// </summary>
367361
void LogManagerImpl::Configure()
368362
{
369-
// TODO: [maxgolov] - add other config params.
370-
#ifdef HAVE_MAT_WININET_HTTP_CLIENT
371-
HttpClient_WinInet* client = static_cast<HttpClient_WinInet*>(m_httpClient.get());
372-
if (client != nullptr)
363+
if (m_httpClient != nullptr)
373364
{
374-
client->SetMsRootCheck(m_logConfiguration[CFG_MAP_HTTP][CFG_BOOL_HTTP_MS_ROOT_CHECK]);
365+
m_httpClient->ApplySettings(m_logConfiguration);
375366
}
376-
#endif
377367
}
378368

379369
LogManagerImpl::~LogManagerImpl() noexcept

lib/config/RuntimeConfig_Default.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ namespace MAT_NS_BEGIN
6060
,
6161
{"contentEncoding", "deflate"},
6262
/* Optional parameter to require Microsoft Root CA */
63-
{CFG_BOOL_HTTP_MS_ROOT_CHECK, false}}},
63+
{CFG_BOOL_HTTP_MS_ROOT_CHECK, false},
64+
/* Optional parameter for SSL certificate verification (curl) */
65+
{CFG_BOOL_HTTP_SSL_VERIFY, true},
66+
/* Optional CA bundle path for OpenSSL-backed curl */
67+
{CFG_STR_HTTP_SSL_CAINFO, ""}}},
6468
{CFG_MAP_TPM,
6569
{
6670
{CFG_INT_TPM_MAX_BLOB_BYTES, 2097152},

lib/http/HttpClient_Curl.cpp

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#include "utils/Utils.hpp"
1616
#include "HttpClient_Curl.hpp"
17+
#include "ILogConfiguration.hpp"
1718

1819
namespace MAT_NS_BEGIN {
1920

@@ -74,7 +75,13 @@ namespace MAT_NS_BEGIN {
7475
requestHeaders[header.first] = header.second;
7576
}
7677

77-
auto curlOperation = std::make_shared<CurlHttpOperation>(curlRequest->m_method, curlRequest->m_url, callback, requestHeaders, curlRequest->m_body);
78+
std::string sslCaInfo;
79+
{
80+
std::lock_guard<std::mutex> lock(m_requestsMtx);
81+
sslCaInfo = m_sslCaInfo;
82+
}
83+
84+
auto curlOperation = std::make_shared<CurlHttpOperation>(curlRequest->m_method, curlRequest->m_url, callback, requestHeaders, curlRequest->m_body, false, HTTP_CONN_TIMEOUT, m_sslVerify, sslCaInfo);
7885
curlRequest->SetOperation(curlOperation);
7986

8087
// The lifetime of curlOperation is guarnteed by the call to result.wait() in the d'tor.
@@ -125,6 +132,20 @@ namespace MAT_NS_BEGIN {
125132
}
126133
}
127134

135+
void HttpClient_Curl::ApplySettings(ILogConfiguration& config)
136+
{
137+
SetSslVerification(
138+
config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY],
139+
(const char *)config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO]);
140+
}
141+
142+
void HttpClient_Curl::SetSslVerification(bool sslVerify, const std::string& caInfo)
143+
{
144+
m_sslVerify = sslVerify;
145+
std::lock_guard<std::mutex> lock(m_requestsMtx);
146+
m_sslCaInfo = caInfo;
147+
}
148+
128149
void HttpClient_Curl::EraseRequest(std::string const& id)
129150
{
130151
std::lock_guard<std::mutex> lock(m_requestsMtx);

lib/http/HttpClient_Curl.hpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,17 @@ class HttpClient_Curl : public IHttpClient {
5555
virtual void SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback) override;
5656
virtual void CancelRequestAsync(std::string const& id) override;
5757

58+
virtual void ApplySettings(ILogConfiguration& config) override;
59+
void SetSslVerification(bool sslVerify, const std::string& caInfo = "");
60+
5861
private:
5962
void EraseRequest(std::string const& id);
6063
void AddRequest(IHttpRequest* request);
6164

6265
std::mutex m_requestsMtx;
6366
std::map<std::string, IHttpRequest*> m_requests;
67+
std::atomic<bool> m_sslVerify { true };
68+
std::string m_sslCaInfo;
6469
};
6570

6671
class CurlHttpOperation {
@@ -91,7 +96,10 @@ class CurlHttpOperation {
9196
const std::vector<uint8_t>& requestBody = std::vector<uint8_t>(),
9297
// Default connectivity and response size options
9398
bool rawResponse = false,
94-
size_t httpConnTimeout = HTTP_CONN_TIMEOUT) :
99+
size_t httpConnTimeout = HTTP_CONN_TIMEOUT,
100+
// SSL certificate verification options
101+
bool sslVerify = true,
102+
const std::string& sslCaInfo = "") :
95103

96104
// Optional connection params
97105
rawResponse(rawResponse),
@@ -129,9 +137,11 @@ class CurlHttpOperation {
129137
// Specify target URL
130138
curl_easy_setopt(curl, CURLOPT_URL, m_url.c_str());
131139

132-
// TODO: expose SSL cert verification opts via ILogConfiguration
133-
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); // 1L
134-
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); // 2L
140+
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, sslVerify ? 1L : 0L);
141+
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, sslVerify ? 2L : 0L);
142+
if (!sslCaInfo.empty()) {
143+
curl_easy_setopt(curl, CURLOPT_CAINFO, sslCaInfo.c_str());
144+
}
135145
// HTTP/2 please, fallback to HTTP/1.1 if not supported
136146
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
137147

lib/http/HttpClient_WinInet.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,11 @@ void HttpClient_WinInet::CancelAllRequests()
546546
/// Enforces MS-root server certificate check.
547547
/// </summary>
548548
/// <param name="enforceMsRoot">if set to <c>true</c> [enforce verification that server cert is MS-Rooted].</param>
549+
void HttpClient_WinInet::ApplySettings(ILogConfiguration& config)
550+
{
551+
SetMsRootCheck(config[CFG_MAP_HTTP][CFG_BOOL_HTTP_MS_ROOT_CHECK]);
552+
}
553+
549554
void HttpClient_WinInet::SetMsRootCheck(bool enforceMsRoot)
550555
{
551556
m_msRootCheck = enforceMsRoot;

lib/http/HttpClient_WinInet.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class HttpClient_WinInet : public IHttpClient {
3030
virtual void CancelRequestAsync(std::string const& id) final;
3131
virtual void CancelAllRequests() final;
3232

33+
virtual void ApplySettings(ILogConfiguration& config) override;
34+
3335
// Methods unique to WinInet implementation.
3436
void SetMsRootCheck(bool enforceMsRoot);
3537
bool IsMsRootCheckRequired();

lib/include/public/IHttpClient.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
///@cond INTERNAL_DOCS
1919
namespace MAT_NS_BEGIN
2020
{
21+
class ILogConfiguration;
2122
/// <summary>
2223
/// The HttpHeaders class contains a set of HTTP headers.
2324
/// </summary>
@@ -543,6 +544,14 @@ namespace MAT_NS_BEGIN
543544
virtual void CancelRequestAsync(std::string const& id) = 0;
544545

545546
virtual void CancelAllRequests() {}
547+
548+
/// <summary>
549+
/// Apply HTTP settings from the log configuration.
550+
/// Subclasses override to handle platform-specific options.
551+
/// Default implementation is a no-op.
552+
/// </summary>
553+
/// <param name="config">The log configuration to read settings from.</param>
554+
virtual void ApplySettings(ILogConfiguration& /*config*/) {}
546555
};
547556

548557
/// @endcond

lib/include/public/ILogConfiguration.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,16 @@ namespace MAT_NS_BEGIN
361361
/// </summary>
362362
static constexpr const char* const CFG_BOOL_HTTP_COMPRESSION = "compress";
363363

364+
/// <summary>
365+
/// HTTP configuration: SSL certificate verification (peer + host)
366+
/// </summary>
367+
static constexpr const char* const CFG_BOOL_HTTP_SSL_VERIFY = "sslVerify";
368+
369+
/// <summary>
370+
/// HTTP configuration: SSL CA bundle file path (for libcurl/OpenSSL)
371+
/// </summary>
372+
static constexpr const char* const CFG_STR_HTTP_SSL_CAINFO = "sslCaInfo";
373+
364374
/// <summary>
365375
/// TPM configuration map
366376
/// </summary>

tests/unittests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ set(SRCS
1919
EventPropertiesTests.cpp
2020
GuidTests.cpp
2121
HttpClientCAPITests.cpp
22+
HttpClientCurlTests.cpp
2223
HttpClientManagerTests.cpp
2324
HttpClientTests.cpp
2425
HttpDeflateCompressionTests.cpp
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
#include "mat/config.h"
6+
7+
// These tests only apply to the curl HTTP client path (Linux, non-Apple, non-Android)
8+
#if defined(MATSDK_PAL_CPP11) && !defined(_MSC_VER) && defined(HAVE_MAT_DEFAULT_HTTP_CLIENT) \
9+
&& !defined(__APPLE__) && !defined(ANDROID)
10+
11+
#include "common/Common.hpp"
12+
#include "http/HttpClient_Curl.hpp"
13+
#include "config/RuntimeConfig_Default.hpp"
14+
15+
using namespace testing;
16+
using namespace MAT;
17+
18+
class HttpClientCurlTests : public ::testing::Test
19+
{
20+
protected:
21+
HttpClient_Curl m_client;
22+
};
23+
24+
// --- SetSslVerification wiring ---
25+
26+
TEST_F(HttpClientCurlTests, SslVerification_DefaultsToTrue)
27+
{
28+
CurlHttpOperation op("GET", "https://example.com", nullptr);
29+
ASSERT_NE(op.GetHandle(), nullptr);
30+
}
31+
32+
TEST_F(HttpClientCurlTests, CurlHttpOperation_ConstructsWithVerifyTrue)
33+
{
34+
CurlHttpOperation op("GET", "https://example.com", nullptr,
35+
std::map<std::string, std::string>(), std::vector<uint8_t>(),
36+
false, 5, true, "");
37+
ASSERT_NE(op.GetHandle(), nullptr);
38+
}
39+
40+
TEST_F(HttpClientCurlTests, CurlHttpOperation_ConstructsWithVerifyFalse)
41+
{
42+
CurlHttpOperation op("GET", "https://example.com", nullptr,
43+
std::map<std::string, std::string>(), std::vector<uint8_t>(),
44+
false, 5, false, "");
45+
ASSERT_NE(op.GetHandle(), nullptr);
46+
}
47+
48+
TEST_F(HttpClientCurlTests, CurlHttpOperation_ConstructsWithCaInfo)
49+
{
50+
CurlHttpOperation op("GET", "https://example.com", nullptr,
51+
std::map<std::string, std::string>(), std::vector<uint8_t>(),
52+
false, 5, true, "/etc/ssl/certs/ca-certificates.crt");
53+
ASSERT_NE(op.GetHandle(), nullptr);
54+
}
55+
56+
// --- ILogConfiguration integration ---
57+
58+
TEST(HttpClientCurlConfigTests, LogConfiguration_SslVerify_DefaultIsTrue)
59+
{
60+
// defaultRuntimeConfig from RuntimeConfig_Default.hpp has the defaults
61+
bool sslVerify = defaultRuntimeConfig[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY];
62+
EXPECT_TRUE(sslVerify);
63+
}
64+
65+
TEST(HttpClientCurlConfigTests, LogConfiguration_SslCaInfo_DefaultIsEmpty)
66+
{
67+
const char* caInfo = defaultRuntimeConfig[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO];
68+
EXPECT_STREQ(caInfo, "");
69+
}
70+
71+
TEST(HttpClientCurlConfigTests, LogConfiguration_SslVerify_CanBeDisabled)
72+
{
73+
ILogConfiguration config;
74+
config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY] = false;
75+
bool sslVerify = config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY];
76+
EXPECT_FALSE(sslVerify);
77+
}
78+
79+
TEST(HttpClientCurlConfigTests, LogConfiguration_SslCaInfo_CanBeSet)
80+
{
81+
ILogConfiguration config;
82+
config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO] = "/custom/ca-bundle.crt";
83+
const char* caInfo = config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO];
84+
EXPECT_STREQ(caInfo, "/custom/ca-bundle.crt");
85+
}
86+
87+
// --- ApplySettings integration ---
88+
89+
TEST_F(HttpClientCurlTests, ApplySettings_ReadsSslConfigFromLogConfiguration)
90+
{
91+
ILogConfiguration config;
92+
config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY] = false;
93+
config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO] = "/custom/ca.pem";
94+
m_client.ApplySettings(config);
95+
// Verify indirectly -- constructing an operation should not fail
96+
SUCCEED();
97+
}
98+
99+
TEST_F(HttpClientCurlTests, ApplySettings_DefaultConfigEnablesVerification)
100+
{
101+
ILogConfiguration config;
102+
m_client.ApplySettings(config);
103+
SUCCEED();
104+
}
105+
106+
// --- Thread safety: SetSslVerification concurrent with reads ---
107+
108+
TEST_F(HttpClientCurlTests, SetSslVerification_ConcurrentCallsNoRace)
109+
{
110+
// Exercise the atomic + mutex path under contention.
111+
// No assertions on output -- this is a sanitizer/TSAN target.
112+
std::vector<std::future<void>> futures;
113+
for (int i = 0; i < 10; ++i)
114+
{
115+
futures.push_back(std::async(std::launch::async, [this, i]() {
116+
m_client.SetSslVerification(i % 2 == 0, (i % 2 == 0) ? "/some/path" : "");
117+
}));
118+
}
119+
for (auto& f : futures)
120+
{
121+
f.get();
122+
}
123+
SUCCEED();
124+
}
125+
126+
#endif // MATSDK_PAL_CPP11 && !_MSC_VER && HAVE_MAT_DEFAULT_HTTP_CLIENT

0 commit comments

Comments
 (0)