// // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2024 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // #include "td/net/SslCtx.h" #include "td/utils/common.h" #include "td/utils/crypto.h" #include "td/utils/FlatHashMap.h" #include "td/utils/logging.h" #include "td/utils/misc.h" #include "td/utils/port/path.h" #include "td/utils/port/wstring_convert.h" #include "td/utils/ScopeGuard.h" #include "td/utils/SliceBuilder.h" #include "td/utils/Time.h" #if !TD_EMSCRIPTEN #include <openssl/err.h> #include <openssl/ssl.h> #include <openssl/x509.h> #include <openssl/x509_vfy.h> #include <cstring> #include <memory> #include <mutex> #if TD_PORT_WINDOWS #include <wincrypt.h> #endif namespace td { namespace detail { namespace { int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { if (!preverify_ok) { char buf[256]; X509_NAME_oneline(X509_get_subject_name(X509_STORE_CTX_get_current_cert(ctx)), buf, 256); int err = X509_STORE_CTX_get_error(ctx); auto warning = PSTRING() << "verify error:num=" << err << ":" << X509_verify_cert_error_string(err) << ":depth=" << X509_STORE_CTX_get_error_depth(ctx) << ":" << Slice(buf, std::strlen(buf)); double now = Time::now(); static std::mutex warning_mutex; { std::lock_guard<std::mutex> lock(warning_mutex); static FlatHashMap<string, double> next_warning_time; double &next = next_warning_time[warning]; if (next <= now) { next = now + 300; // one warning per 5 minutes LOG(WARNING) << warning; } } } return preverify_ok; } X509_STORE *load_system_certificate_store() { int32 cert_count = 0; int32 file_count = 0; LOG(DEBUG) << "Begin to load system certificate store"; SCOPE_EXIT { LOG(DEBUG) << "End to load " << cert_count << " certificates from " << file_count << " files from system store"; if (ERR_peek_error() != 0) { auto error = create_openssl_error(-22, "Have unprocessed errors"); LOG(INFO) << error; } }; #if TD_PORT_WINDOWS auto flags = CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG | CERT_SYSTEM_STORE_CURRENT_USER; HCERTSTORE system_store = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, HCRYPTPROV_LEGACY(), flags, static_cast<const void *>(to_wstring("ROOT").ok().c_str())); if (!system_store) { return nullptr; } X509_STORE *store = X509_STORE_new(); if (store == nullptr) { return nullptr; } for (PCCERT_CONTEXT cert_context = CertEnumCertificatesInStore(system_store, nullptr); cert_context != nullptr; cert_context = CertEnumCertificatesInStore(system_store, cert_context)) { const unsigned char *in = cert_context->pbCertEncoded; X509 *x509 = d2i_X509(nullptr, &in, static_cast<long>(cert_context->cbCertEncoded)); if (x509 != nullptr) { if (X509_STORE_add_cert(store, x509) != 1) { auto error_code = ERR_peek_error(); auto error = create_openssl_error(-20, "Failed to add certificate"); if (ERR_GET_REASON(error_code) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { LOG(ERROR) << error; } else { LOG(INFO) << error; } } else { cert_count++; } X509_free(x509); } else { LOG(ERROR) << create_openssl_error(-21, "Failed to load X509 certificate"); } } CertCloseStore(system_store, 0); #else X509_STORE *store = X509_STORE_new(); if (store == nullptr) { return nullptr; } auto add_file = [&](CSlice path) { if (X509_STORE_load_locations(store, path.c_str(), nullptr) != 1) { auto error = create_openssl_error(-20, "Failed to add certificate"); LOG(INFO) << path << ": " << error; } else { file_count++; } }; string default_cert_dir = X509_get_default_cert_dir(); for (auto cert_dir : full_split(default_cert_dir, ':')) { walk_path(cert_dir, [&](CSlice path, WalkPath::Type type) { if (type != WalkPath::Type::RegularFile && type != WalkPath::Type::Symlink) { return type == WalkPath::Type::EnterDir && path != cert_dir ? WalkPath::Action::SkipDir : WalkPath::Action::Continue; } add_file(path); return WalkPath::Action::Continue; }).ignore(); } string default_cert_path = X509_get_default_cert_file(); if (!default_cert_path.empty()) { add_file(default_cert_path); } #if OPENSSL_VERSION_NUMBER >= 0x10100000L auto objects = X509_STORE_get0_objects(store); cert_count = objects == nullptr ? 0 : sk_X509_OBJECT_num(objects); #else cert_count = -1; #endif #endif return store; } using SslCtxPtr = std::shared_ptr<SSL_CTX>; Result<SslCtxPtr> do_create_ssl_ctx(CSlice cert_file, SslCtx::VerifyPeer verify_peer) { auto ssl_method = #if OPENSSL_VERSION_NUMBER >= 0x10100000L TLS_client_method(); #else SSLv23_client_method(); #endif if (ssl_method == nullptr) { return create_openssl_error(-6, "Failed to create an SSL client method"); } auto ssl_ctx = SSL_CTX_new(ssl_method); if (!ssl_ctx) { return create_openssl_error(-7, "Failed to create an SSL context"); } auto ssl_ctx_ptr = SslCtxPtr(ssl_ctx, SSL_CTX_free); long options = 0; #ifdef SSL_OP_NO_SSLv2 options |= SSL_OP_NO_SSLv2; #endif #ifdef SSL_OP_NO_SSLv3 options |= SSL_OP_NO_SSLv3; #endif SSL_CTX_set_options(ssl_ctx, options); #if OPENSSL_VERSION_NUMBER >= 0x10100000L SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_VERSION); #endif SSL_CTX_set_mode(ssl_ctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_ENABLE_PARTIAL_WRITE); if (cert_file.empty()) { auto *store = load_system_certificate_store(); if (store == nullptr) { auto error = create_openssl_error(-8, "Failed to load system certificate store"); if (verify_peer == SslCtx::VerifyPeer::On) { return std::move(error); } else { LOG(ERROR) << error; } } else { SSL_CTX_set_cert_store(ssl_ctx, store); } } else { if (SSL_CTX_load_verify_locations(ssl_ctx, cert_file.c_str(), nullptr) == 0) { return create_openssl_error(-8, "Failed to set custom certificate file"); } } if (verify_peer == SslCtx::VerifyPeer::On) { SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, verify_callback); constexpr int DEFAULT_VERIFY_DEPTH = 10; SSL_CTX_set_verify_depth(ssl_ctx, DEFAULT_VERIFY_DEPTH); } else { SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_NONE, nullptr); } string cipher_list; if (SSL_CTX_set_cipher_list(ssl_ctx, cipher_list.empty() ? "DEFAULT" : cipher_list.c_str()) == 0) { return create_openssl_error(-9, PSLICE() << "Failed to set cipher list \"" << cipher_list << '"'); } return std::move(ssl_ctx_ptr); } Result<SslCtxPtr> get_default_ssl_ctx() { static auto ctx = do_create_ssl_ctx(CSlice(), SslCtx::VerifyPeer::On); if (ctx.is_error()) { return ctx.error().clone(); } return ctx.ok(); } Result<SslCtxPtr> get_default_unverified_ssl_ctx() { static auto ctx = do_create_ssl_ctx(CSlice(), SslCtx::VerifyPeer::Off); if (ctx.is_error()) { return ctx.error().clone(); } return ctx.ok(); } } // namespace class SslCtxImpl { public: Status init(CSlice cert_file, SslCtx::VerifyPeer verify_peer) { SslCtx::init_openssl(); clear_openssl_errors("Before SslCtx::init"); if (cert_file.empty()) { if (verify_peer == SslCtx::VerifyPeer::On) { TRY_RESULT_ASSIGN(ssl_ctx_ptr_, get_default_ssl_ctx()); } else { TRY_RESULT_ASSIGN(ssl_ctx_ptr_, get_default_unverified_ssl_ctx()); } return Status::OK(); } auto start_time = Time::now(); auto r_ssl_ctx_ptr = do_create_ssl_ctx(cert_file, verify_peer); auto elapsed_time = Time::now() - start_time; if (elapsed_time >= 0.1) { LOG(WARNING) << "SSL context creation took " << elapsed_time << " seconds"; } if (r_ssl_ctx_ptr.is_error()) { return r_ssl_ctx_ptr.move_as_error(); } ssl_ctx_ptr_ = r_ssl_ctx_ptr.move_as_ok(); return Status::OK(); } void *get_openssl_ctx() const { return static_cast<void *>(ssl_ctx_ptr_.get()); } private: SslCtxPtr ssl_ctx_ptr_; }; } // namespace detail SslCtx::SslCtx() = default; SslCtx::SslCtx(const SslCtx &other) { if (other.impl_) { impl_ = make_unique<detail::SslCtxImpl>(*other.impl_); } } SslCtx &SslCtx::operator=(const SslCtx &other) { if (other.impl_) { impl_ = make_unique<detail::SslCtxImpl>(*other.impl_); } else { impl_ = nullptr; } return *this; } SslCtx::SslCtx(SslCtx &&) noexcept = default; SslCtx &SslCtx::operator=(SslCtx &&) noexcept = default; SslCtx::~SslCtx() = default; void SslCtx::init_openssl() { static bool is_inited = [] { #if OPENSSL_VERSION_NUMBER >= 0x10100000L return OPENSSL_init_ssl(0, nullptr) != 0; #else OpenSSL_add_all_algorithms(); SSL_load_error_strings(); return OpenSSL_add_ssl_algorithms() != 0; #endif }(); CHECK(is_inited); } Result<SslCtx> SslCtx::create(CSlice cert_file, VerifyPeer verify_peer) { auto impl = make_unique<detail::SslCtxImpl>(); TRY_STATUS(impl->init(cert_file, verify_peer)); return SslCtx(std::move(impl)); } void *SslCtx::get_openssl_ctx() const { return impl_ == nullptr ? nullptr : impl_->get_openssl_ctx(); } SslCtx::SslCtx(unique_ptr<detail::SslCtxImpl> impl) : impl_(std::move(impl)) { } } // namespace td #else namespace td { namespace detail { class SslCtxImpl {}; } // namespace detail SslCtx::SslCtx() = default; SslCtx::SslCtx(const SslCtx &other) { UNREACHABLE(); } SslCtx &SslCtx::operator=(const SslCtx &other) { UNREACHABLE(); return *this; } SslCtx::SslCtx(SslCtx &&) noexcept = default; SslCtx &SslCtx::operator=(SslCtx &&) noexcept = default; SslCtx::~SslCtx() = default; void SslCtx::init_openssl() { } Result<SslCtx> SslCtx::create(CSlice cert_file, VerifyPeer verify_peer) { return Status::Error("Not supported in Emscripten"); } void *SslCtx::get_openssl_ctx() const { return nullptr; } SslCtx::SslCtx(unique_ptr<detail::SslCtxImpl> impl) : impl_(std::move(impl)) { } } // namespace td #endif