xref: /openbmc/bmcweb/http/mutual_tls.cpp (revision 4d7b5ddb3a2b6cc42b7bbc0c710f297e6df4fd55)
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