| // SPDX-License-Identifier: Apache-2.0 |
| // SPDX-FileCopyrightText: Copyright OpenBMC Authors |
| #include "mutual_tls.hpp" |
| |
| #include "bmcweb_config.h" |
| |
| #include "identity.hpp" |
| #include "mutual_tls_private.hpp" |
| #include "sessions.hpp" |
| |
| #include <bit> |
| #include <cstddef> |
| #include <cstdint> |
| #include <optional> |
| #include <string> |
| |
| extern "C" |
| { |
| #include <openssl/asn1.h> |
| #include <openssl/obj_mac.h> |
| #include <openssl/objects.h> |
| #include <openssl/types.h> |
| #include <openssl/x509.h> |
| #include <openssl/x509_vfy.h> |
| #include <openssl/x509v3.h> |
| } |
| |
| #include "logging.hpp" |
| #include "mutual_tls_meta.hpp" |
| |
| #include <boost/asio/ip/address.hpp> |
| #include <boost/asio/ssl/verify_context.hpp> |
| |
| #include <memory> |
| #include <string_view> |
| |
| std::string getCommonNameFromCert(X509* cert) |
| { |
| std::string commonName; |
| // Extract username contained in CommonName |
| commonName.resize(256, '\0'); |
| int length = X509_NAME_get_text_by_NID( |
| X509_get_subject_name(cert), NID_commonName, commonName.data(), |
| static_cast<int>(commonName.size())); |
| if (length <= 0) |
| { |
| BMCWEB_LOG_DEBUG("TLS cannot get common name to create session"); |
| return ""; |
| } |
| commonName.resize(static_cast<size_t>(length)); |
| return commonName; |
| } |
| |
| bool isUPNMatch(std::string_view upn, std::string_view hostname) |
| { |
| // UPN format: <username>@<domain> (e.g. user@domain.com) |
| // https://learn.microsoft.com/en-us/windows/win32/ad/naming-properties#userprincipalname |
| size_t upnDomainPos = upn.find('@'); |
| if (upnDomainPos == std::string_view::npos) |
| { |
| return false; |
| } |
| |
| // The hostname should match the domain part of the UPN |
| std::string_view upnDomain = upn.substr(upnDomainPos + 1); |
| while (true) |
| { |
| std::string_view upnDomainMatching = upnDomain; |
| size_t dotUPNPos = upnDomain.find_last_of('.'); |
| if (dotUPNPos != std::string_view::npos) |
| { |
| upnDomainMatching = upnDomain.substr(dotUPNPos + 1); |
| } |
| |
| std::string_view hostDomainMatching = hostname; |
| size_t dotHostPos = hostname.find_last_of('.'); |
| if (dotHostPos != std::string_view::npos) |
| { |
| hostDomainMatching = hostname.substr(dotHostPos + 1); |
| } |
| |
| if (upnDomainMatching != hostDomainMatching) |
| { |
| return false; |
| } |
| |
| if (dotUPNPos == std::string_view::npos) |
| { |
| return true; |
| } |
| |
| upnDomain = upnDomain.substr(0, dotUPNPos); |
| hostname = hostname.substr(0, dotHostPos); |
| } |
| } |
| |
| std::string getUPNFromCert(X509* peerCert, std::string_view hostname) |
| { |
| GENERAL_NAMES* gs = static_cast<GENERAL_NAMES*>( |
| X509_get_ext_d2i(peerCert, NID_subject_alt_name, nullptr, nullptr)); |
| if (gs == nullptr) |
| { |
| return ""; |
| } |
| |
| std::string ret; |
| for (int i = 0; i < sk_GENERAL_NAME_num(gs); i++) |
| { |
| GENERAL_NAME* g = sk_GENERAL_NAME_value(gs, i); |
| if (g->type != GEN_OTHERNAME) |
| { |
| continue; |
| } |
| |
| // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access) |
| int nid = OBJ_obj2nid(g->d.otherName->type_id); |
| if (nid != NID_ms_upn) |
| { |
| continue; |
| } |
| |
| int type = g->d.otherName->value->type; |
| if (type != V_ASN1_UTF8STRING) |
| { |
| continue; |
| } |
| |
| char* upnChar = |
| std::bit_cast<char*>(g->d.otherName->value->value.utf8string->data); |
| unsigned int upnLen = static_cast<unsigned int>( |
| g->d.otherName->value->value.utf8string->length); |
| // NOLINTEND(cppcoreguidelines-pro-type-union-access) |
| |
| std::string upn = std::string(upnChar, upnLen); |
| if (!isUPNMatch(upn, hostname)) |
| { |
| continue; |
| } |
| |
| size_t upnDomainPos = upn.find('@'); |
| ret = upn.substr(0, upnDomainPos); |
| break; |
| } |
| GENERAL_NAMES_free(gs); |
| return ret; |
| } |
| |
| std::string getMetaUserNameFromCert(X509* cert) |
| { |
| // Meta Inc. CommonName parsing |
| std::optional<std::string_view> sslUserMeta = |
| mtlsMetaParseSslUser(getCommonNameFromCert(cert)); |
| if (!sslUserMeta) |
| { |
| return ""; |
| } |
| return std::string{*sslUserMeta}; |
| } |
| |
| std::string getUsernameFromCert(X509* cert) |
| { |
| const persistent_data::AuthConfigMethods& authMethodsConfig = |
| persistent_data::SessionStore::getInstance().getAuthMethodsConfig(); |
| switch (authMethodsConfig.mTLSCommonNameParsingMode) |
| { |
| case persistent_data::MTLSCommonNameParseMode::Invalid: |
| case persistent_data::MTLSCommonNameParseMode::Whole: |
| { |
| // Not yet supported |
| return ""; |
| } |
| case persistent_data::MTLSCommonNameParseMode::UserPrincipalName: |
| { |
| std::string hostname = getHostName(); |
| if (hostname.empty()) |
| { |
| BMCWEB_LOG_WARNING("Failed to get hostname"); |
| return ""; |
| } |
| return getUPNFromCert(cert, hostname); |
| } |
| case persistent_data::MTLSCommonNameParseMode::CommonName: |
| { |
| return getCommonNameFromCert(cert); |
| } |
| case persistent_data::MTLSCommonNameParseMode::Meta: |
| { |
| if constexpr (BMCWEB_META_TLS_COMMON_NAME_PARSING) |
| { |
| return getMetaUserNameFromCert(cert); |
| } |
| } |
| default: |
| { |
| return ""; |
| } |
| } |
| } |
| |
| std::shared_ptr<persistent_data::UserSession> verifyMtlsUser( |
| const boost::asio::ip::address& clientIp, |
| boost::asio::ssl::verify_context& ctx) |
| { |
| // do nothing if TLS is disabled |
| if (!persistent_data::SessionStore::getInstance() |
| .getAuthMethodsConfig() |
| .tls) |
| { |
| BMCWEB_LOG_DEBUG("TLS auth_config is disabled"); |
| return nullptr; |
| } |
| |
| X509_STORE_CTX* cts = ctx.native_handle(); |
| if (cts == nullptr) |
| { |
| BMCWEB_LOG_DEBUG("Cannot get native TLS handle."); |
| return nullptr; |
| } |
| |
| // Get certificate |
| X509* peerCert = X509_STORE_CTX_get_current_cert(ctx.native_handle()); |
| if (peerCert == nullptr) |
| { |
| BMCWEB_LOG_DEBUG("Cannot get current TLS certificate."); |
| return nullptr; |
| } |
| |
| // Check if certificate is OK |
| int ctxError = X509_STORE_CTX_get_error(cts); |
| if (ctxError != X509_V_OK) |
| { |
| BMCWEB_LOG_INFO("Last TLS error is: {}", ctxError); |
| return nullptr; |
| } |
| |
| // Check that we have reached final certificate in chain |
| int32_t depth = X509_STORE_CTX_get_error_depth(cts); |
| if (depth != 0) |
| { |
| BMCWEB_LOG_DEBUG( |
| "Certificate verification in progress (depth {}), waiting to reach final depth", |
| depth); |
| return nullptr; |
| } |
| |
| BMCWEB_LOG_DEBUG("Certificate verification of final depth"); |
| |
| if (X509_check_purpose(peerCert, X509_PURPOSE_SSL_CLIENT, 0) != 1) |
| { |
| BMCWEB_LOG_DEBUG( |
| "Chain does not allow certificate to be used for SSL client authentication"); |
| return nullptr; |
| } |
| |
| std::string sslUser = getUsernameFromCert(peerCert); |
| if (sslUser.empty()) |
| { |
| BMCWEB_LOG_WARNING("Failed to get user from peer certificate"); |
| return nullptr; |
| } |
| |
| std::string unsupportedClientId; |
| return persistent_data::SessionStore::getInstance().generateUserSession( |
| sslUser, clientIp, unsupportedClientId, |
| persistent_data::SessionType::MutualTLS); |
| } |