blob: 1d4039cc7539a8b7bfeb5bc3472f111447363930 [file] [log] [blame]
Ed Tanous724985f2024-06-05 09:19:06 -07001#include "ssl_key_handler.hpp"
2
3#include "bmcweb_config.h"
4
5#include "logging.hpp"
6#include "ossl_random.hpp"
7#include "persistent_data.hpp"
8
9#include <boost/beast/core/file_posix.hpp>
10
11extern "C"
12{
13#include <nghttp2/nghttp2.h>
14#include <openssl/bio.h>
15#include <openssl/dh.h>
16#include <openssl/dsa.h>
17#include <openssl/err.h>
18#include <openssl/evp.h>
19#include <openssl/pem.h>
20#include <openssl/rand.h>
21#include <openssl/rsa.h>
22#include <openssl/ssl.h>
23}
24
25#include <boost/asio/ssl/context.hpp>
26#include <boost/system/error_code.hpp>
27
28#include <filesystem>
29#include <memory>
30#include <optional>
31#include <random>
32#include <string>
33
34namespace ensuressl
35{
36
37static EVP_PKEY* createEcKey();
38
39// Mozilla intermediate cipher suites v5.7
40// Sourced from: https://ssl-config.mozilla.org/guidelines/5.7.json
41constexpr const char* mozillaIntermediate = "ECDHE-ECDSA-AES128-GCM-SHA256:"
42 "ECDHE-RSA-AES128-GCM-SHA256:"
43 "ECDHE-ECDSA-AES256-GCM-SHA384:"
44 "ECDHE-RSA-AES256-GCM-SHA384:"
45 "ECDHE-ECDSA-CHACHA20-POLY1305:"
46 "ECDHE-RSA-CHACHA20-POLY1305:"
47 "DHE-RSA-AES128-GCM-SHA256:"
48 "DHE-RSA-AES256-GCM-SHA384:"
49 "DHE-RSA-CHACHA20-POLY1305";
50
51// Trust chain related errors.`
52bool isTrustChainError(int errnum)
53{
54 return (errnum == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) ||
55 (errnum == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN) ||
56 (errnum == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY) ||
57 (errnum == X509_V_ERR_CERT_UNTRUSTED) ||
58 (errnum == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE);
59}
60
61bool validateCertificate(X509* const cert)
62{
63 // Create an empty X509_STORE structure for certificate validation.
64 X509_STORE* x509Store = X509_STORE_new();
65 if (x509Store == nullptr)
66 {
67 BMCWEB_LOG_ERROR("Error occurred during X509_STORE_new call");
68 return false;
69 }
70
71 // Load Certificate file into the X509 structure.
72 X509_STORE_CTX* storeCtx = X509_STORE_CTX_new();
73 if (storeCtx == nullptr)
74 {
75 BMCWEB_LOG_ERROR("Error occurred during X509_STORE_CTX_new call");
76 X509_STORE_free(x509Store);
77 return false;
78 }
79
80 int errCode = X509_STORE_CTX_init(storeCtx, x509Store, cert, nullptr);
81 if (errCode != 1)
82 {
83 BMCWEB_LOG_ERROR("Error occurred during X509_STORE_CTX_init call");
84 X509_STORE_CTX_free(storeCtx);
85 X509_STORE_free(x509Store);
86 return false;
87 }
88
89 errCode = X509_verify_cert(storeCtx);
90 if (errCode == 1)
91 {
92 BMCWEB_LOG_INFO("Certificate verification is success");
93 X509_STORE_CTX_free(storeCtx);
94 X509_STORE_free(x509Store);
95 return true;
96 }
97 if (errCode == 0)
98 {
99 errCode = X509_STORE_CTX_get_error(storeCtx);
100 X509_STORE_CTX_free(storeCtx);
101 X509_STORE_free(x509Store);
102 if (isTrustChainError(errCode))
103 {
104 BMCWEB_LOG_DEBUG("Ignoring Trust Chain error. Reason: {}",
105 X509_verify_cert_error_string(errCode));
106 return true;
107 }
108 BMCWEB_LOG_ERROR("Certificate verification failed. Reason: {}",
109 X509_verify_cert_error_string(errCode));
110 return false;
111 }
112
113 BMCWEB_LOG_ERROR(
114 "Error occurred during X509_verify_cert call. ErrorCode: {}", errCode);
115 X509_STORE_CTX_free(storeCtx);
116 X509_STORE_free(x509Store);
117 return false;
118}
119
120std::string verifyOpensslKeyCert(const std::string& filepath)
121{
122 bool privateKeyValid = false;
123
124 BMCWEB_LOG_INFO("Checking certs in file {}", filepath);
125 boost::beast::file_posix file;
126 boost::system::error_code ec;
127 file.open(filepath.c_str(), boost::beast::file_mode::read, ec);
128 if (ec)
129 {
130 return "";
131 }
132 bool certValid = false;
133 std::string fileContents;
134 fileContents.resize(static_cast<size_t>(file.size(ec)), '\0');
135 file.read(fileContents.data(), fileContents.size(), ec);
136 if (ec)
137 {
138 BMCWEB_LOG_ERROR("Failed to read file");
139 return "";
140 }
141
142 BIO* bufio = BIO_new_mem_buf(static_cast<void*>(fileContents.data()),
143 static_cast<int>(fileContents.size()));
144 EVP_PKEY* pkey = PEM_read_bio_PrivateKey(bufio, nullptr, nullptr, nullptr);
145 BIO_free(bufio);
146 if (pkey != nullptr)
147 {
148 EVP_PKEY_CTX* pkeyCtx = EVP_PKEY_CTX_new_from_pkey(nullptr, pkey,
149 nullptr);
150
151 if (pkeyCtx == nullptr)
152 {
153 BMCWEB_LOG_ERROR("Unable to allocate pkeyCtx {}", ERR_get_error());
154 }
155 else if (EVP_PKEY_check(pkeyCtx) == 1)
156 {
157 privateKeyValid = true;
158 }
159 else
160 {
161 BMCWEB_LOG_ERROR("Key not valid error number {}", ERR_get_error());
162 }
163
164 if (privateKeyValid)
165 {
166 BIO* bufio2 =
167 BIO_new_mem_buf(static_cast<void*>(fileContents.data()),
168 static_cast<int>(fileContents.size()));
169 X509* x509 = PEM_read_bio_X509(bufio2, nullptr, nullptr, nullptr);
170 BIO_free(bufio2);
171 if (x509 == nullptr)
172 {
173 BMCWEB_LOG_ERROR("error getting x509 cert {}", ERR_get_error());
174 }
175 else
176 {
177 certValid = validateCertificate(x509);
178 X509_free(x509);
179 }
180 }
181
182 EVP_PKEY_CTX_free(pkeyCtx);
183 EVP_PKEY_free(pkey);
184 }
185 if (!certValid)
186 {
187 return "";
188 }
189 return fileContents;
190}
191
192X509* loadCert(const std::string& filePath)
193{
194 BIO* certFileBio = BIO_new_file(filePath.c_str(), "rb");
195 if (certFileBio == nullptr)
196 {
197 BMCWEB_LOG_ERROR("Error occurred during BIO_new_file call, FILE= {}",
198 filePath);
199 return nullptr;
200 }
201
202 X509* cert = X509_new();
203 if (cert == nullptr)
204 {
205 BMCWEB_LOG_ERROR("Error occurred during X509_new call, {}",
206 ERR_get_error());
207 BIO_free(certFileBio);
208 return nullptr;
209 }
210
211 if (PEM_read_bio_X509(certFileBio, &cert, nullptr, nullptr) == nullptr)
212 {
213 BMCWEB_LOG_ERROR(
214 "Error occurred during PEM_read_bio_X509 call, FILE= {}", filePath);
215
216 BIO_free(certFileBio);
217 X509_free(cert);
218 return nullptr;
219 }
220 BIO_free(certFileBio);
221 return cert;
222}
223
224int addExt(X509* cert, int nid, const char* value)
225{
226 X509_EXTENSION* ex = nullptr;
227 X509V3_CTX ctx{};
228 X509V3_set_ctx(&ctx, cert, cert, nullptr, nullptr, 0);
229
230 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
231 ex = X509V3_EXT_conf_nid(nullptr, &ctx, nid, const_cast<char*>(value));
232 if (ex == nullptr)
233 {
234 BMCWEB_LOG_ERROR("Error: In X509V3_EXT_conf_nidn: {}", value);
235 return -1;
236 }
237 X509_add_ext(cert, ex, -1);
238 X509_EXTENSION_free(ex);
239 return 0;
240}
241
242// Writes a certificate to a path, ignoring errors
243void writeCertificateToFile(const std::string& filepath,
244 const std::string& certificate)
245{
246 boost::system::error_code ec;
247 boost::beast::file_posix file;
248 file.open(filepath.c_str(), boost::beast::file_mode::write, ec);
249 if (!ec)
250 {
251 file.write(certificate.data(), certificate.size(), ec);
252 // ignore result
253 }
254}
255
256std::string generateSslCertificate(const std::string& cn)
257{
258 BMCWEB_LOG_INFO("Generating new keys");
259
260 std::string buffer;
261 BMCWEB_LOG_INFO("Generating EC key");
262 EVP_PKEY* pPrivKey = createEcKey();
263 if (pPrivKey != nullptr)
264 {
265 BMCWEB_LOG_INFO("Generating x509 Certificates");
266 // Use this code to directly generate a certificate
267 X509* x509 = X509_new();
268 if (x509 != nullptr)
269 {
270 // get a random number from the RNG for the certificate serial
271 // number If this is not random, regenerating certs throws browser
272 // errors
273 bmcweb::OpenSSLGenerator gen;
274 std::uniform_int_distribution<int> dis(
275 1, std::numeric_limits<int>::max());
276 int serial = dis(gen);
277
278 ASN1_INTEGER_set(X509_get_serialNumber(x509), serial);
279
280 // not before this moment
281 X509_gmtime_adj(X509_get_notBefore(x509), 0);
282 // Cert is valid for 10 years
283 X509_gmtime_adj(X509_get_notAfter(x509),
284 60L * 60L * 24L * 365L * 10L);
285
286 // set the public key to the key we just generated
287 X509_set_pubkey(x509, pPrivKey);
288
289 // get the subject name
290 X509_NAME* name = X509_get_subject_name(x509);
291
292 using x509String = const unsigned char;
293 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
294 x509String* country = reinterpret_cast<x509String*>("US");
295 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
296 x509String* company = reinterpret_cast<x509String*>("OpenBMC");
297 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
298 x509String* cnStr = reinterpret_cast<x509String*>(cn.c_str());
299
300 X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, country, -1, -1,
301 0);
302 X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, company, -1, -1,
303 0);
304 X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, cnStr, -1, -1,
305 0);
306 // set the CSR options
307 X509_set_issuer_name(x509, name);
308
309 X509_set_version(x509, 2);
310 addExt(x509, NID_basic_constraints, ("critical,CA:TRUE"));
311 addExt(x509, NID_subject_alt_name, ("DNS:" + cn).c_str());
312 addExt(x509, NID_subject_key_identifier, ("hash"));
313 addExt(x509, NID_authority_key_identifier, ("keyid"));
314 addExt(x509, NID_key_usage, ("digitalSignature, keyEncipherment"));
315 addExt(x509, NID_ext_key_usage, ("serverAuth"));
316 addExt(x509, NID_netscape_comment, (x509Comment));
317
318 // Sign the certificate with our private key
319 X509_sign(x509, pPrivKey, EVP_sha256());
320
321 BIO* bufio = BIO_new(BIO_s_mem());
322
323 int pkeyRet = PEM_write_bio_PrivateKey(
324 bufio, pPrivKey, nullptr, nullptr, 0, nullptr, nullptr);
325 if (pkeyRet <= 0)
326 {
327 BMCWEB_LOG_ERROR(
328 "Failed to write pkey with code {}. Ignoring.", pkeyRet);
329 }
330
331 char* data = nullptr;
332 long int dataLen = BIO_get_mem_data(bufio, &data);
333 buffer += std::string_view(data, static_cast<size_t>(dataLen));
334 BIO_free(bufio);
335
336 bufio = BIO_new(BIO_s_mem());
337 pkeyRet = PEM_write_bio_X509(bufio, x509);
338 if (pkeyRet <= 0)
339 {
340 BMCWEB_LOG_ERROR(
341 "Failed to write X509 with code {}. Ignoring.", pkeyRet);
342 }
343 dataLen = BIO_get_mem_data(bufio, &data);
344 buffer += std::string_view(data, static_cast<size_t>(dataLen));
345
346 BIO_free(bufio);
347 BMCWEB_LOG_INFO("Cert size is {}", buffer.size());
348 X509_free(x509);
349 }
350
351 EVP_PKEY_free(pPrivKey);
352 pPrivKey = nullptr;
353 }
354
355 // cleanup_openssl();
356 return buffer;
357}
358
359EVP_PKEY* createEcKey()
360{
361 EVP_PKEY* pKey = nullptr;
362
363 // Create context for curve parameter generation.
364 std::unique_ptr<EVP_PKEY_CTX, decltype(&::EVP_PKEY_CTX_free)> ctx{
365 EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr), &::EVP_PKEY_CTX_free};
366 if (!ctx)
367 {
368 return nullptr;
369 }
370
371 // Set up curve parameters.
372 EVP_PKEY* params = nullptr;
373 if ((EVP_PKEY_paramgen_init(ctx.get()) <= 0) ||
374 (EVP_PKEY_CTX_set_ec_param_enc(ctx.get(), OPENSSL_EC_NAMED_CURVE) <=
375 0) ||
376 (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx.get(), NID_secp384r1) <=
377 0) ||
378 (EVP_PKEY_paramgen(ctx.get(), &params) <= 0))
379 {
380 return nullptr;
381 }
382
383 // Set up RAII holder for params.
384 std::unique_ptr<EVP_PKEY, decltype(&::EVP_PKEY_free)> pparams{
385 params, &::EVP_PKEY_free};
386
387 // Set new context for key generation, using curve parameters.
388 ctx.reset(EVP_PKEY_CTX_new_from_pkey(nullptr, params, nullptr));
389 if (!ctx || (EVP_PKEY_keygen_init(ctx.get()) <= 0))
390 {
391 return nullptr;
392 }
393
394 // Generate key.
395 if (EVP_PKEY_keygen(ctx.get(), &pKey) <= 0)
396 {
397 return nullptr;
398 }
399
400 return pKey;
401}
402
403std::string ensureOpensslKeyPresentAndValid(const std::string& filepath)
404{
405 std::string cert = verifyOpensslKeyCert(filepath);
406
407 if (cert.empty())
408 {
409 BMCWEB_LOG_WARNING("Error in verifying signature, regenerating");
410 cert = generateSslCertificate("testhost");
411 if (cert.empty())
412 {
413 BMCWEB_LOG_ERROR("Failed to generate cert");
414 }
415 else
416 {
417 writeCertificateToFile(filepath, cert);
418 }
419 }
420 return cert;
421}
422
423static std::string ensureCertificate()
424{
425 namespace fs = std::filesystem;
426 // Cleanup older certificate file existing in the system
427 fs::path oldcertPath = fs::path("/home/root/server.pem");
428 std::error_code ec;
429 fs::remove(oldcertPath, ec);
430 // Ignore failure to remove; File might not exist.
431
432 fs::path certPath = "/etc/ssl/certs/https/";
433 // if path does not exist create the path so that
434 // self signed certificate can be created in the
435 // path
436 fs::path certFile = certPath / "server.pem";
437
438 if (!fs::exists(certPath, ec))
439 {
440 fs::create_directories(certPath, ec);
441 }
442 BMCWEB_LOG_INFO("Building SSL Context file= {}", certFile.string());
443 std::string sslPemFile(certFile);
444 return ensuressl::ensureOpensslKeyPresentAndValid(sslPemFile);
445}
446
447static int nextProtoCallback(SSL* /*unused*/, const unsigned char** data,
448 unsigned int* len, void* /*unused*/)
449{
450 // First byte is the length.
451 constexpr std::string_view h2 = "\x02h2";
452 *data = std::bit_cast<const unsigned char*>(h2.data());
453 *len = static_cast<unsigned int>(h2.size());
454 return SSL_TLSEXT_ERR_OK;
455}
456
457static int alpnSelectProtoCallback(SSL* /*unused*/, const unsigned char** out,
458 unsigned char* outlen,
459 const unsigned char* in, unsigned int inlen,
460 void* /*unused*/)
461{
Ed Tanousfa2d6ae2024-07-26 13:11:24 -0700462 int rv = nghttp2_select_alpn(out, outlen, in, inlen);
463 if (rv == -1)
Ed Tanous724985f2024-06-05 09:19:06 -0700464 {
465 return SSL_TLSEXT_ERR_NOACK;
466 }
Ed Tanousfa2d6ae2024-07-26 13:11:24 -0700467 if (rv == 1)
468 {
469 BMCWEB_LOG_DEBUG("Selected HTTP2");
470 }
Ed Tanous724985f2024-06-05 09:19:06 -0700471 return SSL_TLSEXT_ERR_OK;
472}
473
474static bool getSslContext(boost::asio::ssl::context& mSslContext,
475 const std::string& sslPemFile)
476{
477 mSslContext.set_options(boost::asio::ssl::context::default_workarounds |
478 boost::asio::ssl::context::no_sslv2 |
479 boost::asio::ssl::context::no_sslv3 |
480 boost::asio::ssl::context::single_dh_use |
481 boost::asio::ssl::context::no_tlsv1 |
482 boost::asio::ssl::context::no_tlsv1_1);
483
484 BMCWEB_LOG_DEBUG("Using default TrustStore location: {}", trustStorePath);
485 mSslContext.add_verify_path(trustStorePath);
486
487 if (!sslPemFile.empty())
488 {
489 boost::system::error_code ec;
490
491 boost::asio::const_buffer buf(sslPemFile.data(), sslPemFile.size());
492 mSslContext.use_certificate(buf, boost::asio::ssl::context::pem, ec);
493 if (ec)
494 {
495 return false;
496 }
497 mSslContext.use_private_key(buf, boost::asio::ssl::context::pem, ec);
498 if (ec)
499 {
500 BMCWEB_LOG_CRITICAL("Failed to open ssl pkey");
501 return false;
502 }
503 }
504
505 // Set up EC curves to auto (boost asio doesn't have a method for this)
506 // There is a pull request to add this. Once this is included in an asio
507 // drop, use the right way
508 // http://stackoverflow.com/questions/18929049/boost-asio-with-ecdsa-certificate-issue
509 if (SSL_CTX_set_ecdh_auto(mSslContext.native_handle(), 1) != 1)
510 {}
511
512 if (SSL_CTX_set_cipher_list(mSslContext.native_handle(),
513 mozillaIntermediate) != 1)
514 {
515 BMCWEB_LOG_ERROR("Error setting cipher list");
516 return false;
517 }
518 return true;
519}
520
521std::shared_ptr<boost::asio::ssl::context> getSslServerContext()
522{
523 boost::asio::ssl::context sslCtx(boost::asio::ssl::context::tls_server);
524
525 auto certFile = ensureCertificate();
526 if (!getSslContext(sslCtx, certFile))
527 {
528 BMCWEB_LOG_CRITICAL("Couldn't get server context");
529 return nullptr;
530 }
531 const persistent_data::AuthConfigMethods& c =
532 persistent_data::SessionStore::getInstance().getAuthMethodsConfig();
533
534 boost::asio::ssl::verify_mode mode = boost::asio::ssl::verify_peer;
535 if (c.tlsStrict)
536 {
537 BMCWEB_LOG_DEBUG("Setting verify peer");
538 mode |= boost::asio::ssl::verify_fail_if_no_peer_cert;
539 }
540
541 boost::system::error_code ec;
542 sslCtx.set_verify_mode(mode, ec);
543 if (ec)
544 {
545 BMCWEB_LOG_DEBUG("Failed to set verify mode {}", ec.message());
546 return nullptr;
547 }
548 SSL_CTX_set_options(sslCtx.native_handle(), SSL_OP_NO_RENEGOTIATION);
549
550 if constexpr (BMCWEB_EXPERIMENTAL_HTTP2)
551 {
552 SSL_CTX_set_next_protos_advertised_cb(sslCtx.native_handle(),
553 nextProtoCallback, nullptr);
554
555 SSL_CTX_set_alpn_select_cb(sslCtx.native_handle(),
556 alpnSelectProtoCallback, nullptr);
557 }
558
559 return std::make_shared<boost::asio::ssl::context>(std::move(sslCtx));
560}
561
562std::optional<boost::asio::ssl::context>
563 getSSLClientContext(VerifyCertificate verifyCertificate)
564{
565 namespace fs = std::filesystem;
566
567 boost::asio::ssl::context sslCtx(boost::asio::ssl::context::tls_client);
568
569 // NOTE, this path is temporary; In the future it will need to change to
570 // be set per subscription. Do not rely on this.
571 fs::path certPath = "/etc/ssl/certs/https/client.pem";
572 std::string cert = verifyOpensslKeyCert(certPath);
573
574 if (!getSslContext(sslCtx, cert))
575 {
576 return std::nullopt;
577 }
578
579 // Add a directory containing certificate authority files to be used
580 // for performing verification.
581 boost::system::error_code ec;
582 sslCtx.set_default_verify_paths(ec);
583 if (ec)
584 {
585 BMCWEB_LOG_ERROR("SSL context set_default_verify failed");
586 return std::nullopt;
587 }
588
589 int mode = boost::asio::ssl::verify_peer;
590 if (verifyCertificate == VerifyCertificate::NoVerify)
591 {
592 mode = boost::asio::ssl::verify_none;
593 }
594
595 // Verify the remote server's certificate
596 sslCtx.set_verify_mode(mode, ec);
597 if (ec)
598 {
599 BMCWEB_LOG_ERROR("SSL context set_verify_mode failed");
600 return std::nullopt;
601 }
602
603 if (SSL_CTX_set_cipher_list(sslCtx.native_handle(), mozillaIntermediate) !=
604 1)
605 {
606 BMCWEB_LOG_ERROR("SSL_CTX_set_cipher_list failed");
607 return std::nullopt;
608 }
609
610 return {std::move(sslCtx)};
611}
612
613} // namespace ensuressl