diff --git a/config/bmcweb.service.in b/config/bmcweb.service.in index 63024c83a..d1592614d 100644 --- a/config/bmcweb.service.in +++ b/config/bmcweb.service.in @@ -18,6 +18,7 @@ Type=simple WorkingDirectory=/home/root SyslogLevelPrefix=true WatchdogSec=@BMCWEB_WATCHDOG_TIMEOUT_SECONDS@s +@BMCWEB_OPENSSL_ENV@ [Install] WantedBy=network.target diff --git a/config/meson.build b/config/meson.build index f571d26db..99282f23b 100644 --- a/config/meson.build +++ b/config/meson.build @@ -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'] @@ -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', @@ -191,6 +213,7 @@ configure_file( 'BMCWEB_WATCHDOG_TIMEOUT_SECONDS': get_option( 'watchdog-timeout-seconds', ), + 'BMCWEB_OPENSSL_ENV': openssl_service_env, }, ), ) diff --git a/config/openssl.cnf b/config/openssl.cnf new file mode 100644 index 000000000..944841a5f --- /dev/null +++ b/config/openssl.cnf @@ -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 diff --git a/http/http_client.hpp b/http/http_client.hpp index ea8e069ee..0b3dc111e 100644 --- a/http/http_client.hpp +++ b/http/http_client.hpp @@ -286,8 +286,11 @@ class ConnectionInfo : public std::enable_shared_from_this 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; diff --git a/http/http_connection.hpp b/http/http_connection.hpp index b8f76d08f..0395e0e32 100644 --- a/http/http_connection.hpp +++ b/http/http_connection.hpp @@ -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" @@ -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)); diff --git a/http/ssl_context_factory_sni.hpp b/http/ssl_context_factory_sni.hpp index 3db613012..29fa6ebdf 100644 --- a/http/ssl_context_factory_sni.hpp +++ b/http/ssl_context_factory_sni.hpp @@ -6,11 +6,14 @@ #include +#include #include #include +#include #include #include +#include #include namespace bmcweb @@ -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 { @@ -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(), @@ -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 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); diff --git a/include/ssl_key_handler.hpp b/include/ssl_key_handler.hpp index fad89fc2b..b890c3008 100644 --- a/include/ssl_key_handler.hpp +++ b/include/ssl_key_handler.hpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace ensuressl { @@ -45,4 +46,32 @@ std::shared_ptr getSslServerContext(); std::optional 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 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 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 diff --git a/meson.options b/meson.options index 5ef438066..ab3c135b9 100644 --- a/meson.options +++ b/meson.options @@ -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.''', +) diff --git a/src/ssl_key_handler.cpp b/src/ssl_key_handler.cpp index 000b41fad..5f55d5b2f 100644 --- a/src/ssl_key_handler.cpp +++ b/src/ssl_key_handler.cpp @@ -27,6 +27,7 @@ extern "C" #include #include #include +#include #include #include #include @@ -34,6 +35,7 @@ extern "C" #include } +#include #include #include #include @@ -42,6 +44,7 @@ extern "C" #include #include #include +#include #include #include @@ -204,6 +207,47 @@ std::string verifyOpensslKeyCert(const std::string& filepath) return fileContents; } +// Reads a certificate-only PEM file (no private key) into memory. Used when +// the matching private key lives in a provider (e.g. a TPM) and is loaded +// separately via a key URI, so verifyOpensslKeyCert (which requires the key in +// the same file) cannot be used. +static std::string readCertOnlyFile(const std::string& filepath) +{ + boost::beast::file_posix file; + boost::system::error_code ec; + file.open(filepath.c_str(), boost::beast::file_mode::read, ec); + if (ec) + { + BMCWEB_LOG_ERROR("Failed to open certificate file {}", filepath); + return ""; + } + std::string fileContents; + fileContents.resize(static_cast(file.size(ec)), '\0'); + file.read(fileContents.data(), fileContents.size(), ec); + if (ec) + { + BMCWEB_LOG_ERROR("Failed to read certificate file {}", filepath); + return ""; + } + + BIO* bufio = BIO_new_mem_buf(static_cast(fileContents.data()), + static_cast(fileContents.size())); + X509* x509 = PEM_read_bio_X509(bufio, nullptr, nullptr, nullptr); + BIO_free(bufio); + if (x509 == nullptr) + { + BMCWEB_LOG_ERROR("Failed to load X509 certificate from {}", filepath); + return ""; + } + bool certValid = validateCertificate(x509); + X509_free(x509); + if (!certValid) + { + return ""; + } + return fileContents; +} + X509* loadCert(const std::string& filePath) { BIO* certFileBio = BIO_new_file(filePath.c_str(), "rb"); @@ -485,8 +529,186 @@ static int alpnSelectProtoCallback( return SSL_TLSEXT_ERR_OK; } +// Drains and logs the OpenSSL error queue. Provider/store failures (e.g. a +// TPM TCTI or ReadPublic error) only surface here, so logging the queue turns +// an opaque failure into an actionable message. +void logOpenSSLErrors(std::string_view context) +{ + unsigned long errCode = 0; + while ((errCode = ERR_get_error()) != 0) + { + std::array buf{}; + ERR_error_string_n(errCode, buf.data(), buf.size()); + BMCWEB_LOG_ERROR("{}: {}", context, buf.data()); + } +} + +// Loads a private key from a URI via the OpenSSL OSSL_STORE API. The URI may +// be filesystem backed (file://) or provider backed (e.g. a TPM handle:). For +// a provider the returned key is a non-exportable reference that never leaves +// the hardware. +static EVP_PKEY* loadEvpKeyFromUri(const std::string& uri) +{ + std::unique_ptr store( + OSSL_STORE_open_ex(uri.c_str(), nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr), + &OSSL_STORE_close); + if (!store) + { + BMCWEB_LOG_CRITICAL("Failed to open private key store URI: {}", uri); + logOpenSSLErrors("OSSL_STORE_open_ex"); + return nullptr; + } + + while (OSSL_STORE_eof(store.get()) == 0) + { + std::unique_ptr info( + OSSL_STORE_load(store.get()), &OSSL_STORE_INFO_free); + if (!info) + { + // Stop on a load error/empty item rather than spinning. + logOpenSSLErrors("OSSL_STORE_load"); + break; + } + if (OSSL_STORE_INFO_get_type(info.get()) != OSSL_STORE_INFO_PKEY) + { + continue; + } + EVP_PKEY* pkey = OSSL_STORE_INFO_get1_PKEY(info.get()); + if (pkey == nullptr) + { + BMCWEB_LOG_CRITICAL("Failed to read private key from URI: {}", uri); + logOpenSSLErrors("OSSL_STORE_INFO_get1_PKEY"); + return nullptr; + } + return pkey; + } + + BMCWEB_LOG_CRITICAL("No private key found at URI: {}", uri); + return nullptr; +} + +std::optional fileUriToPath(std::string_view uri) +{ + constexpr std::string_view fileScheme = "file://"; + if (uri.starts_with(fileScheme)) + { + std::string_view path = uri.substr(fileScheme.size()); + // Require an absolute path (file:///path). In the two-slash form + // (file://etc/...) the first component is a URI authority, not part of + // the path, and treating it as a relative path would silently resolve + // against the working directory. + if (!path.starts_with('/')) + { + return std::nullopt; + } + return std::string(path); + } + // Backwards compatibility: a bare absolute path (no scheme) is a filesystem + // path. + if (uri.starts_with('/')) + { + return std::string(uri); + } + // Certificates stay on the filesystem, so only file:// is supported here. + // Provider-backed schemes (e.g. a TPM handle:) are rejected. + return std::nullopt; +} + +bool loadPrivateKeyUriIntoContext(boost::asio::ssl::context& sslCtx, + std::string_view uri) +{ + BMCWEB_LOG_INFO("Loading private key from URI: {}", uri); + std::unique_ptr key( + loadEvpKeyFromUri(std::string(uri)), &EVP_PKEY_free); + if (!key) + { + return false; + } + if (SSL_CTX_use_PrivateKey(sslCtx.native_handle(), key.get()) != 1) + { + BMCWEB_LOG_CRITICAL( + "Failed to install private key into SSL context from URI: {}", uri); + logOpenSSLErrors("SSL_CTX_use_PrivateKey"); + return false; + } + return true; +} + +bool isProviderCert(std::string_view location) +{ + // A TPM NV index is exposed to OpenSSL as a "handle:" OSSL_STORE URI, the + // same scheme used for keys; the provider returns a CERT for an NV index + // and a PKEY for a persistent key handle. file:// URIs and bare paths are + // filesystem certificates handled by fileUriToPath. + return location.starts_with("handle:"); +} + +std::optional loadCertPemFromUri(const std::string& uri) +{ + // Cert counterpart of loadEvpKeyFromUri: open the provider store URI + // (default library context + no property query, so the globally configured + // tpm2 provider resolves it) and pull the certificate object out as PEM. + std::unique_ptr store( + OSSL_STORE_open_ex(uri.c_str(), nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr), + &OSSL_STORE_close); + if (!store) + { + BMCWEB_LOG_CRITICAL("Failed to open certificate store URI: {}", uri); + logOpenSSLErrors("OSSL_STORE_open_ex"); + return std::nullopt; + } + + while (OSSL_STORE_eof(store.get()) == 0) + { + std::unique_ptr info( + OSSL_STORE_load(store.get()), &OSSL_STORE_INFO_free); + if (!info) + { + // Stop on a load error/empty item rather than spinning. + logOpenSSLErrors("OSSL_STORE_load"); + break; + } + if (OSSL_STORE_INFO_get_type(info.get()) != OSSL_STORE_INFO_CERT) + { + continue; + } + // get1_CERT returns an owned reference; free it after serializing. + std::unique_ptr cert( + OSSL_STORE_INFO_get1_CERT(info.get()), &X509_free); + if (!cert) + { + BMCWEB_LOG_CRITICAL("Failed to read certificate from URI: {}", uri); + logOpenSSLErrors("OSSL_STORE_INFO_get1_CERT"); + return std::nullopt; + } + std::unique_ptr bufio(BIO_new(BIO_s_mem()), + &BIO_free); + if (!bufio || PEM_write_bio_X509(bufio.get(), cert.get()) == 0) + { + BMCWEB_LOG_CRITICAL("Failed to serialize certificate from URI: {}", + uri); + logOpenSSLErrors("PEM_write_bio_X509"); + return std::nullopt; + } + char* data = nullptr; + long len = BIO_get_mem_data(bufio.get(), &data); + if (len <= 0 || data == nullptr) + { + BMCWEB_LOG_CRITICAL("Empty certificate PEM from URI: {}", uri); + return std::nullopt; + } + return std::string(data, static_cast(len)); + } + + BMCWEB_LOG_CRITICAL("No certificate found at URI: {}", uri); + return std::nullopt; +} + static bool getSslContext(boost::asio::ssl::context& mSslContext, - const std::string& sslPemFile) + const std::string& sslPemFile, + std::optional keyUri = std::nullopt) { mSslContext.set_options( boost::asio::ssl::context::default_workarounds | @@ -509,13 +731,39 @@ static bool getSslContext(boost::asio::ssl::context& mSslContext, { return false; } - mSslContext.use_private_key(buf, boost::asio::ssl::context::pem, ec); - if (ec) + if (keyUri) { - BMCWEB_LOG_CRITICAL("Failed to open ssl pkey"); - return false; + // Load the private key from the configured URI via OSSL_STORE. + // This is the path that carries a TPM (handle:) key; the key + // material never leaves the provider. + if (!loadPrivateKeyUriIntoContext(mSslContext, *keyUri)) + { + return false; + } + } + else + { + // Default: the private key is in the certificate PEM buffer. + mSslContext.use_private_key(buf, boost::asio::ssl::context::pem, + ec); + if (ec) + { + BMCWEB_LOG_CRITICAL("Failed to open ssl pkey"); + return false; + } } } + else if (keyUri) + { + // A key URI is configured but no usable certificate was found. An + // empty certificate is tolerated when the key lives alongside it (the + // legacy "no TLS material yet" case), but with an external key there is + // no certificate that could match it, so fail rather than bring up a + // certificate-less TLS context that fails every handshake. + BMCWEB_LOG_CRITICAL("No certificate available to pair with key URI {}", + *keyUri); + return false; + } // Set up EC curves to auto (boost asio doesn't have a method for this) // There is a pull request to add this. Once this is included in an asio @@ -600,9 +848,59 @@ std::optional getSSLClientContext( // NOTE, this path is temporary; In the future it will need to change to // be set per subscription. Do not rely on this. fs::path certPath = "/etc/ssl/certs/https/client.pem"; - std::string cert = verifyOpensslKeyCert(certPath); - if (!getSslContext(sslCtx, cert)) + // When a client-key URI is configured the private key lives in a provider + // (e.g. a TPM handle) and is loaded via OSSL_STORE; the certificate is then + // loaded on its own (from a provider or the filesystem). Otherwise the cert + // and key are read together from the combined PEM, preserving old behavior. + std::optional keyUri; + if constexpr (!BMCWEB_URI_KEY.empty()) + { + keyUri = BMCWEB_URI_KEY; + } + + std::string cert; + if (isProviderCert(BMCWEB_URI_CERT)) + { + // Certificate lives in a provider (e.g. a TPM NV index); load via + // OSSL_STORE as PEM and feed the same use_certificate path as a file. + BMCWEB_LOG_INFO("Loading certificate from URI: {}", BMCWEB_URI_CERT); + std::optional pem = + loadCertPemFromUri(std::string(BMCWEB_URI_CERT)); + if (!pem) + { + BMCWEB_LOG_ERROR("Failed to load client certificate from URI: {}", + BMCWEB_URI_CERT); + return std::nullopt; + } + cert = std::move(*pem); + } + else + { + // Filesystem cert: uri-cert (file:// or a bare path) overrides the + // default path when configured; empty keeps the default above. + if constexpr (!BMCWEB_URI_CERT.empty()) + { + std::optional resolved = + fileUriToPath(BMCWEB_URI_CERT); + if (resolved) + { + certPath = *resolved; + } + else + { + BMCWEB_LOG_ERROR( + "Unsupported uri-cert {} (file:// or handle: only); using default {}", + BMCWEB_URI_CERT, certPath.string()); + } + } + // With an external key only the cert is on the filesystem; otherwise + // the combined PEM carries cert and key together. + cert = keyUri ? readCertOnlyFile(certPath) + : verifyOpensslKeyCert(certPath); + } + + if (!getSslContext(sslCtx, cert, keyUri)) { return std::nullopt; } diff --git a/src/webserver_run.cpp b/src/webserver_run.cpp index e8eb8ebe4..dfc95ef29 100644 --- a/src/webserver_run.cpp +++ b/src/webserver_run.cpp @@ -37,6 +37,7 @@ #include #include +#include #include #include @@ -166,12 +167,48 @@ int run() bmcweb::registerUserRemovedSignal(); bmcweb::ServiceWatchdog watchdog; + // mTLS server key location: when uri-key is configured the key lives in a + // provider (e.g. a TPM handle:) and is loaded via OSSL_STORE; otherwise + // fall back to the filesystem PEM. + std::string mtlsServerKey = "/etc/ssl/private/server_pkey.pem"; + if constexpr (!BMCWEB_URI_KEY.empty()) + { + mtlsServerKey = BMCWEB_URI_KEY; + } + // mTLS server cert location: uri-cert overrides the default path when + // configured. A provider URI (e.g. a TPM NV "handle:") is passed + // through verbatim so the SNI factory loads it via OSSL_STORE; a + // file:// URI is resolved to a filesystem path. + std::string mtlsServerCert = "/etc/ssl/certs/https/server_cert.pem"; + if constexpr (!BMCWEB_URI_CERT.empty()) + { + if (ensuressl::isProviderCert(BMCWEB_URI_CERT)) + { + mtlsServerCert = BMCWEB_URI_CERT; + } + else + { + std::optional resolved = + ensuressl::fileUriToPath(BMCWEB_URI_CERT); + if (resolved) + { + mtlsServerCert = *resolved; + } + else + { + BMCWEB_LOG_ERROR( + "Unsupported uri-cert {} (file:// or handle: only); using default {}", + BMCWEB_URI_CERT, mtlsServerCert); + } + } + } bmcweb::SniContextFactoryState state( [](const std::string& sniname) { return sniname.starts_with("9.6.28.10"); }, - "/etc/ssl/certs/https/server_cert.pem", - "/etc/ssl/private/server_pkey.pem", "/etc/ssl/certs/authority"); + mtlsServerCert, + mtlsServerKey, + "/etc/ssl/certs/authority"); app.run(state); systemBus->request_name("xyz.openbmc_project.bmcweb"); diff --git a/test/include/ssl_key_handler_test.cpp b/test/include/ssl_key_handler_test.cpp index 44da0383d..fac2bff77 100644 --- a/test/include/ssl_key_handler_test.cpp +++ b/test/include/ssl_key_handler_test.cpp @@ -3,6 +3,8 @@ #include "file_test_utilities.hpp" #include "ssl_key_handler.hpp" +#include + #include #include @@ -26,4 +28,42 @@ TEST(SSLKeyHandler, GenerateVerifyRoundTrip) EXPECT_EQ(cert, cert2); } +TEST(SSLKeyHandler, LoadPrivateKeyFromFileUri) +{ + // Exercises the OSSL_STORE-based key loading path with a file:// URI (the + // same path a TPM handle: URI takes, minus the provider). A generated PEM + // holds the private key; loading it via the URI must install into the + // context. + std::string cert = generateSslCertificate("TestCommonName"); + ASSERT_FALSE(cert.empty()); + TemporaryFileHandle keyFile(cert); + + boost::asio::ssl::context ctx(boost::asio::ssl::context::tls_server); + EXPECT_TRUE( + loadPrivateKeyUriIntoContext(ctx, "file://" + keyFile.stringPath)); +} + +TEST(SSLKeyHandler, LoadPrivateKeyFromMissingUriFails) +{ + boost::asio::ssl::context ctx(boost::asio::ssl::context::tls_server); + EXPECT_FALSE(loadPrivateKeyUriIntoContext( + ctx, "file:///tmp/bmcweb/does-not-exist-key.pem")); +} + +TEST(SSLKeyHandler, FileUriToPath) +{ + // Absolute file:// URI -> filesystem path. + EXPECT_EQ(fileUriToPath("file:///etc/ssl/certs/https/server.pem"), + "/etc/ssl/certs/https/server.pem"); + // Bare absolute path (no scheme) is accepted for backwards compatibility. + EXPECT_EQ(fileUriToPath("/etc/ssl/certs/https/server.pem"), + "/etc/ssl/certs/https/server.pem"); + // Two-slash form is malformed (the path is not absolute) -> rejected. + EXPECT_EQ(fileUriToPath("file://etc/ssl/certs/https/server.pem"), + std::nullopt); + EXPECT_EQ(fileUriToPath("file://"), std::nullopt); + // Provider-backed schemes are not filesystem paths -> rejected. + EXPECT_EQ(fileUriToPath("handle:0x81000000"), std::nullopt); +} + } // namespace ensuressl