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