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
getCommonNameFromCert(X509 * cert)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
isUPNMatch(std::string_view upn,std::string_view hostname)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
getUPNFromCert(X509 * peerCert,std::string_view hostname)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
getUsernameFromCert(X509 * cert)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
verifyMtlsUser(const boost::asio::ip::address & clientIp,boost::asio::ssl::verify_context & ctx)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