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