Skip to content

Commit d0eb633

Browse files
committed
feat crypto: use curl blob interface for client certs
1 parent a217f05 commit d0eb633

File tree

14 files changed

+471
-45
lines changed

14 files changed

+471
-45
lines changed

.mapping.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3288,13 +3288,15 @@
32883288
"universal/src/crypto/base64.cpp":"taxi/uservices/userver/universal/src/crypto/base64.cpp",
32893289
"universal/src/crypto/base64_test.cpp":"taxi/uservices/userver/universal/src/crypto/base64_test.cpp",
32903290
"universal/src/crypto/certificate.cpp":"taxi/uservices/userver/universal/src/crypto/certificate.cpp",
3291+
"universal/src/crypto/certificate_test.cpp":"taxi/uservices/userver/universal/src/crypto/certificate_test.cpp",
32913292
"universal/src/crypto/hash.cpp":"taxi/uservices/userver/universal/src/crypto/hash.cpp",
32923293
"universal/src/crypto/hash_test.cpp":"taxi/uservices/userver/universal/src/crypto/hash_test.cpp",
32933294
"universal/src/crypto/helpers.cpp":"taxi/uservices/userver/universal/src/crypto/helpers.cpp",
32943295
"universal/src/crypto/helpers.hpp":"taxi/uservices/userver/universal/src/crypto/helpers.hpp",
32953296
"universal/src/crypto/openssl.cpp":"taxi/uservices/userver/universal/src/crypto/openssl.cpp",
32963297
"universal/src/crypto/openssl.hpp":"taxi/uservices/userver/universal/src/crypto/openssl.hpp",
32973298
"universal/src/crypto/private_key.cpp":"taxi/uservices/userver/universal/src/crypto/private_key.cpp",
3299+
"universal/src/crypto/private_key_test.cpp":"taxi/uservices/userver/universal/src/crypto/private_key_test.cpp",
32983300
"universal/src/crypto/public_key.cpp":"taxi/uservices/userver/universal/src/crypto/public_key.cpp",
32993301
"universal/src/crypto/public_key_test.cpp":"taxi/uservices/userver/universal/src/crypto/public_key_test.cpp",
33003302
"universal/src/crypto/random.cpp":"taxi/uservices/userver/universal/src/crypto/random.cpp",

core/src/clients/http/client_crl_test.cpp

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@
1717
#include <userver/utest/utest.hpp>
1818

1919
// Tests in this file make sure that certs do not check CRLs by default.
20-
//
21-
// Fails on MacOS with Segmentation fault while calling
22-
// Request::RequestImpl::on_certificate_request, probably because CURL library
23-
// was misconfigured and uses wrong version of OpenSSL.
2420

2521
USERVER_NAMESPACE_BEGIN
2622

@@ -369,7 +365,7 @@ auto InterceptCrlDistribution() {
369365

370366
} // namespace
371367

372-
UTEST(HttpClient, DISABLED_IN_MAC_OS_TEST_NAME(HttpsWithNoCrl)) {
368+
UTEST(HttpClient, HttpsWithNoCrl) {
373369
(void)kCrlFile;
374370
auto task = InterceptCrlDistribution();
375371

@@ -396,7 +392,7 @@ UTEST(HttpClient, DISABLED_IN_MAC_OS_TEST_NAME(HttpsWithNoCrl)) {
396392
EXPECT_EQ(resp->body_view(), "OK");
397393
}
398394

399-
UTEST(HttpClient, DISABLED_IN_MAC_OS_TEST_NAME(HttpsWithCrl)) {
395+
UTEST(HttpClient, HttpsWithCrl) {
400396
auto tmp_file = fs::blocking::TempFile::Create();
401397
fs::blocking::RewriteFileContents(tmp_file.GetPath(), kCrlFile);
402398

@@ -424,7 +420,7 @@ UTEST(HttpClient, DISABLED_IN_MAC_OS_TEST_NAME(HttpsWithCrl)) {
424420
UEXPECT_THROW(response_future.Get(), clients::http::SSLException);
425421
}
426422

427-
UTEST(HttpClient, DISABLED_IN_MAC_OS_TEST_NAME(HttpsWithCrlNoVerify)) {
423+
UTEST(HttpClient, HttpsWithCrlNoVerify) {
428424
auto tmp_file = fs::blocking::TempFile::Create();
429425
fs::blocking::RewriteFileContents(tmp_file.GetPath(), kCrlFile);
430426

@@ -452,7 +448,7 @@ UTEST(HttpClient, DISABLED_IN_MAC_OS_TEST_NAME(HttpsWithCrlNoVerify)) {
452448
UEXPECT_NO_THROW(response_future.Get());
453449
}
454450

455-
UTEST(HttpClient, DISABLED_IN_MAC_OS_TEST_NAME(HttpsWithNoServerCa)) {
451+
UTEST(HttpClient, HttpsWithNoServerCa) {
456452
(void)kCrlFile;
457453
auto task = InterceptCrlDistribution();
458454

@@ -479,7 +475,7 @@ UTEST(HttpClient, DISABLED_IN_MAC_OS_TEST_NAME(HttpsWithNoServerCa)) {
479475
EXPECT_EQ(resp->body_view(), "OK");
480476
}
481477

482-
UTEST(HttpClient, DISABLED_IN_MAC_OS_TEST_NAME(HttpsWithNoClientCa)) {
478+
UTEST(HttpClient, HttpsWithNoClientCa) {
483479
auto task = InterceptCrlDistribution();
484480
auto pkey = crypto::PrivateKey::LoadFromString(kRevokedClientPrivateKey, "");
485481
auto cert = crypto::Certificate::LoadFromString(kClientCertificate);

core/src/clients/http/plugins/yandex_tracing/plugin.hpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#pragma once
22

33
#include <userver/clients/http/plugin.hpp>
4-
#include <userver/dynamic_config/fwd.hpp>
54

65
USERVER_NAMESPACE_BEGIN
76

core/src/clients/http/request_state.cpp

Lines changed: 81 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <map>
66
#include <string_view>
77

8+
#include <cryptopp/osrng.h>
89
#include <fmt/chrono.h>
910
#include <fmt/format.h>
1011
#include <openssl/ssl.h>
@@ -31,6 +32,11 @@ USERVER_NAMESPACE_BEGIN
3132
namespace clients::http {
3233

3334
namespace {
35+
namespace nolint {
36+
// NOLINT(bugprone-forward-declaration-namespace) due to CryptoPP::Source
37+
using UnusedConfigSourceFwd = dynamic_config::Source&;
38+
} // namespace nolint
39+
3440
/// Default timeout
3541
constexpr auto kDefaultTimeout = std::chrono::milliseconds{100};
3642
/// Maximum number of redirects
@@ -156,6 +162,24 @@ class MaybeOwnedUrl final {
156162
const curl::url* url_ptr_{nullptr};
157163
};
158164

165+
// Instance-wide password used to avoid passing unencrypted keys
166+
[[maybe_unused]] const std::string& GetPkeyPassword() {
167+
static const std::string password = [] {
168+
CryptoPP::DefaultAutoSeededRNG prng;
169+
std::string random_bytes;
170+
random_bytes.resize(32);
171+
// password should not contain '\0' (cURL only accepts C-style strings)
172+
while (random_bytes.find('\0') != std::string::npos) {
173+
static_assert(sizeof(std::string::value_type) == 1,
174+
"string does not consist of bytes");
175+
prng.GenerateBlock(reinterpret_cast<unsigned char*>(random_bytes.data()),
176+
random_bytes.size());
177+
}
178+
return random_bytes;
179+
}();
180+
return password;
181+
}
182+
159183
} // namespace
160184

161185
RequestState::RequestState(
@@ -208,9 +232,17 @@ void RequestState::ca_info(const std::string& file_path) {
208232
}
209233

210234
void RequestState::ca(crypto::Certificate cert) {
211-
ca_ = std::move(cert);
212-
easy().set_ssl_ctx_function(&RequestState::on_certificate_request);
213-
easy().set_ssl_ctx_data(this);
235+
UINVARIANT(cert, "No certificate");
236+
if constexpr (curl::easy::is_set_ca_info_blob_available) {
237+
auto cert_pem = cert.GetPemString();
238+
UINVARIANT(cert_pem, "Could not serialize certificate");
239+
easy().set_ca_info_blob_copy(*cert_pem);
240+
} else {
241+
// Legacy non-portable way, broken since 7.87.0
242+
ca_ = std::move(cert);
243+
easy().set_ssl_ctx_function(&RequestState::on_certificate_request);
244+
easy().set_ssl_ctx_data(this);
245+
}
214246
}
215247

216248
void RequestState::crl_file(const std::string& file_path) {
@@ -222,40 +254,54 @@ void RequestState::client_key_cert(crypto::PrivateKey pkey,
222254
UINVARIANT(pkey, "No private key");
223255
UINVARIANT(cert, "No certificate");
224256

225-
pkey_ = std::move(pkey);
226-
cert_ = std::move(cert);
227-
228-
// FIXME: until cURL 7.71 there is no sane way to pass TLS keys from memory.
229-
// Because of this, we provide our own callback. As a consequence, cURL has
230-
// no knowledge of the key used and may reuse this connection for a request
231-
// with a different key or without one.
232-
// To avoid this until we can upgrade we set the EGD socket option to
233-
// an unusable certificate-specific value. This option should have no effect
234-
// on systems targeted by userver anyway but it is accounted when checking
235-
// cached connection eligibility which is exactly what we need.
236-
237-
// must be larger than sizeof(sockaddr_un::sun_path)
238-
static constexpr size_t kCertIdLength = 255;
239-
240-
// backwards incompatibility
257+
if constexpr (curl::easy::is_set_ssl_cert_blob_available &&
258+
curl::easy::is_set_ssl_key_blob_available) {
259+
auto cert_pem = cert.GetPemString();
260+
UINVARIANT(cert_pem, "Could not serialize certificate");
261+
easy().set_ssl_cert_blob_copy(*cert_pem);
262+
easy().set_ssl_cert_type("PEM");
263+
auto key_pem = pkey.GetPemString(GetPkeyPassword());
264+
UINVARIANT(key_pem, "Could not serialize private key");
265+
easy().set_ssl_key_blob_copy(*key_pem);
266+
easy().set_ssl_key_passwd(GetPkeyPassword());
267+
easy().set_ssl_key_type("PEM");
268+
} else {
269+
// Legacy non-portable way, broken since 7.84.0
270+
pkey_ = std::move(pkey);
271+
cert_ = std::move(cert);
272+
273+
// FIXME: until cURL 7.71 there is no sane way to pass TLS keys from memory.
274+
// Because of this, we provide our own callback. As a consequence, cURL has
275+
// no knowledge of the key used and may reuse this connection for a request
276+
// with a different key or without one.
277+
// To avoid this until we can upgrade we set the EGD socket option to
278+
// an unusable certificate-specific value. This option should have no effect
279+
// on systems targeted by userver anyway but it is accounted when checking
280+
// cached connection eligibility which is exactly what we need.
281+
282+
// must be larger than sizeof(sockaddr_un::sun_path)
283+
static constexpr size_t kCertIdLength = 255;
284+
285+
// backwards incompatibility
241286
#if OPENSSL_VERSION_NUMBER >= 0x010100000L
242-
const
287+
const
243288
#endif
244-
ASN1_BIT_STRING* cert_sig = nullptr;
245-
X509_get0_signature(&cert_sig, nullptr, cert_.GetNative());
246-
UINVARIANT(cert_sig, "Cannot get X509 certificate signature");
247-
248-
std::string cert_id;
249-
cert_id.reserve(kCertIdLength);
250-
utils::encoding::ToHex(
251-
std::string_view{reinterpret_cast<const char*>(cert_sig->data),
252-
std::min<size_t>(cert_sig->length, kCertIdLength / 2)},
253-
cert_id);
254-
cert_id.resize(kCertIdLength, '=');
255-
easy().set_egd_socket(cert_id);
256-
257-
easy().set_ssl_ctx_function(&RequestState::on_certificate_request);
258-
easy().set_ssl_ctx_data(this);
289+
ASN1_BIT_STRING* cert_sig = nullptr;
290+
X509_get0_signature(&cert_sig, nullptr, cert_.GetNative());
291+
UINVARIANT(cert_sig, "Cannot get X509 certificate signature");
292+
293+
std::string cert_id;
294+
cert_id.reserve(kCertIdLength);
295+
utils::encoding::ToHex(
296+
std::string_view{reinterpret_cast<const char*>(cert_sig->data),
297+
std::min<size_t>(cert_sig->length, kCertIdLength / 2)},
298+
cert_id);
299+
cert_id.resize(kCertIdLength, '=');
300+
easy().set_egd_socket(cert_id);
301+
302+
easy().set_ssl_ctx_function(&RequestState::on_certificate_request);
303+
easy().set_ssl_ctx_data(this);
304+
}
259305
}
260306

261307
void RequestState::http_version(curl::easy::http_version_t version) {

core/src/curl-ev/easy.hpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,50 @@ class ThreadControl;
9191
native::curl_easy_setopt(handle_, OPTION_NAME, str.c_str()))); \
9292
}
9393

94+
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
95+
#define IMPLEMENT_CURL_OPTION_BLOB(FUNCTION_NAME, OPTION_NAME) \
96+
private: \
97+
inline void FUNCTION_NAME##_impl(std::string_view sv, unsigned int flags, \
98+
std::error_code& ec) { \
99+
native::curl_blob blob{}; \
100+
/* NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) */ \
101+
blob.data = const_cast<void*>(static_cast<const void*>(sv.data())); \
102+
blob.len = sv.size(); \
103+
blob.flags = flags; \
104+
ec = std::error_code(static_cast<errc::EasyErrorCode>( \
105+
native::curl_easy_setopt(handle_, OPTION_NAME, &blob))); \
106+
} \
107+
\
108+
public: \
109+
static constexpr bool is_##FUNCTION_NAME##_available = true; \
110+
inline void FUNCTION_NAME##_copy(std::string_view sv) { \
111+
std::error_code ec; \
112+
FUNCTION_NAME##_copy(sv, ec); \
113+
throw_error(ec, PP_STRINGIZE(FUNCTION_NAME##_copy)); \
114+
} \
115+
inline void FUNCTION_NAME##_no_copy(std::string_view sv) { \
116+
std::error_code ec; \
117+
FUNCTION_NAME##_no_copy(sv, ec); \
118+
throw_error(ec, PP_STRINGIZE(FUNCTION_NAME##_no_copy)); \
119+
} \
120+
inline void FUNCTION_NAME##_copy(std::string_view sv, std::error_code& ec) { \
121+
FUNCTION_NAME##_impl(sv, CURL_BLOB_COPY, ec); \
122+
} \
123+
inline void FUNCTION_NAME##_no_copy(std::string_view sv, \
124+
std::error_code& ec) { \
125+
FUNCTION_NAME##_impl(sv, CURL_BLOB_NOCOPY, ec); \
126+
}
127+
128+
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
129+
#define DELETE_CURL_OPTION_BLOB(FUNCTION_NAME) \
130+
static constexpr bool is_##FUNCTION_NAME##_available = false; \
131+
inline void FUNCTION_NAME##_copy(std::string_view) = delete; \
132+
inline void FUNCTION_NAME##_no_copy(std::string_view) = delete; \
133+
inline void FUNCTION_NAME##_copy(std::string_view, std::error_code&) = \
134+
delete; \
135+
inline void FUNCTION_NAME##_no_copy(std::string_view, std::error_code&) = \
136+
delete;
137+
94138
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
95139
#define IMPLEMENT_CURL_OPTION_GET_STRING_VIEW(FUNCTION_NAME, OPTION_NAME) \
96140
inline std::string_view FUNCTION_NAME() { \
@@ -558,6 +602,13 @@ class easy final : public std::enable_shared_from_this<easy> {
558602
IMPLEMENT_CURL_OPTION_STRING(set_ssl_engine, native::CURLOPT_SSLENGINE);
559603
IMPLEMENT_CURL_OPTION_STRING(set_ssl_engine_default,
560604
native::CURLOPT_SSLENGINE_DEFAULT);
605+
#if LIBCURL_VERSION_NUM >= 0x074700
606+
IMPLEMENT_CURL_OPTION_BLOB(set_ssl_cert_blob, native::CURLOPT_SSLCERT_BLOB);
607+
IMPLEMENT_CURL_OPTION_BLOB(set_ssl_key_blob, native::CURLOPT_SSLKEY_BLOB);
608+
#else
609+
DELETE_CURL_OPTION_BLOB(set_ssl_cert_blob);
610+
DELETE_CURL_OPTION_BLOB(set_ssl_key_blob);
611+
#endif
561612
enum ssl_version_t {
562613
ssl_version_default = native::CURL_SSLVERSION_DEFAULT,
563614
ssl_version_tls_v1 = native::CURL_SSLVERSION_TLSv1,
@@ -569,6 +620,11 @@ class easy final : public std::enable_shared_from_this<easy> {
569620
IMPLEMENT_CURL_OPTION_BOOLEAN(set_ssl_verify_peer,
570621
native::CURLOPT_SSL_VERIFYPEER);
571622
IMPLEMENT_CURL_OPTION_STRING(set_ca_info, native::CURLOPT_CAINFO);
623+
#if LIBCURL_VERSION_NUM >= 0x074D00
624+
IMPLEMENT_CURL_OPTION_BLOB(set_ca_info_blob, native::CURLOPT_CAINFO_BLOB);
625+
#else
626+
DELETE_CURL_OPTION_BLOB(set_ca_info_blob);
627+
#endif
572628
IMPLEMENT_CURL_OPTION_STRING(set_issuer_cert, native::CURLOPT_ISSUERCERT);
573629
IMPLEMENT_CURL_OPTION_STRING(set_ca_file, native::CURLOPT_CAPATH);
574630
IMPLEMENT_CURL_OPTION_STRING(set_crl_file, native::CURLOPT_CRLFILE);

universal/include/userver/crypto/certificate.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
/// @brief @copybrief crypto::Certificate
55

66
#include <memory>
7+
#include <optional>
8+
#include <string>
79
#include <string_view>
810

911
#include <userver/crypto/basic_types.hpp>
@@ -24,6 +26,11 @@ class Certificate {
2426
NativeType* GetNative() const noexcept { return cert_.get(); }
2527
explicit operator bool() const noexcept { return !!cert_; }
2628

29+
/// Returns a PEM-encoded representation of stored certificate.
30+
///
31+
/// @throw crypto::SerializationError if serialization fails.
32+
std::optional<std::string> GetPemString() const;
33+
2734
/// Accepts a string that contains a certificate, checks that
2835
/// it's correct, loads it into OpenSSL structures and returns as a
2936
/// Certificate variable.

universal/include/userver/crypto/exception.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ class KeyParseError : public CryptoException {
3535
using CryptoException::CryptoException;
3636
};
3737

38+
/// Serialization error
39+
class SerializationError : public CryptoException {
40+
public:
41+
using CryptoException::CryptoException;
42+
};
43+
3844
} // namespace crypto
3945

4046
USERVER_NAMESPACE_END

universal/include/userver/crypto/private_key.hpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
/// @brief @copybrief crypto::PrivateKey
55

66
#include <memory>
7+
#include <optional>
8+
#include <string>
79
#include <string_view>
810

911
#include <userver/crypto/basic_types.hpp>
@@ -24,6 +26,19 @@ class PrivateKey {
2426
NativeType* GetNative() const noexcept { return pkey_.get(); }
2527
explicit operator bool() const noexcept { return !!pkey_; }
2628

29+
/// Returns a PEM-encoded representation of stored private key encrypted by
30+
/// the provided password.
31+
///
32+
/// @throw crypto::SerializationError if the password is empty or
33+
/// serialization fails.
34+
std::optional<std::string> GetPemString(std::string_view password) const;
35+
36+
/// Returns a PEM-encoded representation of stored private key in an
37+
/// unencrypted form.
38+
///
39+
/// @throw crypto::SerializationError if serialization fails.
40+
std::optional<std::string> GetPemStringUnencrypted() const;
41+
2742
/// Accepts a string that contains a private key and a password, checks the
2843
/// key and password, loads it into OpenSSL structures and returns as a
2944
/// PrivateKey variable.

0 commit comments

Comments
 (0)