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