1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #include "mutual_tls.hpp" 4 5 #include "bmcweb_config.h" 6 7 #include "identity.hpp" 8 #include "mutual_tls_private.hpp" 9 #include "sessions.hpp" 10 11 #include <bit> 12 #include <cstddef> 13 #include <cstdint> 14 #include <optional> 15 #include <string> 16 17 extern "C" 18 { 19 #include <openssl/asn1.h> 20 #include <openssl/obj_mac.h> 21 #include <openssl/objects.h> 22 #include <openssl/types.h> 23 #include <openssl/x509.h> 24 #include <openssl/x509_vfy.h> 25 #include <openssl/x509v3.h> 26 } 27 28 #include "logging.hpp" 29 #include "mutual_tls_meta.hpp" 30 31 #include <boost/asio/ip/address.hpp> 32 #include <boost/asio/ssl/verify_context.hpp> 33 34 #include <memory> 35 #include <string_view> 36 37 std::string getCommonNameFromCert(X509* cert) 38 { 39 std::string commonName; 40 // Extract username contained in CommonName 41 commonName.resize(256, '\0'); 42 int length = X509_NAME_get_text_by_NID( 43 X509_get_subject_name(cert), NID_commonName, commonName.data(), 44 static_cast<int>(commonName.size())); 45 if (length <= 0) 46 { 47 BMCWEB_LOG_DEBUG("TLS cannot get common name to create session"); 48 return ""; 49 } 50 commonName.resize(static_cast<size_t>(length)); 51 return commonName; 52 } 53 54 bool isUPNMatch(std::string_view upn, std::string_view hostname) 55 { 56 // UPN format: <username>@<domain> (e.g. user@domain.com) 57 // https://learn.microsoft.com/en-us/windows/win32/ad/naming-properties#userprincipalname 58 size_t upnDomainPos = upn.find('@'); 59 if (upnDomainPos == std::string_view::npos) 60 { 61 return false; 62 } 63 64 // The hostname should match the domain part of the UPN 65 std::string_view upnDomain = upn.substr(upnDomainPos + 1); 66 while (true) 67 { 68 std::string_view upnDomainMatching = upnDomain; 69 size_t dotUPNPos = upnDomain.find_last_of('.'); 70 if (dotUPNPos != std::string_view::npos) 71 { 72 upnDomainMatching = upnDomain.substr(dotUPNPos + 1); 73 } 74 75 std::string_view hostDomainMatching = hostname; 76 size_t dotHostPos = hostname.find_last_of('.'); 77 if (dotHostPos != std::string_view::npos) 78 { 79 hostDomainMatching = hostname.substr(dotHostPos + 1); 80 } 81 82 if (upnDomainMatching != hostDomainMatching) 83 { 84 return false; 85 } 86 87 if (dotUPNPos == std::string_view::npos) 88 { 89 return true; 90 } 91 92 upnDomain = upnDomain.substr(0, dotUPNPos); 93 hostname = hostname.substr(0, dotHostPos); 94 } 95 } 96 97 std::string getUPNFromCert(X509* peerCert, std::string_view hostname) 98 { 99 GENERAL_NAMES* gs = static_cast<GENERAL_NAMES*>( 100 X509_get_ext_d2i(peerCert, NID_subject_alt_name, nullptr, nullptr)); 101 if (gs == nullptr) 102 { 103 return ""; 104 } 105 106 std::string ret; 107 for (int i = 0; i < sk_GENERAL_NAME_num(gs); i++) 108 { 109 GENERAL_NAME* g = sk_GENERAL_NAME_value(gs, i); 110 if (g->type != GEN_OTHERNAME) 111 { 112 continue; 113 } 114 115 // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access) 116 int nid = OBJ_obj2nid(g->d.otherName->type_id); 117 if (nid != NID_ms_upn) 118 { 119 continue; 120 } 121 122 int type = g->d.otherName->value->type; 123 if (type != V_ASN1_UTF8STRING) 124 { 125 continue; 126 } 127 128 char* upnChar = 129 std::bit_cast<char*>(g->d.otherName->value->value.utf8string->data); 130 unsigned int upnLen = static_cast<unsigned int>( 131 g->d.otherName->value->value.utf8string->length); 132 // NOLINTEND(cppcoreguidelines-pro-type-union-access) 133 134 std::string upn = std::string(upnChar, upnLen); 135 if (!isUPNMatch(upn, hostname)) 136 { 137 continue; 138 } 139 140 size_t upnDomainPos = upn.find('@'); 141 ret = upn.substr(0, upnDomainPos); 142 break; 143 } 144 GENERAL_NAMES_free(gs); 145 return ret; 146 } 147 148 std::string getMetaUserNameFromCert(X509* cert) 149 { 150 // Meta Inc. CommonName parsing 151 std::optional<std::string_view> sslUserMeta = 152 mtlsMetaParseSslUser(getCommonNameFromCert(cert)); 153 if (!sslUserMeta) 154 { 155 return ""; 156 } 157 return std::string{*sslUserMeta}; 158 } 159 160 std::string getUsernameFromCert(X509* cert) 161 { 162 const persistent_data::AuthConfigMethods& authMethodsConfig = 163 persistent_data::SessionStore::getInstance().getAuthMethodsConfig(); 164 switch (authMethodsConfig.mTLSCommonNameParsingMode) 165 { 166 case persistent_data::MTLSCommonNameParseMode::Invalid: 167 case persistent_data::MTLSCommonNameParseMode::Whole: 168 { 169 // Not yet supported 170 return ""; 171 } 172 case persistent_data::MTLSCommonNameParseMode::UserPrincipalName: 173 { 174 std::string hostname = getHostName(); 175 if (hostname.empty()) 176 { 177 BMCWEB_LOG_WARNING("Failed to get hostname"); 178 return ""; 179 } 180 return getUPNFromCert(cert, hostname); 181 } 182 case persistent_data::MTLSCommonNameParseMode::CommonName: 183 { 184 return getCommonNameFromCert(cert); 185 } 186 case persistent_data::MTLSCommonNameParseMode::Meta: 187 { 188 if constexpr (BMCWEB_META_TLS_COMMON_NAME_PARSING) 189 { 190 return getMetaUserNameFromCert(cert); 191 } 192 } 193 default: 194 { 195 return ""; 196 } 197 } 198 } 199 200 std::shared_ptr<persistent_data::UserSession> verifyMtlsUser( 201 const boost::asio::ip::address& clientIp, 202 boost::asio::ssl::verify_context& ctx) 203 { 204 // do nothing if TLS is disabled 205 if (!persistent_data::SessionStore::getInstance() 206 .getAuthMethodsConfig() 207 .tls) 208 { 209 BMCWEB_LOG_DEBUG("TLS auth_config is disabled"); 210 return nullptr; 211 } 212 213 X509_STORE_CTX* cts = ctx.native_handle(); 214 if (cts == nullptr) 215 { 216 BMCWEB_LOG_DEBUG("Cannot get native TLS handle."); 217 return nullptr; 218 } 219 220 // Get certificate 221 X509* peerCert = X509_STORE_CTX_get_current_cert(ctx.native_handle()); 222 if (peerCert == nullptr) 223 { 224 BMCWEB_LOG_DEBUG("Cannot get current TLS certificate."); 225 return nullptr; 226 } 227 228 // Check if certificate is OK 229 int ctxError = X509_STORE_CTX_get_error(cts); 230 if (ctxError != X509_V_OK) 231 { 232 BMCWEB_LOG_INFO("Last TLS error is: {}", ctxError); 233 return nullptr; 234 } 235 236 // Check that we have reached final certificate in chain 237 int32_t depth = X509_STORE_CTX_get_error_depth(cts); 238 if (depth != 0) 239 { 240 BMCWEB_LOG_DEBUG( 241 "Certificate verification in progress (depth {}), waiting to reach final depth", 242 depth); 243 return nullptr; 244 } 245 246 BMCWEB_LOG_DEBUG("Certificate verification of final depth"); 247 248 if (X509_check_purpose(peerCert, X509_PURPOSE_SSL_CLIENT, 0) != 1) 249 { 250 BMCWEB_LOG_DEBUG( 251 "Chain does not allow certificate to be used for SSL client authentication"); 252 return nullptr; 253 } 254 255 std::string sslUser = getUsernameFromCert(peerCert); 256 if (sslUser.empty()) 257 { 258 BMCWEB_LOG_WARNING("Failed to get user from peer certificate"); 259 return nullptr; 260 } 261 262 std::string unsupportedClientId; 263 return persistent_data::SessionStore::getInstance().generateUserSession( 264 sslUser, clientIp, unsupportedClientId, 265 persistent_data::SessionType::MutualTLS); 266 } 267