LCOV - code coverage report
Current view: top level - asio/ssl - certificate_manager.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 94.0 % 151 142
Test Date: 2026-02-20 15:38:22 Functions: 100.0 % 19 19

            Line data    Source code
       1              : #include "certificate_manager.hpp"
       2              : #include "../../util/logger.hpp"
       3              : #include <openssl/ssl.h>
       4              : #include <openssl/x509.h>
       5              : 
       6              : namespace thinger::asio {
       7              : 
       8           21 : certificate_manager::certificate_manager() 
       9           21 :     : ssl_contexts_(std::make_unique<regex_map<std::shared_ptr<boost::asio::ssl::context>>>()) {
      10           21 : }
      11              : 
      12          305 : certificate_manager& certificate_manager::instance() {
      13          305 :     static certificate_manager instance;
      14          305 :     return instance;
      15              : }
      16              : 
      17          402 : std::shared_ptr<boost::asio::ssl::context> certificate_manager::create_base_ssl_context() {
      18          402 :     auto context = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls_server);
      19          402 :     context->set_options(boost::asio::ssl::context::single_dh_use);
      20              :     
      21              :     // Enable older TLS versions if configured
      22          402 :     if (enable_legacy_protocols_) {
      23          354 :         SSL_CTX_set_security_level(context->native_handle(), 0);
      24              :     }
      25              :     
      26              :     // Set cipher list if configured
      27          402 :     if (!server_ciphers_.empty()) {
      28          354 :         SSL_CTX_set_cipher_list(context->native_handle(), server_ciphers_.c_str());
      29          354 :         if (prefer_server_ciphers_) {
      30          354 :             SSL_CTX_set_options(context->native_handle(), SSL_OP_CIPHER_SERVER_PREFERENCE);
      31              :         }
      32              :     }
      33              :     
      34          402 :     return context;
      35            0 : }
      36              : 
      37          381 : std::shared_ptr<boost::asio::ssl::context> certificate_manager::create_ssl_context(
      38              :     const std::string& cert_chain,
      39              :     const std::string& private_key) {
      40              :     
      41          381 :     auto context = create_base_ssl_context();
      42              : 
      43          381 :     boost::system::error_code ec;
      44          381 :     context->use_certificate_chain(boost::asio::buffer(cert_chain.data(), cert_chain.size()), ec);
      45          381 :     if (ec) {
      46            3 :         LOG_ERROR("Cannot set certificate chain: {}", ec.message());
      47            3 :         return nullptr;
      48              :     }
      49              : 
      50          378 :     context->use_private_key(boost::asio::buffer(private_key.data(), private_key.size()),
      51              :                            boost::asio::ssl::context::pem, ec);
      52          378 :     if (ec) {
      53            0 :         LOG_ERROR("Cannot set private key: {}", ec.message());
      54            0 :         return nullptr;
      55              :     }
      56              : 
      57          378 :     return context;
      58          381 : }
      59              : 
      60          375 : bool certificate_manager::set_certificate(const std::string& hostname, 
      61              :                                          const std::string& certificate, 
      62              :                                          const std::string& private_key) {
      63          375 :     LOG_INFO("Setting SSL certificate for domain: {}", hostname);
      64          375 :     return set_certificate(hostname, create_ssl_context(certificate, private_key));
      65              : }
      66              : 
      67          375 : bool certificate_manager::set_certificate(const std::string& hostname,
      68              :                                          std::shared_ptr<boost::asio::ssl::context> ssl_context) {
      69          375 :     LOG_INFO("Setting SSL certificate for domain: {}", hostname);
      70              :     
      71          375 :     if (hostname.empty() || !ssl_context) {
      72            6 :         return false;
      73              :     }
      74              :     
      75          369 :     std::lock_guard<std::mutex> lock(mutex_);
      76              :     
      77          369 :     std::string computed_hostname = hostname;
      78              :     
      79              :     // Check if the hostname is a wildcard (e.g., *.example.com)
      80          369 :     static std::regex wildcard_regex(R"(^\*\.(.*)$)");
      81          369 :     std::smatch match;
      82          369 :     if (std::regex_search(hostname, match, wildcard_regex)) {
      83              :         // Get domain (e.g., example.com)
      84           24 :         std::string wildcard_domain = match[1].str();
      85              :         // Replace domain dots with "\." for the regex
      86           24 :         wildcard_domain = std::regex_replace(wildcard_domain, std::regex("\\."), "\\.");
      87              :         // Compute wildcard regex hostname (std::regex doesn't support named groups)
      88              :         // This will match any subdomain followed by the domain
      89           24 :         computed_hostname = "^[^.]+\\." + wildcard_domain + "$";
      90           24 :         LOG_DEBUG("Computed wildcard certificate regex: {}", computed_hostname);
      91           24 :     }
      92              :     
      93          369 :     ssl_contexts_->set(computed_hostname, ssl_context, hostname);
      94              :     
      95          369 :     if (default_host_ == hostname) {
      96           21 :         LOG_INFO("Overriding default SSL certificate for domain: {}", computed_hostname);
      97           21 :         default_context_ = ssl_context;
      98              :     }
      99              :     
     100          369 :     return true;
     101          369 : }
     102              : 
     103          163 : std::shared_ptr<boost::asio::ssl::context> certificate_manager::get_certificate(const std::string& hostname) const {
     104          163 :     std::lock_guard<std::mutex> lock(mutex_);
     105          163 :     auto cert = ssl_contexts_->get(hostname);
     106          163 :     if (cert.has_value()) {
     107           72 :         return cert.value();
     108              :     }
     109           91 :     return nullptr;
     110          163 : }
     111              : 
     112           33 : bool certificate_manager::has_certificate(const std::string& hostname) const {
     113           33 :     std::lock_guard<std::mutex> lock(mutex_);
     114           66 :     return ssl_contexts_->has(hostname);
     115           33 : }
     116              : 
     117          363 : bool certificate_manager::remove_certificate(const std::string& hostname) {
     118          363 :     LOG_INFO("Removing SSL certificate: {}", hostname);
     119          363 :     std::lock_guard<std::mutex> lock(mutex_);
     120          363 :     ssl_contexts_->erase(hostname);
     121          363 :     return true;
     122          363 : }
     123              : 
     124          141 : void certificate_manager::set_default_certificate(std::shared_ptr<boost::asio::ssl::context> ssl_context) {
     125          141 :     LOG_INFO("Setting default SSL certificate");
     126          141 :     std::lock_guard<std::mutex> lock(mutex_);
     127          141 :     default_context_ = ssl_context;
     128          141 : }
     129              : 
     130            6 : void certificate_manager::set_default_certificate(const std::string& certificate, const std::string& private_key) {
     131            6 :     set_default_certificate(create_ssl_context(certificate, private_key));
     132            6 : }
     133              : 
     134           61 : std::shared_ptr<boost::asio::ssl::context> certificate_manager::get_default_certificate() const {
     135           61 :     std::lock_guard<std::mutex> lock(mutex_);
     136              :     
     137              :     // If no default certificate is set, generate a self-signed one
     138           61 :     if (!default_context_) {
     139           21 :         LOG_WARNING("No default SSL certificate configured, generating self-signed certificate for development use");
     140           21 :         const_cast<certificate_manager*>(this)->generate_self_signed_certificate();
     141              :     }
     142              :     
     143          122 :     return default_context_;
     144           61 : }
     145              : 
     146            6 : void certificate_manager::set_default_host(const std::string& host) {
     147            6 :     std::lock_guard<std::mutex> lock(mutex_);
     148            6 :     default_host_ = host;
     149            6 : }
     150              : 
     151            6 : const std::string& certificate_manager::get_default_host() const {
     152            6 :     std::lock_guard<std::mutex> lock(mutex_);
     153            6 :     return default_host_;
     154            6 : }
     155              : 
     156          141 : std::set<std::string> certificate_manager::get_registered_hosts() const {
     157          141 :     std::lock_guard<std::mutex> lock(mutex_);
     158          282 :     return ssl_contexts_->keys();
     159          141 : }
     160              : 
     161            3 : void certificate_manager::set_server_ciphers(const std::string& ciphers, bool prefer_server_ciphers) {
     162            3 :     std::lock_guard<std::mutex> lock(mutex_);
     163            3 :     server_ciphers_ = ciphers;
     164            3 :     prefer_server_ciphers_ = prefer_server_ciphers;
     165            3 : }
     166              : 
     167            3 : void certificate_manager::enable_legacy_protocols(bool enable) {
     168            3 :     std::lock_guard<std::mutex> lock(mutex_);
     169            3 :     enable_legacy_protocols_ = enable;
     170            3 : }
     171              : 
     172           21 : void certificate_manager::generate_self_signed_certificate() {
     173              :     // Generate RSA key
     174           21 :     EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
     175           21 :     EVP_PKEY* pkey = nullptr;
     176              :     
     177           21 :     if (ctx) {
     178           21 :         if (EVP_PKEY_keygen_init(ctx) > 0) {
     179           21 :             if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) > 0) {
     180           21 :                 EVP_PKEY_keygen(ctx, &pkey);
     181              :             }
     182              :         }
     183           21 :         EVP_PKEY_CTX_free(ctx);
     184              :     }
     185              :     
     186           21 :     if (!pkey) {
     187            0 :         LOG_ERROR("Failed to generate RSA key for self-signed certificate");
     188            0 :         return;
     189              :     }
     190              :     
     191              :     // Generate certificate
     192           21 :     X509* x509 = X509_new();
     193           21 :     ASN1_INTEGER_set(X509_get_serialNumber(x509), 1);
     194           21 :     X509_gmtime_adj(X509_get_notBefore(x509), 0);
     195           21 :     X509_gmtime_adj(X509_get_notAfter(x509), 31536000L); // 1 year
     196              :     
     197           21 :     X509_set_pubkey(x509, pkey);
     198              :     
     199              :     // Set subject
     200           21 :     X509_NAME* name = X509_get_subject_name(x509);
     201           21 :     X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, 
     202              :                               reinterpret_cast<const unsigned char*>("US"), -1, -1, 0);
     203           21 :     X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, 
     204              :                               reinterpret_cast<const unsigned char*>("Thinger Development"), -1, -1, 0);
     205           21 :     X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, 
     206              :                               reinterpret_cast<const unsigned char*>("localhost"), -1, -1, 0);
     207              :     
     208           21 :     X509_set_issuer_name(x509, name);
     209           21 :     X509_sign(x509, pkey, EVP_sha256());
     210              :     
     211              :     // Create SSL context
     212           21 :     auto context = create_base_ssl_context();
     213              :     
     214              :     // Set certificate and key
     215           21 :     if (SSL_CTX_use_certificate(context->native_handle(), x509) != 1) {
     216            0 :         LOG_ERROR("Failed to use certificate for self-signed cert");
     217           21 :     } else if (SSL_CTX_use_PrivateKey(context->native_handle(), pkey) != 1) {
     218            0 :         LOG_ERROR("Failed to use private key for self-signed cert");
     219              :     } else {
     220           21 :         default_context_ = context;
     221           21 :         LOG_INFO("Generated self-signed certificate for development use (CN=localhost)");
     222              :     }
     223              :     
     224              :     // Cleanup
     225           21 :     X509_free(x509);
     226           21 :     EVP_PKEY_free(pkey);
     227           21 : }
     228              : 
     229              : // SNI callback implementation
     230           55 : int certificate_manager::sni_callback(SSL* ssl, int* al, void* arg) {
     231           55 :     if (!ssl) return SSL_TLSEXT_ERR_OK;
     232              : 
     233              :     // Get SNI server name
     234           52 :     const char* hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
     235              :     
     236           52 :     if (hostname != nullptr) {
     237           52 :         LOG_DEBUG("SNI connection for: {}", hostname);
     238              :         
     239           52 :         auto& mgr = certificate_manager::instance();
     240           52 :         auto certificate = mgr.get_certificate(hostname);
     241              :         
     242           52 :         if (certificate) {
     243            0 :             auto ssl_ctx = certificate->native_handle();
     244            0 :             SSL_set_SSL_CTX(ssl, ssl_ctx);
     245              :         } else {
     246           52 :             LOG_WARNING("Using default server certificate for hostname: {}", hostname);
     247              :         }
     248           52 :     }
     249              :     
     250           52 :     return SSL_TLSEXT_ERR_OK;
     251              : }
     252              : 
     253              : } // namespace thinger::asio
        

Generated by: LCOV version 2.0-1