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