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