1 #pragma once
2 
3 #include <openssl/bio.h>
4 #include <openssl/dh.h>
5 #include <openssl/dsa.h>
6 #include <openssl/err.h>
7 #include <openssl/evp.h>
8 #include <openssl/pem.h>
9 #include <openssl/rand.h>
10 #include <openssl/rsa.h>
11 #include <openssl/ssl.h>
12 
13 #include <boost/asio/ssl/context.hpp>
14 #include <random.hpp>
15 
16 #include <random>
17 
18 namespace ensuressl
19 {
20 constexpr char const* trustStorePath = "/etc/ssl/certs/authority";
21 constexpr char const* x509Comment = "Generated from OpenBMC service";
22 static void initOpenssl();
23 static EVP_PKEY* createEcKey();
24 
25 // Trust chain related errors.`
26 inline bool isTrustChainError(int errnum)
27 {
28     return (errnum == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) ||
29            (errnum == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN) ||
30            (errnum == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY) ||
31            (errnum == X509_V_ERR_CERT_UNTRUSTED) ||
32            (errnum == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE);
33 }
34 
35 inline bool validateCertificate(X509* const cert)
36 {
37     // Create an empty X509_STORE structure for certificate validation.
38     X509_STORE* x509Store = X509_STORE_new();
39     if (x509Store == nullptr)
40     {
41         BMCWEB_LOG_ERROR << "Error occurred during X509_STORE_new call";
42         return false;
43     }
44 
45     // Load Certificate file into the X509 structure.
46     X509_STORE_CTX* storeCtx = X509_STORE_CTX_new();
47     if (storeCtx == nullptr)
48     {
49         BMCWEB_LOG_ERROR << "Error occurred during X509_STORE_CTX_new call";
50         X509_STORE_free(x509Store);
51         return false;
52     }
53 
54     int errCode = X509_STORE_CTX_init(storeCtx, x509Store, cert, nullptr);
55     if (errCode != 1)
56     {
57         BMCWEB_LOG_ERROR << "Error occurred during X509_STORE_CTX_init call";
58         X509_STORE_CTX_free(storeCtx);
59         X509_STORE_free(x509Store);
60         return false;
61     }
62 
63     errCode = X509_verify_cert(storeCtx);
64     if (errCode == 1)
65     {
66         BMCWEB_LOG_INFO << "Certificate verification is success";
67         X509_STORE_CTX_free(storeCtx);
68         X509_STORE_free(x509Store);
69         return true;
70     }
71     if (errCode == 0)
72     {
73         errCode = X509_STORE_CTX_get_error(storeCtx);
74         X509_STORE_CTX_free(storeCtx);
75         X509_STORE_free(x509Store);
76         if (isTrustChainError(errCode))
77         {
78             BMCWEB_LOG_DEBUG << "Ignoring Trust Chain error. Reason: "
79                              << X509_verify_cert_error_string(errCode);
80             return true;
81         }
82         BMCWEB_LOG_ERROR << "Certificate verification failed. Reason: "
83                          << X509_verify_cert_error_string(errCode);
84         return false;
85     }
86 
87     BMCWEB_LOG_ERROR
88         << "Error occurred during X509_verify_cert call. ErrorCode: "
89         << errCode;
90     X509_STORE_CTX_free(storeCtx);
91     X509_STORE_free(x509Store);
92     return false;
93 }
94 
95 inline bool verifyOpensslKeyCert(const std::string& filepath)
96 {
97     bool privateKeyValid = false;
98     bool certValid = false;
99 
100     std::cout << "Checking certs in file " << filepath << "\n";
101 
102     FILE* file = fopen(filepath.c_str(), "r");
103     if (file != nullptr)
104     {
105         EVP_PKEY* pkey = PEM_read_PrivateKey(file, nullptr, nullptr, nullptr);
106         if (pkey != nullptr)
107         {
108 #if (OPENSSL_VERSION_NUMBER < 0x30000000L)
109             RSA* rsa = EVP_PKEY_get1_RSA(pkey);
110             if (rsa != nullptr)
111             {
112                 std::cout << "Found an RSA key\n";
113                 if (RSA_check_key(rsa) == 1)
114                 {
115                     privateKeyValid = true;
116                 }
117                 else
118                 {
119                     std::cerr << "Key not valid error number "
120                               << ERR_get_error() << "\n";
121                 }
122                 RSA_free(rsa);
123             }
124             else
125             {
126                 EC_KEY* ec = EVP_PKEY_get1_EC_KEY(pkey);
127                 if (ec != nullptr)
128                 {
129                     std::cout << "Found an EC key\n";
130                     if (EC_KEY_check_key(ec) == 1)
131                     {
132                         privateKeyValid = true;
133                     }
134                     else
135                     {
136                         std::cerr << "Key not valid error number "
137                                   << ERR_get_error() << "\n";
138                     }
139                     EC_KEY_free(ec);
140                 }
141             }
142 #else
143             EVP_PKEY_CTX* pkeyCtx =
144                 EVP_PKEY_CTX_new_from_pkey(nullptr, pkey, nullptr);
145 
146             if (pkeyCtx == nullptr)
147             {
148                 std::cerr << "Unable to allocate pkeyCtx " << ERR_get_error()
149                           << "\n";
150             }
151             else if (EVP_PKEY_check(pkeyCtx) == 1)
152             {
153                 privateKeyValid = true;
154             }
155             else
156             {
157 
158                 std::cerr << "Key not valid error number " << ERR_get_error()
159                           << "\n";
160             }
161 #endif
162 
163             if (privateKeyValid)
164             {
165                 // If the order is certificate followed by key in input file
166                 // then, certificate read will fail. So, setting the file
167                 // pointer to point beginning of file to avoid certificate and
168                 // key order issue.
169                 fseek(file, 0, SEEK_SET);
170 
171                 X509* x509 = PEM_read_X509(file, nullptr, nullptr, nullptr);
172                 if (x509 == nullptr)
173                 {
174                     std::cout << "error getting x509 cert " << ERR_get_error()
175                               << "\n";
176                 }
177                 else
178                 {
179                     certValid = validateCertificate(x509);
180                     X509_free(x509);
181                 }
182             }
183 
184 #if (OPENSSL_VERSION_NUMBER > 0x30000000L)
185             EVP_PKEY_CTX_free(pkeyCtx);
186 #endif
187             EVP_PKEY_free(pkey);
188         }
189         fclose(file);
190     }
191     return certValid;
192 }
193 
194 inline X509* loadCert(const std::string& filePath)
195 {
196     BIO* certFileBio = BIO_new_file(filePath.c_str(), "rb");
197     if (certFileBio == nullptr)
198     {
199         BMCWEB_LOG_ERROR << "Error occured during BIO_new_file call, "
200                          << "FILE= " << filePath;
201         return nullptr;
202     }
203 
204     X509* cert = X509_new();
205     if (cert == nullptr)
206     {
207         BMCWEB_LOG_ERROR << "Error occured during X509_new call, "
208                          << ERR_get_error();
209         BIO_free(certFileBio);
210         return nullptr;
211     }
212 
213     if (PEM_read_bio_X509(certFileBio, &cert, nullptr, nullptr) == nullptr)
214     {
215         BMCWEB_LOG_ERROR << "Error occured during PEM_read_bio_X509 call, "
216                          << "FILE= " << filePath;
217 
218         BIO_free(certFileBio);
219         X509_free(cert);
220         return nullptr;
221     }
222     BIO_free(certFileBio);
223     return cert;
224 }
225 
226 inline int addExt(X509* cert, int nid, const char* value)
227 {
228     X509_EXTENSION* ex = nullptr;
229     X509V3_CTX ctx{};
230     X509V3_set_ctx(&ctx, cert, cert, nullptr, nullptr, 0);
231 
232     // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
233     ex = X509V3_EXT_conf_nid(nullptr, &ctx, nid, const_cast<char*>(value));
234     if (ex == nullptr)
235     {
236         BMCWEB_LOG_ERROR << "Error: In X509V3_EXT_conf_nidn: " << value;
237         return -1;
238     }
239     X509_add_ext(cert, ex, -1);
240     X509_EXTENSION_free(ex);
241     return 0;
242 }
243 
244 inline void generateSslCertificate(const std::string& filepath,
245                                    const std::string& cn)
246 {
247     FILE* pFile = nullptr;
248     std::cout << "Generating new keys\n";
249     initOpenssl();
250 
251     std::cerr << "Generating EC key\n";
252     EVP_PKEY* pPrivKey = createEcKey();
253     if (pPrivKey != nullptr)
254     {
255         std::cerr << "Generating x509 Certificate\n";
256         // Use this code to directly generate a certificate
257         X509* x509 = X509_new();
258         if (x509 != nullptr)
259         {
260             // get a random number from the RNG for the certificate serial
261             // number If this is not random, regenerating certs throws broswer
262             // errors
263             bmcweb::OpenSSLGenerator gen;
264             std::uniform_int_distribution<int> dis(
265                 1, std::numeric_limits<int>::max());
266             int serial = dis(gen);
267 
268             ASN1_INTEGER_set(X509_get_serialNumber(x509), serial);
269 
270             // not before this moment
271             X509_gmtime_adj(X509_get_notBefore(x509), 0);
272             // Cert is valid for 10 years
273             X509_gmtime_adj(X509_get_notAfter(x509),
274                             60L * 60L * 24L * 365L * 10L);
275 
276             // set the public key to the key we just generated
277             X509_set_pubkey(x509, pPrivKey);
278 
279             // get the subject name
280             X509_NAME* name = X509_get_subject_name(x509);
281 
282             using x509String = const unsigned char;
283             // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
284             x509String* country = reinterpret_cast<x509String*>("US");
285             // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
286             x509String* company = reinterpret_cast<x509String*>("OpenBMC");
287             // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
288             x509String* cnStr = reinterpret_cast<x509String*>(cn.c_str());
289 
290             X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, country, -1, -1,
291                                        0);
292             X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, company, -1, -1,
293                                        0);
294             X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, cnStr, -1, -1,
295                                        0);
296             // set the CSR options
297             X509_set_issuer_name(x509, name);
298 
299             X509_set_version(x509, 2);
300             addExt(x509, NID_basic_constraints, ("critical,CA:TRUE"));
301             addExt(x509, NID_subject_alt_name, ("DNS:" + cn).c_str());
302             addExt(x509, NID_subject_key_identifier, ("hash"));
303             addExt(x509, NID_authority_key_identifier, ("keyid"));
304             addExt(x509, NID_key_usage, ("digitalSignature, keyEncipherment"));
305             addExt(x509, NID_ext_key_usage, ("serverAuth"));
306             addExt(x509, NID_netscape_comment, (x509Comment));
307 
308             // Sign the certificate with our private key
309             X509_sign(x509, pPrivKey, EVP_sha256());
310 
311             pFile = fopen(filepath.c_str(), "wt");
312 
313             if (pFile != nullptr)
314             {
315                 PEM_write_PrivateKey(pFile, pPrivKey, nullptr, nullptr, 0,
316                                      nullptr, nullptr);
317 
318                 PEM_write_X509(pFile, x509);
319                 fclose(pFile);
320                 pFile = nullptr;
321             }
322 
323             X509_free(x509);
324         }
325 
326         EVP_PKEY_free(pPrivKey);
327         pPrivKey = nullptr;
328     }
329 
330     // cleanup_openssl();
331 }
332 
333 EVP_PKEY* createEcKey()
334 {
335     EVP_PKEY* pKey = nullptr;
336 
337 #if (OPENSSL_VERSION_NUMBER < 0x30000000L)
338     int eccgrp = 0;
339     eccgrp = OBJ_txt2nid("secp384r1");
340 
341     EC_KEY* myecc = EC_KEY_new_by_curve_name(eccgrp);
342     if (myecc != nullptr)
343     {
344         EC_KEY_set_asn1_flag(myecc, OPENSSL_EC_NAMED_CURVE);
345         EC_KEY_generate_key(myecc);
346         pKey = EVP_PKEY_new();
347         if (pKey != nullptr)
348         {
349             if (EVP_PKEY_assign(pKey, EVP_PKEY_EC, myecc) != 0)
350             {
351                 /* pKey owns myecc from now */
352                 if (EC_KEY_check_key(myecc) <= 0)
353                 {
354                     std::cerr << "EC_check_key failed.\n";
355                 }
356             }
357         }
358     }
359 #else
360     // Create context for curve parameter generation.
361     std::unique_ptr<EVP_PKEY_CTX, decltype(&::EVP_PKEY_CTX_free)> ctx{
362         EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr), &::EVP_PKEY_CTX_free};
363     if (!ctx)
364     {
365         return nullptr;
366     }
367 
368     // Set up curve parameters.
369     EVP_PKEY* params = nullptr;
370     if ((EVP_PKEY_paramgen_init(ctx.get()) <= 0) ||
371         (EVP_PKEY_CTX_set_ec_param_enc(ctx.get(), OPENSSL_EC_NAMED_CURVE) <=
372          0) ||
373         (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx.get(), NID_secp384r1) <=
374          0) ||
375         (EVP_PKEY_paramgen(ctx.get(), &params) <= 0))
376     {
377         return nullptr;
378     }
379 
380     // Set up RAII holder for params.
381     std::unique_ptr<EVP_PKEY, decltype(&::EVP_PKEY_free)> pparams{
382         params, &::EVP_PKEY_free};
383 
384     // Set new context for key generation, using curve parameters.
385     ctx.reset(EVP_PKEY_CTX_new_from_pkey(nullptr, params, nullptr));
386     if (!ctx || (EVP_PKEY_keygen_init(ctx.get()) <= 0))
387     {
388         return nullptr;
389     }
390 
391     // Generate key.
392     if (EVP_PKEY_keygen(ctx.get(), &pKey) <= 0)
393     {
394         return nullptr;
395     }
396 #endif
397 
398     return pKey;
399 }
400 
401 void initOpenssl()
402 {
403 #if OPENSSL_VERSION_NUMBER < 0x10100000L
404     SSL_load_error_strings();
405     OpenSSL_add_all_algorithms();
406     RAND_load_file("/dev/urandom", 1024);
407 #endif
408 }
409 
410 inline void ensureOpensslKeyPresentAndValid(const std::string& filepath)
411 {
412     bool pemFileValid = false;
413 
414     pemFileValid = verifyOpensslKeyCert(filepath);
415 
416     if (!pemFileValid)
417     {
418         std::cerr << "Error in verifying signature, regenerating\n";
419         generateSslCertificate(filepath, "testhost");
420     }
421 }
422 
423 inline std::shared_ptr<boost::asio::ssl::context>
424     getSslContext(const std::string& sslPemFile)
425 {
426     std::shared_ptr<boost::asio::ssl::context> mSslContext =
427         std::make_shared<boost::asio::ssl::context>(
428             boost::asio::ssl::context::tls_server);
429     mSslContext->set_options(boost::asio::ssl::context::default_workarounds |
430                              boost::asio::ssl::context::no_sslv2 |
431                              boost::asio::ssl::context::no_sslv3 |
432                              boost::asio::ssl::context::single_dh_use |
433                              boost::asio::ssl::context::no_tlsv1 |
434                              boost::asio::ssl::context::no_tlsv1_1);
435 
436     // BIG WARNING: This needs to stay disabled, as there will always be
437     // unauthenticated endpoints
438     // mSslContext->set_verify_mode(boost::asio::ssl::verify_peer);
439 
440     SSL_CTX_set_options(mSslContext->native_handle(), SSL_OP_NO_RENEGOTIATION);
441 
442     BMCWEB_LOG_DEBUG << "Using default TrustStore location: " << trustStorePath;
443     mSslContext->add_verify_path(trustStorePath);
444 
445     mSslContext->use_certificate_file(sslPemFile,
446                                       boost::asio::ssl::context::pem);
447     mSslContext->use_private_key_file(sslPemFile,
448                                       boost::asio::ssl::context::pem);
449 
450     // Set up EC curves to auto (boost asio doesn't have a method for this)
451     // There is a pull request to add this.  Once this is included in an asio
452     // drop, use the right way
453     // http://stackoverflow.com/questions/18929049/boost-asio-with-ecdsa-certificate-issue
454     if (SSL_CTX_set_ecdh_auto(mSslContext->native_handle(), 1) != 1)
455     {
456         BMCWEB_LOG_ERROR << "Error setting tmp ecdh list\n";
457     }
458 
459     std::string mozillaModern = "ECDHE-ECDSA-AES256-GCM-SHA384:"
460                                 "ECDHE-RSA-AES256-GCM-SHA384:"
461                                 "ECDHE-ECDSA-CHACHA20-POLY1305:"
462                                 "ECDHE-RSA-CHACHA20-POLY1305:"
463                                 "ECDHE-ECDSA-AES128-GCM-SHA256:"
464                                 "ECDHE-RSA-AES128-GCM-SHA256:"
465                                 "ECDHE-ECDSA-AES256-SHA384:"
466                                 "ECDHE-RSA-AES256-SHA384:"
467                                 "ECDHE-ECDSA-AES128-SHA256:"
468                                 "ECDHE-RSA-AES128-SHA256";
469 
470     if (SSL_CTX_set_cipher_list(mSslContext->native_handle(),
471                                 mozillaModern.c_str()) != 1)
472     {
473         BMCWEB_LOG_ERROR << "Error setting cipher list\n";
474     }
475     return mSslContext;
476 }
477 
478 inline std::optional<boost::asio::ssl::context> getSSLClientContext()
479 {
480     boost::asio::ssl::context sslCtx(boost::asio::ssl::context::tls_client);
481 
482     boost::system::error_code ec;
483 
484     // Support only TLS v1.2 & v1.3
485     sslCtx.set_options(boost::asio::ssl::context::default_workarounds |
486                            boost::asio::ssl::context::no_sslv2 |
487                            boost::asio::ssl::context::no_sslv3 |
488                            boost::asio::ssl::context::single_dh_use |
489                            boost::asio::ssl::context::no_tlsv1 |
490                            boost::asio::ssl::context::no_tlsv1_1,
491                        ec);
492     if (ec)
493     {
494         BMCWEB_LOG_ERROR << "SSL context set_options failed";
495         return std::nullopt;
496     }
497 
498     // Add a directory containing certificate authority files to be used
499     // for performing verification.
500     sslCtx.set_default_verify_paths(ec);
501     if (ec)
502     {
503         BMCWEB_LOG_ERROR << "SSL context set_default_verify failed";
504         return std::nullopt;
505     }
506 
507     // Verify the remote server's certificate
508     sslCtx.set_verify_mode(boost::asio::ssl::verify_peer, ec);
509     if (ec)
510     {
511         BMCWEB_LOG_ERROR << "SSL context set_verify_mode failed";
512         return std::nullopt;
513     }
514 
515     // All cipher suites are set as per OWASP datasheet.
516     // https://cheatsheetseries.owasp.org/cheatsheets/TLS_Cipher_String_Cheat_Sheet.html
517     constexpr const char* sslCiphers = "ECDHE-ECDSA-AES128-GCM-SHA256:"
518                                        "ECDHE-RSA-AES128-GCM-SHA256:"
519                                        "ECDHE-ECDSA-AES256-GCM-SHA384:"
520                                        "ECDHE-RSA-AES256-GCM-SHA384:"
521                                        "ECDHE-ECDSA-CHACHA20-POLY1305:"
522                                        "ECDHE-RSA-CHACHA20-POLY1305:"
523                                        "DHE-RSA-AES128-GCM-SHA256:"
524                                        "DHE-RSA-AES256-GCM-SHA384"
525                                        "TLS_AES_128_GCM_SHA256:"
526                                        "TLS_AES_256_GCM_SHA384:"
527                                        "TLS_CHACHA20_POLY1305_SHA256";
528 
529     if (SSL_CTX_set_cipher_list(sslCtx.native_handle(), sslCiphers) != 1)
530     {
531         BMCWEB_LOG_ERROR << "SSL_CTX_set_cipher_list failed";
532         return std::nullopt;
533     }
534 
535     return sslCtx;
536 }
537 
538 } // namespace ensuressl
539