blob: 209c929928597cc1471ee5d1879049faf967551b [file] [log] [blame]
// 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);
}