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