| /** |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "controllers/SSLContextService.h" |
| |
| #include <openssl/err.h> |
| #include <openssl/ssl.h> |
| #include <openssl/x509v3.h> |
| #include <openssl/bio.h> |
| #ifdef WIN32 |
| #pragma comment(lib, "crypt32.lib") |
| #pragma comment(lib, "Ws2_32.lib") |
| #include <optional> |
| #endif // WIN32 |
| |
| #include <fstream> |
| #include <memory> |
| #include <string> |
| |
| #include "core/Resource.h" |
| #include "io/validation.h" |
| #include "properties/Configure.h" |
| #include "utils/HTTPUtils.h" |
| #include "utils/tls/CertificateUtils.h" |
| #include "utils/tls/TLSUtils.h" |
| #include "utils/tls/DistinguishedName.h" |
| #include "utils/tls/WindowsCertStoreLocation.h" |
| #include "utils/TimeUtil.h" |
| |
| namespace org::apache::nifi::minifi::controllers { |
| |
| namespace { |
| bool is_valid_and_readable_path(const std::filesystem::path& path_to_be_tested) { |
| std::ifstream file_to_be_tested(path_to_be_tested); |
| return file_to_be_tested.good(); |
| } |
| |
| #ifdef WIN32 |
| std::string getCertName(const utils::tls::X509_unique_ptr& cert) { |
| const size_t BUFFER_SIZE = 256; |
| char name_buffer[BUFFER_SIZE]; |
| |
| BIO* bio = BIO_new(BIO_s_mem()); |
| if (!bio) { |
| return {}; |
| } |
| const auto guard = gsl::finally([&bio]() { |
| BIO_free(bio); |
| }); |
| |
| X509_NAME_print_ex(bio, X509_get_subject_name(cert.get()), 0, XN_FLAG_ONELINE); |
| |
| int length = BIO_read(bio, name_buffer, BUFFER_SIZE - 1); |
| name_buffer[length] = '\0'; |
| |
| return name_buffer; |
| } |
| #endif |
| } // namespace |
| |
| void SSLContextService::initialize() { |
| std::lock_guard<std::mutex> lock(initialization_mutex_); |
| if (initialized_) { |
| return; |
| } |
| |
| ControllerServiceImpl::initialize(); |
| |
| initializeProperties(); |
| |
| initialized_ = true; |
| } |
| |
| bool SSLContextService::configure_ssl_context(void* raw_ctx) { |
| auto* const ctx = static_cast<SSL_CTX*>(raw_ctx); |
| if (!certificate_.empty()) { |
| if (isFileTypeP12(certificate_)) { |
| if (!addP12CertificateToSSLContext(ctx)) { |
| return false; |
| } |
| } else { |
| if (!addPemCertificateToSSLContext(ctx)) { |
| return false; |
| } |
| } |
| |
| if (!SSL_CTX_check_private_key(ctx)) { |
| logger_->log_error("Private key does not match the public certificate, {}", getLatestOpenSSLErrorString()); |
| return false; |
| } |
| } |
| |
| SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr); |
| |
| if (!ca_certificate_.empty()) { |
| if (SSL_CTX_load_verify_locations(ctx, ca_certificate_.string().c_str(), nullptr) == 0) { |
| logger_->log_error("Cannot load CA certificate, exiting, {}", getLatestOpenSSLErrorString()); |
| return false; |
| } |
| } |
| |
| if (use_system_cert_store_ && certificate_.empty()) { |
| if (!addClientCertificateFromSystemStoreToSSLContext(ctx)) { |
| return false; |
| } |
| } |
| |
| if (use_system_cert_store_ && ca_certificate_.empty()) { |
| if (!addServerCertificatesFromSystemStoreToSSLContext(ctx)) { |
| return false; |
| } |
| } |
| |
| // Security level set to 0 for backwards compatibility to support TLS versions below v1.2 |
| if ((minimum_tls_version_ != -1 && minimum_tls_version_ < TLS1_2_VERSION) || (maximum_tls_version_ != -1 && maximum_tls_version_ < TLS1_2_VERSION)) { |
| SSL_CTX_set_security_level(ctx, 0); |
| } |
| |
| if (minimum_tls_version_ != -1) { |
| SSL_CTX_set_min_proto_version(ctx, minimum_tls_version_); |
| } |
| |
| if (maximum_tls_version_ != -1) { |
| SSL_CTX_set_max_proto_version(ctx, maximum_tls_version_); |
| } |
| |
| return true; |
| } |
| |
| bool SSLContextService::addP12CertificateToSSLContext(SSL_CTX* ctx) const { |
| auto error = utils::tls::processP12Certificate(certificate_, passphrase_, { |
| .cert_cb = [&] (auto cert) -> std::error_code { |
| if (SSL_CTX_use_certificate(ctx, cert.get()) != 1) { |
| return utils::tls::get_last_ssl_error_code(); |
| } |
| return {}; |
| }, |
| .chain_cert_cb = [&] (auto cacert) -> std::error_code { |
| if (SSL_CTX_add_extra_chain_cert(ctx, cacert.get()) != 1) { |
| return utils::tls::get_last_ssl_error_code(); |
| } |
| static_cast<void>(cacert.release()); // a successful SSL_CTX_add_extra_chain_cert() takes ownership of cacert |
| return {}; |
| }, |
| .priv_key_cb = [&] (auto priv_key) -> std::error_code { |
| if (SSL_CTX_use_PrivateKey(ctx, priv_key.get()) != 1) { |
| return utils::tls::get_last_ssl_error_code(); |
| } |
| return {}; |
| } |
| }); |
| if (error) { |
| logger_->log_error("{}", error.message()); |
| return false; |
| } |
| return true; |
| } |
| |
| bool SSLContextService::addPemCertificateToSSLContext(SSL_CTX* ctx) const { |
| if (SSL_CTX_use_certificate_chain_file(ctx, certificate_.string().c_str()) <= 0) { |
| logger_->log_error("Could not load client certificate {}, {}", certificate_.string(), getLatestOpenSSLErrorString()); |
| return false; |
| } |
| |
| if (!IsNullOrEmpty(passphrase_)) { |
| void* passphrase = const_cast<std::string*>(&passphrase_); |
| SSL_CTX_set_default_passwd_cb_userdata(ctx, passphrase); |
| SSL_CTX_set_default_passwd_cb(ctx, minifi::utils::tls::pemPassWordCb); |
| } |
| |
| if (!IsNullOrEmpty(private_key_)) { |
| int retp = SSL_CTX_use_PrivateKey_file(ctx, private_key_.string().c_str(), SSL_FILETYPE_PEM); |
| if (retp != 1) { |
| logger_->log_error("Could not load private key, {} on {}, {}", retp, private_key_, getLatestOpenSSLErrorString()); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| #ifdef WIN32 |
| bool SSLContextService::findClientCertificate(ClientCertCallback cb) const { |
| utils::tls::WindowsCertStore cert_store(utils::tls::WindowsCertStoreLocation{cert_store_location_}, client_cert_store_); |
| if (auto error = cert_store.error()) { |
| logger_->log_error("Could not open system certificate store {}/{} (client certificates): {}", cert_store_location_, client_cert_store_, error.message()); |
| return false; |
| } |
| |
| logger_->log_debug("Looking for client certificate in sytem store {}/{}", cert_store_location_, client_cert_store_); |
| |
| while (auto cert_ctx = cert_store.nextCert()) { |
| if (useClientCertificate(cert_ctx, cb)) { |
| return true; |
| } |
| } |
| |
| logger_->log_error("Could not find any suitable client certificate in sytem store {}/{}", cert_store_location_, client_cert_store_); |
| return false; |
| } |
| |
| #endif |
| |
| #ifdef WIN32 |
| bool SSLContextService::addClientCertificateFromSystemStoreToSSLContext(SSL_CTX* ctx) const { |
| return findClientCertificate([&] (auto cert, auto priv_key) -> bool { |
| auto cert_name = getCertName(cert); |
| if (SSL_CTX_use_certificate(ctx, cert.get()) != 1) { |
| logger_->log_error("Failed to set certificate from {}, {}", cert_name, getLatestOpenSSLErrorString()); |
| return false; |
| } |
| |
| if (SSL_CTX_use_PrivateKey(ctx, priv_key.get()) != 1) { |
| logger_->log_error("Failed to use private key {}, {}", cert_name, getLatestOpenSSLErrorString()); |
| return false; |
| } |
| return true; |
| }); |
| } |
| #else |
| bool SSLContextService::addClientCertificateFromSystemStoreToSSLContext(SSL_CTX* /*ctx*/) const { |
| logger_->log_error("Getting client certificate from the system store is only supported on Windows"); |
| return false; |
| } |
| #endif // WIN32 |
| |
| #ifdef WIN32 |
| bool SSLContextService::useClientCertificate(PCCERT_CONTEXT certificate, ClientCertCallback cb) const { |
| utils::tls::X509_unique_ptr x509_cert = utils::tls::convertWindowsCertificate(certificate); |
| if (!x509_cert) { |
| logger_->log_error("Failed to convert system store client certificate to X.509 format"); |
| return false; |
| } |
| |
| std::string x509_name = getCertName(x509_cert); |
| utils::tls::EVP_PKEY_unique_ptr private_key = utils::tls::extractPrivateKey(certificate); |
| if (!private_key) { |
| logger_->log_debug("Skipping client certificate {} because it has no exportable private key", x509_name); |
| return false; |
| } |
| |
| if (!client_cert_cn_.empty()) { |
| utils::tls::DistinguishedName dn = utils::tls::DistinguishedName::fromSlashSeparated(x509_name); |
| std::optional<std::string> cn = dn.getCN(); |
| if (!cn || *cn != client_cert_cn_) { |
| logger_->log_debug("Skipping client certificate {} because it doesn't match CN={}", x509_name, client_cert_cn_); |
| return false; |
| } |
| } |
| |
| utils::tls::EXTENDED_KEY_USAGE_unique_ptr key_usage{static_cast<EXTENDED_KEY_USAGE*>(X509_get_ext_d2i(x509_cert.get(), NID_ext_key_usage, nullptr, nullptr))}; |
| if (!key_usage) { |
| logger_->log_error("Skipping client certificate {} because it has no extended key usage", x509_name); |
| return false; |
| } |
| |
| if (!(client_cert_key_usage_.isSubsetOf(utils::tls::ExtendedKeyUsage{*key_usage}))) { |
| logger_->log_debug("Skipping client certificate {} because its extended key usage set does not contain all usages specified in {}", |
| x509_name, Configuration::nifi_security_windows_client_cert_key_usage); |
| return false; |
| } |
| |
| if (cb(std::move(x509_cert), std::move(private_key))) { |
| logger_->log_debug("Found client certificate {}", x509_name); |
| return true; |
| } |
| |
| return false; |
| } |
| #endif // WIN32 |
| |
| bool SSLContextService::addServerCertificatesFromSystemStoreToSSLContext(SSL_CTX* ctx) const { // NOLINT(readability-convert-member-functions-to-static) |
| #ifdef WIN32 |
| X509_STORE* ssl_store = SSL_CTX_get_cert_store(ctx); |
| if (!ssl_store) { |
| logger_->log_error("Could not get handle to SSL certificate store"); |
| return false; |
| } |
| |
| findServerCertificate([&] (auto cert) -> bool { |
| // return false to indicate that we wish to iterate over all subsequent certificates as well |
| auto cert_name = getCertName(cert); |
| int success = X509_STORE_add_cert(ssl_store, cert.get()); |
| if (success == 1) { |
| logger_->log_debug("Added server certificate {} from the system store to the SSL store", cert_name); |
| return false; |
| } |
| |
| auto err = ERR_peek_last_error(); |
| if (ERR_GET_REASON(err) == X509_R_CERT_ALREADY_IN_HASH_TABLE) { |
| logger_->log_debug("Ignoring duplicate server certificate {}", cert_name); |
| return false; |
| } |
| |
| logger_->log_error("Failed to add server certificate {} to the SSL store; error: {}", cert_name, getLatestOpenSSLErrorString()); |
| return false; |
| }); |
| |
| return true; |
| #else |
| static const auto default_ca_file = utils::getDefaultCAFile(); |
| if (default_ca_file) { |
| SSL_CTX_load_verify_file(ctx, std::string(*default_ca_file).c_str()); |
| } else { |
| SSL_CTX_set_default_verify_paths(ctx); |
| } |
| return true; |
| #endif // WIN32 |
| } |
| |
| #ifdef WIN32 |
| bool SSLContextService::findServerCertificate(ServerCertCallback cb) const { |
| utils::tls::WindowsCertStore cert_store(utils::tls::WindowsCertStoreLocation{cert_store_location_}, server_cert_store_); |
| if (auto error = cert_store.error()) { |
| logger_->log_error("Could not open system certificate store {}/{} (server certificates): {}", cert_store_location_, server_cert_store_, error.message()); |
| return false; |
| } |
| |
| logger_->log_debug("Adding server certificates from system store {}/{}", cert_store_location_, server_cert_store_); |
| |
| while (auto cert_ctx = cert_store.nextCert()) { |
| if (useServerCertificate(cert_ctx, cb)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| #endif |
| |
| #ifdef WIN32 |
| bool SSLContextService::useServerCertificate(PCCERT_CONTEXT certificate, ServerCertCallback cb) const { |
| utils::tls::X509_unique_ptr x509_cert = utils::tls::convertWindowsCertificate(certificate); |
| if (!x509_cert) { |
| logger_->log_error("Failed to convert system store server certificate to X.509 format"); |
| return false; |
| } |
| |
| return cb(std::move(x509_cert)); |
| } |
| #endif // WIN32 |
| |
| /** |
| * If OpenSSL is not installed we may still continue operations. Nullptr will |
| * be returned and it will be up to the caller to determine if this failure is |
| * recoverable. |
| */ |
| std::unique_ptr<SSLContext> SSLContextService::createSSLContext() { |
| SSL_library_init(); |
| const SSL_METHOD *method = nullptr; |
| |
| OpenSSL_add_all_algorithms(); |
| SSL_load_error_strings(); |
| method = TLS_client_method(); |
| SSL_CTX *ctx = SSL_CTX_new(method); |
| |
| if (ctx == nullptr) { |
| return nullptr; |
| } |
| |
| if (!configure_ssl_context(ctx)) { |
| SSL_CTX_free(ctx); |
| return nullptr; |
| } |
| |
| return std::make_unique<SSLContext>(ctx); |
| } |
| |
| const std::filesystem::path &SSLContextService::getCertificateFile() const { |
| std::lock_guard<std::mutex> lock(initialization_mutex_); |
| return certificate_; |
| } |
| |
| const std::string &SSLContextService::getPassphrase() const { |
| std::lock_guard<std::mutex> lock(initialization_mutex_); |
| return passphrase_; |
| } |
| |
| const std::filesystem::path &SSLContextService::getPrivateKeyFile() const { |
| std::lock_guard<std::mutex> lock(initialization_mutex_); |
| return private_key_; |
| } |
| |
| const std::filesystem::path &SSLContextService::getCACertificate() const { |
| std::lock_guard<std::mutex> lock(initialization_mutex_); |
| return ca_certificate_; |
| } |
| |
| void SSLContextService::onEnable() { |
| std::filesystem::path default_dir; |
| |
| if (configuration_) { |
| if (auto default_dir_str = configuration_->get(Configure::nifi_default_directory)) { |
| default_dir = default_dir_str.value(); |
| } |
| } |
| |
| logger_->log_trace("onEnable()"); |
| |
| certificate_.clear(); |
| if (auto certificate = getProperty(ClientCertificate.name)) { |
| if (is_valid_and_readable_path(*certificate)) { |
| certificate_ = *certificate; |
| } else { |
| logger_->log_warn("Cannot open certificate file {}", *certificate); |
| if (is_valid_and_readable_path(default_dir / *certificate)) { |
| certificate_ = default_dir / *certificate; |
| } else { |
| logger_->log_error("Cannot open certificate file {}", (default_dir / *certificate)); |
| } |
| } |
| } else { |
| logger_->log_debug("Certificate empty"); |
| } |
| |
| private_key_.clear(); |
| if (!certificate_.empty() && !isFileTypeP12(certificate_)) { |
| if (auto private_key = getProperty(PrivateKey.name)) { |
| if (is_valid_and_readable_path(*private_key)) { |
| private_key_ = *private_key; |
| } else { |
| logger_->log_warn("Cannot open private key file {}", *private_key); |
| if (is_valid_and_readable_path(default_dir / *private_key)) { |
| private_key_ = default_dir / *private_key; |
| } else { |
| logger_->log_error("Cannot open private key file {}", (default_dir / *private_key)); |
| } |
| } |
| logger_->log_info("Using private key file {}", private_key_); |
| } else { |
| logger_->log_debug("Private key empty"); |
| } |
| } |
| |
| passphrase_.clear(); |
| if (auto passphrase = getProperty(Passphrase.name)) { |
| passphrase_ = *passphrase; |
| std::ifstream passphrase_file(passphrase_); |
| if (passphrase_file.good()) { |
| // we should read it from the file |
| passphrase_.assign((std::istreambuf_iterator<char>(passphrase_file)), std::istreambuf_iterator<char>()); |
| } else { |
| auto test_passphrase = default_dir / passphrase_; |
| std::ifstream passphrase_file_test(test_passphrase); |
| if (passphrase_file_test.good()) { |
| passphrase_.assign((std::istreambuf_iterator<char>(passphrase_file_test)), std::istreambuf_iterator<char>()); |
| } else { |
| // not an invalid file since we support a passphrase of unencrypted text |
| } |
| passphrase_file_test.close(); |
| } |
| passphrase_file.close(); |
| } else { |
| logger_->log_debug("No pass phrase for {}", certificate_); |
| } |
| |
| ca_certificate_.clear(); |
| if (auto ca_certificate = getProperty(CACertificate.name)) { |
| if (is_valid_and_readable_path(*ca_certificate)) { |
| ca_certificate_ = *ca_certificate; |
| } else { |
| logger_->log_warn("Cannot open CA certificate file {}", *ca_certificate); |
| if (is_valid_and_readable_path(default_dir / *ca_certificate)) { |
| ca_certificate_ = default_dir / *ca_certificate; |
| } else { |
| logger_->log_error("Cannot open CA certificate file {}", (default_dir / *ca_certificate)); |
| } |
| } |
| logger_->log_info("Using CA certificate file {}", ca_certificate_); |
| } else { |
| logger_->log_debug("CA Certificate empty"); |
| } |
| |
| use_system_cert_store_ = (getProperty(UseSystemCertStore.name) | utils::andThen(parsing::parseBool)).value_or(false); |
| |
| #ifdef WIN32 |
| cert_store_location_ = getProperty(CertStoreLocation.name).value_or(""); |
| server_cert_store_ = getProperty(ServerCertStore.name).value_or(""); |
| client_cert_store_ = getProperty(ClientCertStore.name).value_or(""); |
| client_cert_cn_ = getProperty(ClientCertCN.name).value_or(""); |
| |
| std::string client_cert_key_usage = getProperty(ClientCertKeyUsage.name).value_or(""); |
| client_cert_key_usage_ = utils::tls::ExtendedKeyUsage{client_cert_key_usage}; |
| #endif // WIN32 |
| |
| logger_->log_debug("Using certificate file \"{}\"", certificate_); |
| logger_->log_debug("Using private key file \"{}\"", private_key_); |
| logger_->log_debug("Using CA certificate file \"{}\"", ca_certificate_); |
| logger_->log_debug("Using the system cert store: {}", use_system_cert_store_ ? "yes" : "no"); |
| |
| verifyCertificateExpiration(); |
| } |
| |
| void SSLContextService::initializeProperties() { |
| setSupportedProperties(Properties); |
| } |
| |
| void SSLContextService::verifyCertificateExpiration() { |
| auto verify = [&] (const std::filesystem::path& cert_file, const utils::tls::X509_unique_ptr& cert) { |
| if (auto end_date = utils::tls::getCertificateExpiration(cert)) { |
| std::string end_date_str = utils::timeutils::getTimeStr(*end_date); |
| if (end_date.value() < std::chrono::system_clock::now()) { |
| logger_->log_error("Certificate in '{}' expired at {}", cert_file, end_date_str); |
| } else if (auto diff = end_date.value() - std::chrono::system_clock::now(); diff < std::chrono::weeks{2}) { |
| logger_->log_warn("Certificate in '{}' will expire at {}", cert_file, end_date_str); |
| } else { |
| logger_->log_debug("Certificate in '{}' will expire at {}", cert_file, end_date_str); |
| } |
| } else { |
| logger_->log_error("Could not determine expiration date for certificate in '{}'", cert_file); |
| } |
| }; |
| if (!IsNullOrEmpty(certificate_)) { |
| if (isFileTypeP12(certificate_)) { |
| auto error = utils::tls::processP12Certificate(certificate_, passphrase_, { |
| .cert_cb = [&](auto cert) -> std::error_code { |
| verify(certificate_, cert); |
| return {}; |
| }, |
| .chain_cert_cb = [&](auto cert) -> std::error_code { |
| verify(certificate_, cert); |
| return {}; |
| }, |
| .priv_key_cb = {} |
| }); |
| if (error) { |
| logger_->log_error("{}", error.value()); |
| } |
| } else { |
| auto error = utils::tls::processPEMCertificate(certificate_, passphrase_, { |
| .cert_cb = [&](auto cert) -> std::error_code { |
| verify(certificate_, cert); |
| return {}; |
| }, |
| .chain_cert_cb = [&](auto cert) -> std::error_code { |
| verify(certificate_, cert); |
| return {}; |
| }, |
| .priv_key_cb = {} |
| }); |
| if (error) { |
| logger_->log_error("{}", error.value()); |
| } |
| } |
| } |
| |
| if (!IsNullOrEmpty(ca_certificate_)) { |
| auto error = utils::tls::processPEMCertificate(ca_certificate_, std::nullopt, { |
| .cert_cb = [&](auto cert) -> std::error_code { |
| verify(ca_certificate_, cert); |
| return {}; |
| }, |
| .chain_cert_cb = [&](auto cert) -> std::error_code { |
| verify(ca_certificate_, cert); |
| return {}; |
| }, |
| .priv_key_cb = {} |
| }); |
| if (error) { |
| logger_->log_error("{}", error.message()); |
| } |
| } |
| |
| #ifdef WIN32 |
| if (use_system_cert_store_ && IsNullOrEmpty(certificate_)) { |
| findClientCertificate([&] (auto cert, auto /*priv_key*/) -> bool { |
| auto cert_name = getCertName(cert); |
| verify(cert_name, cert); |
| return false; // keep on iterating, check all |
| }); |
| } |
| |
| if (use_system_cert_store_ && IsNullOrEmpty(ca_certificate_)) { |
| findServerCertificate([&] (auto cert) -> bool { |
| auto cert_name = getCertName(cert); |
| verify(cert_name, cert); |
| return false; // keep on iterating, check all |
| }); |
| } |
| #endif |
| } |
| |
| REGISTER_RESOURCE_IMPLEMENTATION(SSLContextService, "SSLContextService", ControllerService); |
| |
| } // namespace org::apache::nifi::minifi::controllers |