mtls: implement UPN parse mode

This commit is intended to implement the UserPrincipalName (UPN) parse
mode on mutual TLS (MTLS). By implementing this we can use the X509
certificate extension Subject Alternative Name (SAN), specifically UPN
to be used as the username

In our case, this feature is needed because we have a specific format
on our Subject CN of X509 certificate. This format cannot directly
mapped to the username of bmcweb because it contains special
characters (`/` and `:`), which cannot exist in the username.
Changing the format of our Subject CN is very risky. By enabling
this feature we can use other field, which is the SAN extension to
be used as the username and do not change our Subject CN on the
X509 certificate

In general, by implementing this feature, we can enable multiple
options for the system. There might be other cases where we want to
have the username of the bmcweb is not equal to the Subject CN of the
certificate, instead the username is added as the UserPrincipalName
field in the certificate

The format of the UPN is `<username>@<domain>` [1][2]. The format
is similar to email format. The domain name identifies the domain
in which the user is located [3] and it should match the device name's
domain (domain forest).

Tested
- Test using `generate_auth_certificate.py` (extended on patch [4])
- Manual testing (please see the script mentioned above for more detail)
  - Setup certificate with UPN inside SAN extension
  - Change the CertificateMappingAttribute to use UPN
  - Get request to `/SessionService/Sessions`
- Run unit tests

[1] UPN Format: https://learn.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#user-principal-name
[2] UPN Properties: https://learn.microsoft.com/en-us/windows/win32/ad/naming-properties#userprincipalname
[3] UPN Glossary: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/719b890d-62e6-4322-b9b1-1f34d11535b4#gt_9d606f55-b798-4def-bf96-97b878bb92c6
[4] Patch Testing Script: https://gerrit.openbmc.org/c/openbmc/bmcweb/+/78837

Change-Id: I490da8b95aee9579546971e58ab2c4afd64c5997
Signed-off-by: Malik Akbar Hashemi Rafsanjani <malikrafsan@meta.com>
diff --git a/http/mutual_tls.cpp b/http/mutual_tls.cpp
index 0074711..209c929 100644
--- a/http/mutual_tls.cpp
+++ b/http/mutual_tls.cpp
@@ -2,8 +2,13 @@
 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
 #include "mutual_tls.hpp"
 
+#include "bmcweb_config.h"
+
+#include "identity.hpp"
+#include "mutual_tls_private.hpp"
 #include "sessions.hpp"
 
+#include <bit>
 #include <cstddef>
 #include <cstdint>
 #include <optional>
@@ -11,7 +16,9 @@
 
 extern "C"
 {
+#include <openssl/asn1.h>
 #include <openssl/obj_mac.h>
+#include <openssl/objects.h>
 #include <openssl/types.h>
 #include <openssl/x509.h>
 #include <openssl/x509_vfy.h>
@@ -27,7 +34,130 @@
 #include <memory>
 #include <string_view>
 
-std::string getUsernameFromCommonName(std::string_view commonName)
+std::string getCommonNameFromCert(X509* cert)
+{
+    std::string commonName;
+    // Extract username contained in CommonName
+    commonName.resize(256, '\0');
+    int length = X509_NAME_get_text_by_NID(
+        X509_get_subject_name(cert), NID_commonName, commonName.data(),
+        static_cast<int>(commonName.size()));
+    if (length <= 0)
+    {
+        BMCWEB_LOG_DEBUG("TLS cannot get common name to create session");
+        return "";
+    }
+    commonName.resize(static_cast<size_t>(length));
+    return commonName;
+}
+
+bool isUPNMatch(std::string_view upn, std::string_view hostname)
+{
+    // UPN format: <username>@<domain> (e.g. user@domain.com)
+    // https://learn.microsoft.com/en-us/windows/win32/ad/naming-properties#userprincipalname
+    size_t upnDomainPos = upn.find('@');
+    if (upnDomainPos == std::string_view::npos)
+    {
+        return false;
+    }
+
+    // The hostname should match the domain part of the UPN
+    std::string_view upnDomain = upn.substr(upnDomainPos + 1);
+    while (true)
+    {
+        std::string_view upnDomainMatching = upnDomain;
+        size_t dotUPNPos = upnDomain.find_last_of('.');
+        if (dotUPNPos != std::string_view::npos)
+        {
+            upnDomainMatching = upnDomain.substr(dotUPNPos + 1);
+        }
+
+        std::string_view hostDomainMatching = hostname;
+        size_t dotHostPos = hostname.find_last_of('.');
+        if (dotHostPos != std::string_view::npos)
+        {
+            hostDomainMatching = hostname.substr(dotHostPos + 1);
+        }
+
+        if (upnDomainMatching != hostDomainMatching)
+        {
+            return false;
+        }
+
+        if (dotUPNPos == std::string_view::npos)
+        {
+            return true;
+        }
+
+        upnDomain = upnDomain.substr(0, dotUPNPos);
+        hostname = hostname.substr(0, dotHostPos);
+    }
+}
+
+std::string getUPNFromCert(X509* peerCert, std::string_view hostname)
+{
+    GENERAL_NAMES* gs = static_cast<GENERAL_NAMES*>(
+        X509_get_ext_d2i(peerCert, NID_subject_alt_name, nullptr, nullptr));
+    if (gs == nullptr)
+    {
+        return "";
+    }
+
+    std::string ret;
+    for (int i = 0; i < sk_GENERAL_NAME_num(gs); i++)
+    {
+        GENERAL_NAME* g = sk_GENERAL_NAME_value(gs, i);
+        if (g->type != GEN_OTHERNAME)
+        {
+            continue;
+        }
+
+        // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access)
+        int nid = OBJ_obj2nid(g->d.otherName->type_id);
+        if (nid != NID_ms_upn)
+        {
+            continue;
+        }
+
+        int type = g->d.otherName->value->type;
+        if (type != V_ASN1_UTF8STRING)
+        {
+            continue;
+        }
+
+        char* upnChar =
+            std::bit_cast<char*>(g->d.otherName->value->value.utf8string->data);
+        unsigned int upnLen = static_cast<unsigned int>(
+            g->d.otherName->value->value.utf8string->length);
+        // NOLINTEND(cppcoreguidelines-pro-type-union-access)
+
+        std::string upn = std::string(upnChar, upnLen);
+        if (!isUPNMatch(upn, hostname))
+        {
+            continue;
+        }
+
+        size_t upnDomainPos = upn.find('@');
+        ret = upn.substr(0, upnDomainPos);
+        break;
+    }
+    GENERAL_NAMES_free(gs);
+    return ret;
+}
+
+std::string getMetaUserNameFromCert(X509* cert)
+{
+    // Meta Inc. CommonName parsing
+    std::optional<std::string_view> sslUserMeta =
+        mtlsMetaParseSslUser(getCommonNameFromCert(cert));
+    if (!sslUserMeta)
+    {
+        return "";
+    }
+    return std::string{*sslUserMeta};
+}
+
+std::string getUsernameFromCert(X509* cert)
 {
     const persistent_data::AuthConfigMethods& authMethodsConfig =
         persistent_data::SessionStore::getInstance().getAuthMethodsConfig();
@@ -35,25 +165,30 @@
     {
         case persistent_data::MTLSCommonNameParseMode::Invalid:
         case persistent_data::MTLSCommonNameParseMode::Whole:
-        case persistent_data::MTLSCommonNameParseMode::UserPrincipalName:
         {
             // Not yet supported
             return "";
         }
+        case persistent_data::MTLSCommonNameParseMode::UserPrincipalName:
+        {
+            std::string hostname = getHostName();
+            if (hostname.empty())
+            {
+                BMCWEB_LOG_WARNING("Failed to get hostname");
+                return "";
+            }
+            return getUPNFromCert(cert, hostname);
+        }
         case persistent_data::MTLSCommonNameParseMode::CommonName:
         {
-            return std::string{commonName};
+            return getCommonNameFromCert(cert);
         }
         case persistent_data::MTLSCommonNameParseMode::Meta:
         {
-            // Meta Inc. CommonName parsing
-            std::optional<std::string_view> sslUserMeta =
-                mtlsMetaParseSslUser(commonName);
-            if (!sslUserMeta)
+            if constexpr (BMCWEB_META_TLS_COMMON_NAME_PARSING)
             {
-                return "";
+                return getMetaUserNameFromCert(cert);
             }
-            return std::string{*sslUserMeta};
         }
         default:
         {
@@ -117,25 +252,10 @@
         return nullptr;
     }
 
-    std::string commonName;
-    // Extract username contained in CommonName
-    commonName.resize(256, '\0');
-
-    int length = X509_NAME_get_text_by_NID(
-        X509_get_subject_name(peerCert), NID_commonName, commonName.data(),
-        static_cast<int>(commonName.size()));
-    if (length <= 0)
-    {
-        BMCWEB_LOG_DEBUG("TLS cannot get common name to create session");
-        return nullptr;
-    }
-
-    commonName.resize(static_cast<size_t>(length));
-    std::string sslUser = getUsernameFromCommonName(commonName);
+    std::string sslUser = getUsernameFromCert(peerCert);
     if (sslUser.empty())
     {
-        BMCWEB_LOG_WARNING("Failed to get user from common name {}",
-                           commonName);
+        BMCWEB_LOG_WARNING("Failed to get user from peer certificate");
         return nullptr;
     }