Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/bmcweb.service.in
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Type=simple
WorkingDirectory=/home/root
SyslogLevelPrefix=true
WatchdogSec=@BMCWEB_WATCHDOG_TIMEOUT_SECONDS@s
@BMCWEB_OPENSSL_ENV@

[Install]
WantedBy=network.target
23 changes: 23 additions & 0 deletions config/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@ feature_options = [
string_options = [
'dns-resolver',
'mutual-tls-common-name-parsing-default',
'openssl-config-path',
'redfish-manager-uri-name',
'redfish-system-uri-name',
'uri-cert',
'uri-key',
]

int_options = ['http-body-limit', 'watchdog-timeout-seconds']
Expand Down Expand Up @@ -180,6 +183,25 @@ foreach index : range(ports.length())
)
endforeach

# When openssl-config-path is set, install bmcweb's openssl.cnf there and point
# the bmcweb process at it via OPENSSL_CONF, so OpenSSL activates the configured
# providers (e.g. tpm2) for bmcweb only. The environment is templated into the
# unit file (rather than a drop-in) so it ships inside bmcweb.service and needs
# no recipe FILES change. Provider settings such as the TPM TCTI live in
# openssl.cnf itself, not here.
openssl_config_path = get_option('openssl-config-path')
openssl_service_env = ''
if openssl_config_path != ''
fs = import('fs')
install_data(
'openssl.cnf',
install_dir: fs.parent(openssl_config_path),
rename: fs.name(openssl_config_path),
)

openssl_service_env = 'Environment="OPENSSL_CONF=' + openssl_config_path + '"'
endif

configure_file(
input: 'bmcweb.service.in',
output: 'bmcweb.service',
Expand All @@ -191,6 +213,7 @@ configure_file(
'BMCWEB_WATCHDOG_TIMEOUT_SECONDS': get_option(
'watchdog-timeout-seconds',
),
'BMCWEB_OPENSSL_ENV': openssl_service_env,
},
),
)
Expand Down
37 changes: 37 additions & 0 deletions config/openssl.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# OpenSSL configuration scoped to the bmcweb process.
#
# bmcweb.service points OPENSSL_CONF at this file (when the meson option
# openssl-config-path is set), so the providers activated here apply to bmcweb
# only and not to the rest of the system.
#
# This is what enables loading a TLS private key from a cryptographic provider
# such as a TPM (e.g. uri-key=handle:0x81000000): activating the tpm2 provider
# registers the OSSL_STORE loader for the handle scheme.

openssl_conf = openssl_init

# Treat configuration errors as fatal. If a provider listed below cannot be
# activated (e.g. tpm2 is not installed) OpenSSL initialization fails and
# bmcweb fails to start, rather than silently serving without it.
config_diagnostics = 1

[openssl_init]
providers = provider_sect

[provider_sect]
default = default_sect
tpm2 = tpm2_sect

# Explicitly activating any provider disables the implicit default provider, so
# the default provider must be activated here as well or standard algorithms
# become unavailable.
[default_sect]
activate = 1

[tpm2_sect]
activate = 1
# TPM device / TCTI used by the tpm2 provider. device:/dev/tpmrm0 is the
# in-kernel resource manager, which arbitrates concurrent TLS handshakes.
# Change this for a different TPM access method (e.g. a userspace resource
# manager or a software TPM socket).
tcti = device:/dev/tpmrm0
7 changes: 5 additions & 2 deletions http/http_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,11 @@ class ConnectionInfo : public std::enable_shared_from_this<ConnectionInfo>
timer.cancel();
if (ec)
{
BMCWEB_LOG_ERROR("SSL Handshake failed - id: {} error: {}", connId,
ec.message());
BMCWEB_LOG_ERROR(
"SSL Handshake failed - id: {} error: {} (negotiated {})",
connId, ec.message(),
sslConn ? SSL_get_version(sslConn->native_handle()) : "no-ssl");
ensuressl::logOpenSSLErrors("aggregation client SSL handshake");
state = ConnState::handshakeFailed;
waitAndRetry();
return;
Expand Down
6 changes: 5 additions & 1 deletion http/http_connection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "logging.hpp"
#include "mutual_tls.hpp"
#include "sessions.hpp"
#include "ssl_key_handler.hpp"
#include "str_utility.hpp"
#include "utility.hpp"

Expand Down Expand Up @@ -237,7 +238,10 @@ class Connection :
buffer.consume(bytesParsed);
if (ec)
{
BMCWEB_LOG_WARNING("{} SSL handshake failed", logPtr(this));
BMCWEB_LOG_WARNING("{} SSL handshake failed (negotiated {})",
logPtr(this),
SSL_get_version(adaptor.native_handle()));
ensuressl::logOpenSSLErrors("server SSL handshake");
return;
}
BMCWEB_LOG_DEBUG("{} SSL handshake succeeded", logPtr(this));
Expand Down
69 changes: 66 additions & 3 deletions http/ssl_context_factory_sni.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@

#include <openssl/ssl.h>

#include <boost/asio/buffer.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/system/error_code.hpp>

#include <functional>
#include <memory>
#include <optional>
#include <string>

namespace bmcweb
Expand All @@ -30,6 +33,16 @@ inline int tlsVerifyCallback([[maybe_unused]] int preverified,
return preverified;
}

// Returns true if the configured mTLS key location is a URI (e.g. a TPM
// "handle:0x81000000" or a "file://" URI) rather than a bare filesystem path.
// URI keys are loaded via the OpenSSL OSSL_STORE API; bare paths keep using
// use_private_key_file.
inline bool isKeyUri(const std::string& keyLocation)
{
return keyLocation.starts_with("handle:") ||
keyLocation.find("://") != std::string::npos;
}

// Callable object for SNI context factory with state
struct SniContextFactoryState
{
Expand Down Expand Up @@ -100,6 +113,12 @@ struct SniContextFactoryState

// Create primary SSL context (default/non-mTLS) using ensuressl
auto primaryCtx = ensuressl::getSslServerContext();
if (primaryCtx == nullptr)
{
BMCWEB_LOG_CRITICAL(
"Failed to build the primary SSL context; TLS is unavailable");
return nullptr;
}

// Set up SNI callback to switch contexts based on hostname
SSL_CTX_set_tlsext_servername_callback(primaryCtx->native_handle(),
Expand Down Expand Up @@ -130,11 +149,55 @@ struct SniContextFactoryState
boost::asio::ssl::context::single_dh_use);

BMCWEB_LOG_INFO("Loading certificate from: {}", mtlsCertFile);
clientAuthContext->use_certificate_chain_file(mtlsCertFile);
if (ensuressl::isProviderCert(mtlsCertFile))
{
// Cert lives in a provider (e.g. a TPM NV index);
// use_certificate_chain_file cannot read a provider, so load it
// via OSSL_STORE as PEM and install the buffer.
std::optional<std::string> pem =
ensuressl::loadCertPemFromUri(mtlsCertFile);
if (!pem)
{
BMCWEB_LOG_ERROR(
"Failed to load mTLS server cert from URI: {}",
mtlsCertFile);
clientAuthContext = std::nullopt;
return false;
}
boost::system::error_code ec;
boost::asio::const_buffer certBuf(pem->data(), pem->size());
clientAuthContext->use_certificate_chain(certBuf, ec);
if (ec)
{
BMCWEB_LOG_ERROR("Failed to install mTLS server cert: {}",
ec.message());
clientAuthContext = std::nullopt;
return false;
}
}
else
{
clientAuthContext->use_certificate_chain_file(mtlsCertFile);
}

BMCWEB_LOG_INFO("Loading private key from: {}", mtlsKeyFile);
clientAuthContext->use_private_key_file(
mtlsKeyFile, boost::asio::ssl::context::pem);
if (isKeyUri(mtlsKeyFile))
{
if (!ensuressl::loadPrivateKeyUriIntoContext(*clientAuthContext,
mtlsKeyFile))
{
BMCWEB_LOG_ERROR(
"Failed to load mTLS server key from URI: {}",
mtlsKeyFile);
clientAuthContext = std::nullopt;
return false;
}
}
else
{
clientAuthContext->use_private_key_file(
mtlsKeyFile, boost::asio::ssl::context::pem);
}

BMCWEB_LOG_INFO("Loading trust store from: {}", mtlsTrustStore);
clientAuthContext->add_verify_path(mtlsTrustStore);
Expand Down
29 changes: 29 additions & 0 deletions include/ssl_key_handler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <memory>
#include <optional>
#include <string>
#include <string_view>

namespace ensuressl
{
Expand Down Expand Up @@ -45,4 +46,32 @@ std::shared_ptr<boost::asio::ssl::context> getSslServerContext();
std::optional<boost::asio::ssl::context> getSSLClientContext(
VerifyCertificate verifyCertificate);

// Loads a private key from a URI (file:// or a provider-backed scheme such as
// a TPM handle:) via the OpenSSL OSSL_STORE API and installs it into the SSL
// context. The certificate must already be set on the context. Returns false
// on failure.
bool loadPrivateKeyUriIntoContext(boost::asio::ssl::context& sslCtx,
std::string_view uri);

// Resolves a certificate location (a file:// URI or a bare absolute filesystem
// path) to a filesystem path usable with use_certificate_chain_file. Provider
// schemes (e.g. a TPM handle:) are handled separately via loadCertPemFromUri;
// returns nullopt for those and other unsupported schemes.
std::optional<std::string> fileUriToPath(std::string_view uri);

// True if the certificate location is a provider object (e.g. a TPM NV index
// "handle:0x1500010") that must be read via OSSL_STORE rather than the
// filesystem. file:// URIs and bare paths are filesystem paths.
bool isProviderCert(std::string_view location);

// Loads a certificate from a provider URI (e.g. a TPM NV "handle:") via the
// OpenSSL OSSL_STORE API and returns it as a PEM string. This is the cert
// counterpart of loadPrivateKeyUriIntoContext; the returned PEM feeds the same
// use_certificate_chain path a filesystem cert does. Returns nullopt on
// failure.
std::optional<std::string> loadCertPemFromUri(const std::string& uri);

// Drains and logs the OpenSSL error queue with the given context prefix.
void logOpenSSLErrors(std::string_view context);

} // namespace ensuressl
46 changes: 46 additions & 0 deletions meson.options
Original file line number Diff line number Diff line change
Expand Up @@ -624,3 +624,49 @@ option(
value: 'enabled',
description: 'Enable the Hardware Isolation feature',
)

# BMCWEB_URI_KEY
option(
'uri-key',
type: 'string',
value: '',
description: '''URI of the private key for the BMC's mTLS identity, loaded
via the OpenSSL OSSL_STORE API. It is used in both BMC-to-BMC
aggregation roles: the key the satellite presents as the TLS
server, and the key the aggregator uses as the TLS client.
When empty (the default) the key is read from the filesystem
PEM, preserving existing behavior. A provider-backed URI
(e.g. a TPM object handle such as handle:0x81000000) keeps
the key inside the provider; it requires the tpm2 provider to
be activated via openssl-config-path.''',
)

# BMCWEB_URI_CERT
option(
'uri-cert',
type: 'string',
value: '',
description: '''URI of the certificate for the BMC's mTLS identity. Paired
with uri-key; used for both aggregation roles (satellite
server cert and aggregator client cert). A file:// URI (or
bare path) keeps the certificate on the filesystem; a
provider URI such as a TPM NV index (handle:0x1500010) is
read via the OpenSSL OSSL_STORE API. When empty (the default)
each role uses its existing hardcoded cert path, preserving
existing behavior.''',
)

# BMCWEB_OPENSSL_CONFIG_PATH
option(
'openssl-config-path',
type: 'string',
value: '',
description: '''Path to an OpenSSL configuration file scoped to the bmcweb
process via the OPENSSL_CONF environment variable. When set,
bmcweb installs its openssl.cnf to this path and the
bmcweb.service unit points OPENSSL_CONF at it, so OpenSSL
activates the configured providers (e.g. tpm2) for bmcweb
only. When empty (the default) bmcweb uses the system
OpenSSL configuration and the implicit default provider,
with no behavior change.''',
)
Loading