xref: /openbmc/bmcweb/http/mutual_tls.cpp (revision 3577e44683a5ade8ad02a6418984b56f4ca2bcac)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #include "mutual_tls.hpp"
4 
5 #include "identity.hpp"
6 #include "mutual_tls_private.hpp"
7 #include "sessions.hpp"
8 
9 #include <bit>
10 #include <cstddef>
11 #include <cstdint>
12 #include <optional>
13 #include <string>
14 
15 extern "C"
16 {
17 #include <openssl/asn1.h>
18 #include <openssl/obj_mac.h>
19 #include <openssl/objects.h>
20 #include <openssl/types.h>
21 #include <openssl/x509.h>
22 #include <openssl/x509_vfy.h>
23 #include <openssl/x509v3.h>
24 }
25 
26 #include "logging.hpp"
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 
getCommonNameFromCert(X509 * cert)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 
isUPNMatch(std::string_view upn,std::string_view hostname)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 
getUPNFromCert(X509 * peerCert,std::string_view hostname)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 
getUsernameFromCert(X509 * cert)145 std::string getUsernameFromCert(X509* cert)
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:
153         {
154             // Not yet supported
155             return "";
156         }
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         }
167         case persistent_data::MTLSCommonNameParseMode::CommonName:
168         {
169             return getCommonNameFromCert(cert);
170         }
171         default:
172         {
173             return "";
174         }
175     }
176 }
177 
verifyMtlsUser(const boost::asio::ip::address & clientIp,boost::asio::ssl::verify_context & ctx)178 std::shared_ptr<persistent_data::UserSession> verifyMtlsUser(
179     const boost::asio::ip::address& clientIp,
180     boost::asio::ssl::verify_context& ctx)
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 
233     std::string sslUser = getUsernameFromCert(peerCert);
234     if (sslUser.empty())
235     {
236         BMCWEB_LOG_WARNING("Failed to get user from peer certificate");
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 }
245