Ed Tanous | 40e9b92 | 2024-09-10 13:50:16 -0700 | [diff] [blame] | 1 | // SPDX-License-Identifier: Apache-2.0 |
| 2 | // SPDX-FileCopyrightText: Copyright OpenBMC Authors |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 3 | #include "mutual_tls.hpp" |
| 4 | |
Malik Akbar Hashemi Rafsanjani | 4d7b5dd | 2025-02-26 13:14:30 -0800 | [diff] [blame] | 5 | #include "mutual_tls_private.hpp" |
Ed Tanous | f0b59af | 2024-03-20 13:38:04 -0700 | [diff] [blame] | 6 | #include "sessions.hpp" |
| 7 | |
Malik Akbar Hashemi Rafsanjani | 4d7b5dd | 2025-02-26 13:14:30 -0800 | [diff] [blame] | 8 | #include <cstring> |
| 9 | #include <string> |
| 10 | |
Ed Tanous | 6dbe9be | 2024-04-14 10:24:20 -0700 | [diff] [blame] | 11 | extern "C" |
| 12 | { |
Ed Tanous | f0b59af | 2024-03-20 13:38:04 -0700 | [diff] [blame] | 13 | #include <openssl/asn1.h> |
| 14 | #include <openssl/ec.h> |
| 15 | #include <openssl/evp.h> |
| 16 | #include <openssl/obj_mac.h> |
Malik Akbar Hashemi Rafsanjani | 4d7b5dd | 2025-02-26 13:14:30 -0800 | [diff] [blame] | 17 | #include <openssl/objects.h> |
Ed Tanous | f0b59af | 2024-03-20 13:38:04 -0700 | [diff] [blame] | 18 | #include <openssl/types.h> |
| 19 | #include <openssl/x509.h> |
| 20 | #include <openssl/x509_vfy.h> |
| 21 | #include <openssl/x509v3.h> |
Ed Tanous | 6dbe9be | 2024-04-14 10:24:20 -0700 | [diff] [blame] | 22 | } |
Ed Tanous | f0b59af | 2024-03-20 13:38:04 -0700 | [diff] [blame] | 23 | |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 24 | #include <boost/asio/ip/address.hpp> |
| 25 | #include <boost/asio/ssl/verify_context.hpp> |
| 26 | |
Ed Tanous | f0b59af | 2024-03-20 13:38:04 -0700 | [diff] [blame] | 27 | #include <array> |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 28 | #include <memory> |
| 29 | |
| 30 | #include <gmock/gmock.h> |
Ed Tanous | 478b7ad | 2024-07-15 19:11:54 -0700 | [diff] [blame] | 31 | #include <gtest/gtest.h> |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 32 | |
| 33 | using ::testing::IsNull; |
| 34 | using ::testing::NotNull; |
| 35 | |
| 36 | namespace |
| 37 | { |
| 38 | class OSSLX509 |
| 39 | { |
| 40 | X509* ptr = X509_new(); |
| 41 | |
| 42 | public: |
| 43 | OSSLX509& operator=(const OSSLX509&) = delete; |
| 44 | OSSLX509& operator=(OSSLX509&&) = delete; |
| 45 | |
| 46 | OSSLX509(const OSSLX509&) = delete; |
| 47 | OSSLX509(OSSLX509&&) = delete; |
| 48 | |
| 49 | OSSLX509() = default; |
Ed Tanous | 23f1c96 | 2023-12-05 15:57:46 -0800 | [diff] [blame] | 50 | |
| 51 | void setSubjectName() |
| 52 | { |
| 53 | X509_NAME* name = X509_get_subject_name(ptr); |
| 54 | std::array<unsigned char, 5> user = {'u', 's', 'e', 'r', '\0'}; |
| 55 | X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, user.data(), -1, |
| 56 | -1, 0); |
| 57 | } |
| 58 | void sign() |
| 59 | { |
| 60 | // Generate test key |
| 61 | EVP_PKEY* pkey = nullptr; |
| 62 | EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr); |
| 63 | ASSERT_EQ(EVP_PKEY_keygen_init(pctx), 1); |
| 64 | ASSERT_EQ( |
| 65 | EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, NID_X9_62_prime256v1), |
| 66 | 1); |
| 67 | ASSERT_EQ(EVP_PKEY_keygen(pctx, &pkey), 1); |
| 68 | EVP_PKEY_CTX_free(pctx); |
| 69 | |
| 70 | // Sign cert with key |
| 71 | ASSERT_EQ(X509_set_pubkey(ptr, pkey), 1); |
| 72 | ASSERT_GT(X509_sign(ptr, pkey, EVP_sha256()), 0); |
| 73 | EVP_PKEY_free(pkey); |
| 74 | } |
| 75 | |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 76 | X509* get() |
| 77 | { |
| 78 | return ptr; |
| 79 | } |
| 80 | ~OSSLX509() |
| 81 | { |
| 82 | X509_free(ptr); |
| 83 | } |
| 84 | }; |
| 85 | |
| 86 | class OSSLX509StoreCTX |
| 87 | { |
| 88 | X509_STORE_CTX* ptr = X509_STORE_CTX_new(); |
| 89 | |
| 90 | public: |
| 91 | OSSLX509StoreCTX& operator=(const OSSLX509StoreCTX&) = delete; |
| 92 | OSSLX509StoreCTX& operator=(OSSLX509StoreCTX&&) = delete; |
| 93 | |
| 94 | OSSLX509StoreCTX(const OSSLX509StoreCTX&) = delete; |
| 95 | OSSLX509StoreCTX(OSSLX509StoreCTX&&) = delete; |
| 96 | |
| 97 | OSSLX509StoreCTX() = default; |
| 98 | X509_STORE_CTX* get() |
| 99 | { |
| 100 | return ptr; |
| 101 | } |
| 102 | ~OSSLX509StoreCTX() |
| 103 | { |
| 104 | X509_STORE_CTX_free(ptr); |
| 105 | } |
| 106 | }; |
| 107 | |
| 108 | TEST(MutualTLS, GoodCert) |
| 109 | { |
| 110 | OSSLX509 x509; |
| 111 | |
Ed Tanous | 23f1c96 | 2023-12-05 15:57:46 -0800 | [diff] [blame] | 112 | x509.setSubjectName(); |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 113 | X509_EXTENSION* ex = X509V3_EXT_conf_nid(nullptr, nullptr, NID_key_usage, |
| 114 | "digitalSignature, keyAgreement"); |
| 115 | ASSERT_THAT(ex, NotNull()); |
| 116 | ASSERT_EQ(X509_add_ext(x509.get(), ex, -1), 1); |
| 117 | X509_EXTENSION_free(ex); |
| 118 | ex = X509V3_EXT_conf_nid(nullptr, nullptr, NID_ext_key_usage, "clientAuth"); |
| 119 | ASSERT_THAT(ex, NotNull()); |
| 120 | ASSERT_EQ(X509_add_ext(x509.get(), ex, -1), 1); |
| 121 | X509_EXTENSION_free(ex); |
| 122 | |
Ed Tanous | 23f1c96 | 2023-12-05 15:57:46 -0800 | [diff] [blame] | 123 | x509.sign(); |
| 124 | |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 125 | OSSLX509StoreCTX x509Store; |
| 126 | X509_STORE_CTX_set_current_cert(x509Store.get(), x509.get()); |
| 127 | |
| 128 | boost::asio::ip::address ip; |
| 129 | boost::asio::ssl::verify_context ctx(x509Store.get()); |
Patrick Williams | bd79bce | 2024-08-16 15:22:20 -0400 | [diff] [blame] | 130 | std::shared_ptr<persistent_data::UserSession> session = |
| 131 | verifyMtlsUser(ip, ctx); |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 132 | ASSERT_THAT(session, NotNull()); |
| 133 | EXPECT_THAT(session->username, "user"); |
| 134 | } |
| 135 | |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 136 | TEST(MutualTLS, MissingKeyUsage) |
| 137 | { |
Ed Tanous | 23f1c96 | 2023-12-05 15:57:46 -0800 | [diff] [blame] | 138 | for (const char* usageString : |
| 139 | {"digitalSignature", "keyAgreement", "digitalSignature, keyAgreement"}) |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 140 | { |
| 141 | OSSLX509 x509; |
Ed Tanous | 23f1c96 | 2023-12-05 15:57:46 -0800 | [diff] [blame] | 142 | x509.setSubjectName(); |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 143 | |
Patrick Williams | bd79bce | 2024-08-16 15:22:20 -0400 | [diff] [blame] | 144 | X509_EXTENSION* ex = |
| 145 | X509V3_EXT_conf_nid(nullptr, nullptr, NID_key_usage, usageString); |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 146 | |
| 147 | ASSERT_THAT(ex, NotNull()); |
| 148 | ASSERT_EQ(X509_add_ext(x509.get(), ex, -1), 1); |
| 149 | X509_EXTENSION_free(ex); |
| 150 | ex = X509V3_EXT_conf_nid(nullptr, nullptr, NID_ext_key_usage, |
| 151 | "clientAuth"); |
| 152 | ASSERT_THAT(ex, NotNull()); |
| 153 | ASSERT_EQ(X509_add_ext(x509.get(), ex, -1), 1); |
| 154 | X509_EXTENSION_free(ex); |
Ed Tanous | 23f1c96 | 2023-12-05 15:57:46 -0800 | [diff] [blame] | 155 | x509.sign(); |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 156 | |
| 157 | OSSLX509StoreCTX x509Store; |
| 158 | X509_STORE_CTX_set_current_cert(x509Store.get(), x509.get()); |
| 159 | |
| 160 | boost::asio::ip::address ip; |
| 161 | boost::asio::ssl::verify_context ctx(x509Store.get()); |
| 162 | std::shared_ptr<persistent_data::UserSession> session = |
| 163 | verifyMtlsUser(ip, ctx); |
Ed Tanous | 23f1c96 | 2023-12-05 15:57:46 -0800 | [diff] [blame] | 164 | ASSERT_THAT(session, NotNull()); |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 165 | } |
| 166 | } |
| 167 | |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 168 | TEST(MutualTLS, MissingCert) |
| 169 | { |
| 170 | OSSLX509StoreCTX x509Store; |
| 171 | |
| 172 | boost::asio::ip::address ip; |
| 173 | boost::asio::ssl::verify_context ctx(x509Store.get()); |
Patrick Williams | bd79bce | 2024-08-16 15:22:20 -0400 | [diff] [blame] | 174 | std::shared_ptr<persistent_data::UserSession> session = |
| 175 | verifyMtlsUser(ip, ctx); |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 176 | ASSERT_THAT(session, IsNull()); |
| 177 | } |
Malik Akbar Hashemi Rafsanjani | 4d7b5dd | 2025-02-26 13:14:30 -0800 | [diff] [blame] | 178 | |
| 179 | TEST(GetCommonNameFromCert, EmptyCommonName) |
| 180 | { |
| 181 | OSSLX509 x509; |
| 182 | std::string commonName = getCommonNameFromCert(x509.get()); |
| 183 | EXPECT_THAT(commonName, ""); |
| 184 | } |
| 185 | |
| 186 | TEST(GetCommonNameFromCert, ValidCommonName) |
| 187 | { |
| 188 | OSSLX509 x509; |
| 189 | x509.setSubjectName(); |
| 190 | std::string commonName = getCommonNameFromCert(x509.get()); |
| 191 | EXPECT_THAT(commonName, "user"); |
| 192 | } |
| 193 | |
| 194 | TEST(GetUPNFromCert, EmptySubjectAlternativeName) |
| 195 | { |
| 196 | OSSLX509 x509; |
| 197 | std::string upn = getUPNFromCert(x509.get(), ""); |
| 198 | EXPECT_THAT(upn, ""); |
| 199 | } |
| 200 | |
| 201 | TEST(GetUPNFromCert, NonOthernameSubjectAlternativeName) |
| 202 | { |
| 203 | OSSLX509 x509; |
| 204 | |
| 205 | ASN1_IA5STRING* ia5 = ASN1_IA5STRING_new(); |
| 206 | ASSERT_THAT(ia5, NotNull()); |
| 207 | |
| 208 | const char* user = "user@domain.com"; |
| 209 | ASSERT_NE(ASN1_STRING_set(ia5, user, static_cast<int>(strlen(user))), 0); |
| 210 | |
| 211 | GENERAL_NAMES* gens = sk_GENERAL_NAME_new_null(); |
| 212 | ASSERT_THAT(gens, NotNull()); |
| 213 | |
| 214 | GENERAL_NAME* gen = GENERAL_NAME_new(); |
| 215 | ASSERT_THAT(gen, NotNull()); |
| 216 | |
| 217 | GENERAL_NAME_set0_value(gen, GEN_EMAIL, ia5); |
| 218 | ASSERT_EQ(sk_GENERAL_NAME_push(gens, gen), 1); |
| 219 | |
| 220 | ASSERT_EQ(X509_add1_ext_i2d(x509.get(), NID_subject_alt_name, gens, 0, 0), |
| 221 | 1); |
| 222 | |
| 223 | std::string upn = getUPNFromCert(x509.get(), "hostname.domain.com"); |
| 224 | EXPECT_THAT(upn, ""); |
| 225 | |
| 226 | GENERAL_NAME_free(gen); |
| 227 | sk_GENERAL_NAME_free(gens); |
| 228 | } |
| 229 | |
| 230 | TEST(GetUPNFromCert, NonUPNSubjectAlternativeName) |
| 231 | { |
| 232 | OSSLX509 x509; |
| 233 | |
| 234 | GENERAL_NAMES* gens = sk_GENERAL_NAME_new_null(); |
| 235 | ASSERT_THAT(gens, NotNull()); |
| 236 | |
| 237 | GENERAL_NAME* gen = GENERAL_NAME_new(); |
| 238 | ASSERT_THAT(gen, NotNull()); |
| 239 | |
| 240 | ASN1_OBJECT* othType = OBJ_nid2obj(NID_SRVName); |
| 241 | |
| 242 | ASN1_TYPE* value = ASN1_TYPE_new(); |
| 243 | ASSERT_THAT(value, NotNull()); |
| 244 | value->type = V_ASN1_UTF8STRING; |
| 245 | |
| 246 | // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access) |
| 247 | value->value.utf8string = ASN1_UTF8STRING_new(); |
| 248 | ASSERT_THAT(value->value.utf8string, NotNull()); |
| 249 | const char* user = "user@domain.com"; |
| 250 | ASN1_STRING_set(value->value.utf8string, user, |
| 251 | static_cast<int>(strlen(user))); |
| 252 | // NOLINTEND(cppcoreguidelines-pro-type-union-access) |
| 253 | |
| 254 | ASSERT_EQ(GENERAL_NAME_set0_othername(gen, othType, value), 1); |
| 255 | ASSERT_EQ(sk_GENERAL_NAME_push(gens, gen), 1); |
| 256 | ASSERT_EQ(X509_add1_ext_i2d(x509.get(), NID_subject_alt_name, gens, 0, 0), |
| 257 | 1); |
| 258 | |
| 259 | std::string upn = getUPNFromCert(x509.get(), "hostname.domain.com"); |
| 260 | EXPECT_THAT(upn, ""); |
| 261 | |
| 262 | sk_GENERAL_NAME_pop_free(gens, GENERAL_NAME_free); |
| 263 | } |
| 264 | |
| 265 | TEST(GetUPNFromCert, NonUTF8UPNSubjectAlternativeName) |
| 266 | { |
| 267 | OSSLX509 x509; |
| 268 | |
| 269 | GENERAL_NAMES* gens = sk_GENERAL_NAME_new_null(); |
| 270 | ASSERT_THAT(gens, NotNull()); |
| 271 | |
| 272 | GENERAL_NAME* gen = GENERAL_NAME_new(); |
| 273 | ASSERT_THAT(gen, NotNull()); |
| 274 | |
| 275 | ASN1_OBJECT* othType = OBJ_nid2obj(NID_ms_upn); |
| 276 | |
| 277 | ASN1_TYPE* value = ASN1_TYPE_new(); |
| 278 | ASSERT_THAT(value, NotNull()); |
| 279 | value->type = V_ASN1_OCTET_STRING; |
| 280 | |
| 281 | // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access) |
| 282 | value->value.octet_string = ASN1_OCTET_STRING_new(); |
| 283 | ASSERT_THAT(value->value.octet_string, NotNull()); |
| 284 | const char* user = "0123456789"; |
| 285 | ASN1_STRING_set(value->value.octet_string, user, |
| 286 | static_cast<int>(strlen(user))); |
| 287 | // NOLINTEND(cppcoreguidelines-pro-type-union-access) |
| 288 | |
| 289 | ASSERT_EQ(GENERAL_NAME_set0_othername(gen, othType, value), 1); |
| 290 | ASSERT_EQ(sk_GENERAL_NAME_push(gens, gen), 1); |
| 291 | ASSERT_EQ(X509_add1_ext_i2d(x509.get(), NID_subject_alt_name, gens, 0, 0), |
| 292 | 1); |
| 293 | |
| 294 | std::string upn = getUPNFromCert(x509.get(), "hostname.domain.com"); |
| 295 | EXPECT_THAT(upn, ""); |
| 296 | |
| 297 | sk_GENERAL_NAME_pop_free(gens, GENERAL_NAME_free); |
| 298 | } |
| 299 | |
| 300 | TEST(GetUPNFromCert, ValidUPN) |
| 301 | { |
| 302 | OSSLX509 x509; |
| 303 | |
| 304 | GENERAL_NAMES* gens = sk_GENERAL_NAME_new_null(); |
| 305 | ASSERT_THAT(gens, NotNull()); |
| 306 | |
| 307 | GENERAL_NAME* gen = GENERAL_NAME_new(); |
| 308 | ASSERT_THAT(gen, NotNull()); |
| 309 | |
| 310 | ASN1_OBJECT* othType = OBJ_nid2obj(NID_ms_upn); |
| 311 | |
| 312 | ASN1_TYPE* value = ASN1_TYPE_new(); |
| 313 | ASSERT_THAT(value, NotNull()); |
| 314 | value->type = V_ASN1_UTF8STRING; |
| 315 | |
| 316 | // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access) |
| 317 | value->value.utf8string = ASN1_UTF8STRING_new(); |
| 318 | ASSERT_THAT(value->value.utf8string, NotNull()); |
| 319 | const char* user = "user@domain.com"; |
| 320 | ASN1_STRING_set(value->value.utf8string, user, |
| 321 | static_cast<int>(strlen(user))); |
| 322 | // NOLINTEND(cppcoreguidelines-pro-type-union-access) |
| 323 | |
| 324 | ASSERT_EQ(GENERAL_NAME_set0_othername(gen, othType, value), 1); |
| 325 | ASSERT_EQ(sk_GENERAL_NAME_push(gens, gen), 1); |
| 326 | ASSERT_EQ(X509_add1_ext_i2d(x509.get(), NID_subject_alt_name, gens, 0, 0), |
| 327 | 1); |
| 328 | |
| 329 | std::string upn = getUPNFromCert(x509.get(), "hostname.domain.com"); |
| 330 | EXPECT_THAT(upn, "user"); |
| 331 | |
| 332 | sk_GENERAL_NAME_pop_free(gens, GENERAL_NAME_free); |
| 333 | } |
| 334 | |
| 335 | TEST(IsUPNMatch, MultipleCases) |
| 336 | { |
| 337 | EXPECT_FALSE(isUPNMatch("user", "hostname.domain.com")); |
| 338 | EXPECT_TRUE(isUPNMatch("user@domain.com", "hostname.domain.com")); |
| 339 | EXPECT_FALSE(isUPNMatch("user@domain.com", "hostname.domain.org")); |
| 340 | EXPECT_FALSE(isUPNMatch("user@region.com", "hostname.domain.com")); |
| 341 | EXPECT_TRUE(isUPNMatch("user@com", "hostname.region.domain.com")); |
| 342 | } |
Ed Tanous | e792399 | 2023-12-03 14:14:02 -0800 | [diff] [blame] | 343 | } // namespace |