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
|