blob: 165805d864f15c7e9905e0927ed46a70d346eb62 [file] [log] [blame]
/**
* 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>
#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 <set>
#include "core/PropertyBuilder.h"
#include "core/Resource.h"
#include "io/validation.h"
#include "properties/Configure.h"
#include "utils/gsl.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 {
const core::Property SSLContextService::ClientCertificate(
core::PropertyBuilder::createProperty("Client Certificate")
->withDescription("Client Certificate")
->isRequired(false)
->build());
const core::Property SSLContextService::PrivateKey(
core::PropertyBuilder::createProperty("Private Key")
->withDescription("Private Key file")
->isRequired(false)
->build());
const core::Property SSLContextService::Passphrase(
core::PropertyBuilder::createProperty("Passphrase")
->withDescription("Client passphrase. Either a file or unencrypted text")
->isRequired(false)
->build());
const core::Property SSLContextService::CACertificate(
core::PropertyBuilder::createProperty("CA Certificate")
->withDescription("CA certificate file")
->isRequired(false)
->build());
const core::Property SSLContextService::UseSystemCertStore(
core::PropertyBuilder::createProperty("Use System Cert Store")
->withDescription("Whether to use the certificates in the OS's certificate store")
->isRequired(false)
->withDefaultValue<bool>(false)
->build());
#ifdef WIN32
const core::Property SSLContextService::CertStoreLocation(
core::PropertyBuilder::createProperty("Certificate Store Location")
->withDescription("One of the Windows certificate store locations, eg. LocalMachine or CurrentUser")
->withAllowableValues(utils::tls::WindowsCertStoreLocation::allowedLocations())
->isRequired(false)
->withDefaultValue(utils::tls::WindowsCertStoreLocation::defaultLocation())
->build());
const core::Property SSLContextService::ServerCertStore(
core::PropertyBuilder::createProperty("Server Cert Store")
->withDescription("The name of the certificate store which contains the server certificate")
->isRequired(false)
->withDefaultValue("ROOT")
->build());
const core::Property SSLContextService::ClientCertStore(
core::PropertyBuilder::createProperty("Client Cert Store")
->withDescription("The name of the certificate store which contains the client certificate")
->isRequired(false)
->withDefaultValue("MY")
->build());
const core::Property SSLContextService::ClientCertCN(
core::PropertyBuilder::createProperty("Client Cert CN")
->withDescription("The CN that the client certificate is required to match; default: use the first available client certificate in the store")
->isRequired(false)
->build());
const core::Property SSLContextService::ClientCertKeyUsage(
core::PropertyBuilder::createProperty("Client Cert Key Usage")
->withDescription("Comma-separated list of enhanced key usage values that the client certificate is required to have")
->isRequired(false)
->withDefaultValue("Client Authentication")
->build());
#endif // WIN32
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;
}
}
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 {
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;
}
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_cert->name);
return false;
}
if (!client_cert_cn_.empty()) {
utils::tls::DistinguishedName dn = utils::tls::DistinguishedName::fromSlashSeparated(x509_cert->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_cert->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_cert->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_cert->name, Configuration::nifi_security_windows_client_cert_key_usage);
return false;
}
std::string cert_name = x509_cert->name;
if (cb(std::move(x509_cert), std::move(private_key))) {
logger_->log_debug("Found client certificate %s", cert_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
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 = TLSv1_2_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() {
std::lock_guard<std::mutex> lock(initialization_mutex_);
return certificate_;
}
const std::string &SSLContextService::getPassphrase() {
std::lock_guard<std::mutex> lock(initialization_mutex_);
return passphrase_;
}
const std::filesystem::path &SSLContextService::getPrivateKeyFile() {
std::lock_guard<std::mutex> lock(initialization_mutex_);
return private_key_;
}
const std::filesystem::path &SSLContextService::getCACertificate() {
std::lock_guard<std::mutex> lock(initialization_mutex_);
return ca_certificate_;
}
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();
}
} // namespace
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.getName())) {
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.getName())) {
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.getName(), 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.getName())) {
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.getName(), use_system_cert_store_);
#ifdef WIN32
getProperty(CertStoreLocation.getName(), cert_store_location_);
getProperty(ServerCertStore.getName(), server_cert_store_);
getProperty(ClientCertStore.getName(), client_cert_store_);
getProperty(ClientCertCN.getName(), client_cert_cn_);
std::string client_cert_key_usage;
getProperty(ClientCertKeyUsage.getName(), 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 {
verify(cert->name, cert);
return false; // keep on iterating, check all
});
}
if (use_system_cert_store_ && IsNullOrEmpty(ca_certificate_)) {
findServerCertificate([&] (auto cert) -> bool {
verify(cert->name, cert);
return false; // keep on iterating, check all
});
}
#endif
}
REGISTER_RESOURCE(SSLContextService, ControllerService);
} // namespace org::apache::nifi::minifi::controllers