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