libminifi/src/controllers/SSLContextService.cpp (488 lines of code) (raw):
/**
* 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"
#ifdef OPENSSL_SUPPORT
#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
#endif // OPENSSL_SUPPORT
#include <fstream>
#include <memory>
#include <string>
#include "core/Resource.h"
#include "io/validation.h"
#include "properties/Configure.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;
}
ControllerService::initialize();
initializeProperties();
initialized_ = true;
}
#ifdef OPENSSL_SUPPORT
bool SSLContextService::configure_ssl_context(SSL_CTX *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)) {
core::logging::LOG_ERROR(logger_) << "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) {
core::logging::LOG_ERROR(logger_) << "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
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_);
} else {
SSL_CTX_set_max_proto_version(ctx, TLS1_2_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) {
core::logging::LOG_ERROR(logger_) << 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) {
core::logging::LOG_ERROR(logger_) << "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) {
core::logging::LOG_ERROR(logger_) << "Could not load private key, " << retp << " on " << 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 %s/%s (client certificates): %s", cert_store_location_, client_cert_store_, error.message());
return false;
}
logger_->log_debug("Looking for client certificate in sytem store %s/%s", 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 %s/%s", 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 %s, %s", cert_name, getLatestOpenSSLErrorString);
return false;
}
if (SSL_CTX_use_PrivateKey(ctx, priv_key.get()) != 1) {
logger_->log_error("Failed to use private key %s, %s", 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 %s 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 %s because it doesn't match CN=%s", 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 %s 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 %s because its extended key usage set does not contain all usages specified in %s",
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 %s", 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 %s 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 %s", cert_name);
return false;
}
logger_->log_error("Failed to add server certificate %s to the SSL store; error: %s", cert_name, getLatestOpenSSLErrorString());
return false;
});
return true;
#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 %s/%s (server certificates): %s", cert_store_location_, server_cert_store_, error.message());
return false;
}
logger_->log_debug("Adding server certificates from system store %s/%s", 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
#endif // OPENSSL_SUPPORT
/**
* 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() {
#ifdef OPENSSL_SUPPORT
SSL_library_init();
const SSL_METHOD *method;
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);
#else
return nullptr;
#endif
}
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)) {
if (is_valid_and_readable_path(*certificate)) {
certificate_ = *certificate;
} else {
logger_->log_warn("Cannot open certificate file %s", *certificate);
if (is_valid_and_readable_path(default_dir / *certificate)) {
certificate_ = default_dir / *certificate;
} else {
logger_->log_error("Cannot open certificate file %s", (default_dir / *certificate).string());
}
}
} else {
logger_->log_debug("Certificate empty");
}
private_key_.clear();
if (!certificate_.empty() && !isFileTypeP12(certificate_)) {
if (auto private_key = getProperty(PrivateKey)) {
if (is_valid_and_readable_path(*private_key)) {
private_key_ = *private_key;
} else {
logger_->log_warn("Cannot open private key file %s", *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 %s", (default_dir / *private_key).string());
}
}
logger_->log_info("Using private key file %s", private_key_.string());
} else {
logger_->log_debug("Private key empty");
}
}
passphrase_.clear();
if (!getProperty(Passphrase, passphrase_)) {
logger_->log_debug("No pass phrase for %s", certificate_.string());
} else {
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();
}
ca_certificate_.clear();
if (auto ca_certificate = getProperty(CACertificate)) {
if (is_valid_and_readable_path(*ca_certificate)) {
ca_certificate_ = *ca_certificate;
} else {
logger_->log_warn("Cannot open CA certificate file %s", *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 %s", (default_dir / *ca_certificate).string());
}
}
logger_->log_info("Using CA certificate file %s", ca_certificate_.string());
} else {
logger_->log_debug("CA Certificate empty");
}
getProperty(UseSystemCertStore, use_system_cert_store_);
#ifdef WIN32
getProperty(CertStoreLocation, cert_store_location_);
getProperty(ServerCertStore, server_cert_store_);
getProperty(ClientCertStore, client_cert_store_);
getProperty(ClientCertCN, client_cert_cn_);
std::string client_cert_key_usage;
getProperty(ClientCertKeyUsage, client_cert_key_usage);
client_cert_key_usage_ = utils::tls::ExtendedKeyUsage{client_cert_key_usage};
#endif // WIN32
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()) {
core::logging::LOG_ERROR(logger_) << "Certificate in '" << cert_file << "' expired at " << end_date_str;
} else if (auto diff = end_date.value() - std::chrono::system_clock::now(); diff < std::chrono::weeks{2}) {
core::logging::LOG_WARN(logger_) << "Certificate in '" << cert_file << "' will expire at " << end_date_str;
} else {
core::logging::LOG_DEBUG(logger_) << "Certificate in '" << cert_file << "' will expire at " << end_date_str;
}
} else {
core::logging::LOG_ERROR(logger_) << "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) {
core::logging::LOG_ERROR(logger_) << 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) {
core::logging::LOG_ERROR(logger_) << 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) {
core::logging::LOG_ERROR(logger_) << 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(SSLContextService, ControllerService);
} // namespace org::apache::nifi::minifi::controllers