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/test/http/mutual_tls.cpp b/test/http/mutual_tls.cpp
index e77315a..e89a3cf 100644
--- a/test/http/mutual_tls.cpp
+++ b/test/http/mutual_tls.cpp
@@ -2,14 +2,19 @@
// SPDX-FileCopyrightText: Copyright OpenBMC Authors
#include "mutual_tls.hpp"
+#include "mutual_tls_private.hpp"
#include "sessions.hpp"
+#include <cstring>
+#include <string>
+
extern "C"
{
#include <openssl/asn1.h>
#include <openssl/ec.h>
#include <openssl/evp.h>
#include <openssl/obj_mac.h>
+#include <openssl/objects.h>
#include <openssl/types.h>
#include <openssl/x509.h>
#include <openssl/x509_vfy.h>
@@ -170,4 +175,169 @@
verifyMtlsUser(ip, ctx);
ASSERT_THAT(session, IsNull());
}
+
+TEST(GetCommonNameFromCert, EmptyCommonName)
+{
+ OSSLX509 x509;
+ std::string commonName = getCommonNameFromCert(x509.get());
+ EXPECT_THAT(commonName, "");
+}
+
+TEST(GetCommonNameFromCert, ValidCommonName)
+{
+ OSSLX509 x509;
+ x509.setSubjectName();
+ std::string commonName = getCommonNameFromCert(x509.get());
+ EXPECT_THAT(commonName, "user");
+}
+
+TEST(GetUPNFromCert, EmptySubjectAlternativeName)
+{
+ OSSLX509 x509;
+ std::string upn = getUPNFromCert(x509.get(), "");
+ EXPECT_THAT(upn, "");
+}
+
+TEST(GetUPNFromCert, NonOthernameSubjectAlternativeName)
+{
+ OSSLX509 x509;
+
+ ASN1_IA5STRING* ia5 = ASN1_IA5STRING_new();
+ ASSERT_THAT(ia5, NotNull());
+
+ const char* user = "user@domain.com";
+ ASSERT_NE(ASN1_STRING_set(ia5, user, static_cast<int>(strlen(user))), 0);
+
+ GENERAL_NAMES* gens = sk_GENERAL_NAME_new_null();
+ ASSERT_THAT(gens, NotNull());
+
+ GENERAL_NAME* gen = GENERAL_NAME_new();
+ ASSERT_THAT(gen, NotNull());
+
+ GENERAL_NAME_set0_value(gen, GEN_EMAIL, ia5);
+ ASSERT_EQ(sk_GENERAL_NAME_push(gens, gen), 1);
+
+ ASSERT_EQ(X509_add1_ext_i2d(x509.get(), NID_subject_alt_name, gens, 0, 0),
+ 1);
+
+ std::string upn = getUPNFromCert(x509.get(), "hostname.domain.com");
+ EXPECT_THAT(upn, "");
+
+ GENERAL_NAME_free(gen);
+ sk_GENERAL_NAME_free(gens);
+}
+
+TEST(GetUPNFromCert, NonUPNSubjectAlternativeName)
+{
+ OSSLX509 x509;
+
+ GENERAL_NAMES* gens = sk_GENERAL_NAME_new_null();
+ ASSERT_THAT(gens, NotNull());
+
+ GENERAL_NAME* gen = GENERAL_NAME_new();
+ ASSERT_THAT(gen, NotNull());
+
+ ASN1_OBJECT* othType = OBJ_nid2obj(NID_SRVName);
+
+ ASN1_TYPE* value = ASN1_TYPE_new();
+ ASSERT_THAT(value, NotNull());
+ value->type = V_ASN1_UTF8STRING;
+
+ // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access)
+ value->value.utf8string = ASN1_UTF8STRING_new();
+ ASSERT_THAT(value->value.utf8string, NotNull());
+ const char* user = "user@domain.com";
+ ASN1_STRING_set(value->value.utf8string, user,
+ static_cast<int>(strlen(user)));
+ // NOLINTEND(cppcoreguidelines-pro-type-union-access)
+
+ ASSERT_EQ(GENERAL_NAME_set0_othername(gen, othType, value), 1);
+ ASSERT_EQ(sk_GENERAL_NAME_push(gens, gen), 1);
+ ASSERT_EQ(X509_add1_ext_i2d(x509.get(), NID_subject_alt_name, gens, 0, 0),
+ 1);
+
+ std::string upn = getUPNFromCert(x509.get(), "hostname.domain.com");
+ EXPECT_THAT(upn, "");
+
+ sk_GENERAL_NAME_pop_free(gens, GENERAL_NAME_free);
+}
+
+TEST(GetUPNFromCert, NonUTF8UPNSubjectAlternativeName)
+{
+ OSSLX509 x509;
+
+ GENERAL_NAMES* gens = sk_GENERAL_NAME_new_null();
+ ASSERT_THAT(gens, NotNull());
+
+ GENERAL_NAME* gen = GENERAL_NAME_new();
+ ASSERT_THAT(gen, NotNull());
+
+ ASN1_OBJECT* othType = OBJ_nid2obj(NID_ms_upn);
+
+ ASN1_TYPE* value = ASN1_TYPE_new();
+ ASSERT_THAT(value, NotNull());
+ value->type = V_ASN1_OCTET_STRING;
+
+ // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access)
+ value->value.octet_string = ASN1_OCTET_STRING_new();
+ ASSERT_THAT(value->value.octet_string, NotNull());
+ const char* user = "0123456789";
+ ASN1_STRING_set(value->value.octet_string, user,
+ static_cast<int>(strlen(user)));
+ // NOLINTEND(cppcoreguidelines-pro-type-union-access)
+
+ ASSERT_EQ(GENERAL_NAME_set0_othername(gen, othType, value), 1);
+ ASSERT_EQ(sk_GENERAL_NAME_push(gens, gen), 1);
+ ASSERT_EQ(X509_add1_ext_i2d(x509.get(), NID_subject_alt_name, gens, 0, 0),
+ 1);
+
+ std::string upn = getUPNFromCert(x509.get(), "hostname.domain.com");
+ EXPECT_THAT(upn, "");
+
+ sk_GENERAL_NAME_pop_free(gens, GENERAL_NAME_free);
+}
+
+TEST(GetUPNFromCert, ValidUPN)
+{
+ OSSLX509 x509;
+
+ GENERAL_NAMES* gens = sk_GENERAL_NAME_new_null();
+ ASSERT_THAT(gens, NotNull());
+
+ GENERAL_NAME* gen = GENERAL_NAME_new();
+ ASSERT_THAT(gen, NotNull());
+
+ ASN1_OBJECT* othType = OBJ_nid2obj(NID_ms_upn);
+
+ ASN1_TYPE* value = ASN1_TYPE_new();
+ ASSERT_THAT(value, NotNull());
+ value->type = V_ASN1_UTF8STRING;
+
+ // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access)
+ value->value.utf8string = ASN1_UTF8STRING_new();
+ ASSERT_THAT(value->value.utf8string, NotNull());
+ const char* user = "user@domain.com";
+ ASN1_STRING_set(value->value.utf8string, user,
+ static_cast<int>(strlen(user)));
+ // NOLINTEND(cppcoreguidelines-pro-type-union-access)
+
+ ASSERT_EQ(GENERAL_NAME_set0_othername(gen, othType, value), 1);
+ ASSERT_EQ(sk_GENERAL_NAME_push(gens, gen), 1);
+ ASSERT_EQ(X509_add1_ext_i2d(x509.get(), NID_subject_alt_name, gens, 0, 0),
+ 1);
+
+ std::string upn = getUPNFromCert(x509.get(), "hostname.domain.com");
+ EXPECT_THAT(upn, "user");
+
+ sk_GENERAL_NAME_pop_free(gens, GENERAL_NAME_free);
+}
+
+TEST(IsUPNMatch, MultipleCases)
+{
+ EXPECT_FALSE(isUPNMatch("user", "hostname.domain.com"));
+ EXPECT_TRUE(isUPNMatch("user@domain.com", "hostname.domain.com"));
+ EXPECT_FALSE(isUPNMatch("user@domain.com", "hostname.domain.org"));
+ EXPECT_FALSE(isUPNMatch("user@region.com", "hostname.domain.com"));
+ EXPECT_TRUE(isUPNMatch("user@com", "hostname.region.domain.com"));
+}
} // namespace