1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #include "mutual_tls.hpp"
4
5 #include "mutual_tls_private.hpp"
6 #include "sessions.hpp"
7
8 #include <cstring>
9 #include <string>
10
11 extern "C"
12 {
13 #include <openssl/asn1.h>
14 #include <openssl/ec.h>
15 #include <openssl/evp.h>
16 #include <openssl/obj_mac.h>
17 #include <openssl/objects.h>
18 #include <openssl/types.h>
19 #include <openssl/x509.h>
20 #include <openssl/x509_vfy.h>
21 #include <openssl/x509v3.h>
22 }
23
24 #include <boost/asio/ip/address.hpp>
25 #include <boost/asio/ssl/verify_context.hpp>
26
27 #include <array>
28 #include <memory>
29
30 #include <gmock/gmock.h>
31 #include <gtest/gtest.h>
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;
50
setSubjectName()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 }
sign()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
get()76 X509* get()
77 {
78 return ptr;
79 }
~OSSLX509()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;
get()98 X509_STORE_CTX* get()
99 {
100 return ptr;
101 }
~OSSLX509StoreCTX()102 ~OSSLX509StoreCTX()
103 {
104 X509_STORE_CTX_free(ptr);
105 }
106 };
107
TEST(MutualTLS,GoodCert)108 TEST(MutualTLS, GoodCert)
109 {
110 OSSLX509 x509;
111
112 x509.setSubjectName();
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
123 x509.sign();
124
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());
130 std::shared_ptr<persistent_data::UserSession> session =
131 verifyMtlsUser(ip, ctx);
132 ASSERT_THAT(session, NotNull());
133 EXPECT_THAT(session->username, "user");
134 }
135
TEST(MutualTLS,MissingKeyUsage)136 TEST(MutualTLS, MissingKeyUsage)
137 {
138 for (const char* usageString :
139 {"digitalSignature", "keyAgreement", "digitalSignature, keyAgreement"})
140 {
141 OSSLX509 x509;
142 x509.setSubjectName();
143
144 X509_EXTENSION* ex =
145 X509V3_EXT_conf_nid(nullptr, nullptr, NID_key_usage, usageString);
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);
155 x509.sign();
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);
164 ASSERT_THAT(session, NotNull());
165 }
166 }
167
TEST(MutualTLS,MissingCert)168 TEST(MutualTLS, MissingCert)
169 {
170 OSSLX509StoreCTX x509Store;
171
172 boost::asio::ip::address ip;
173 boost::asio::ssl::verify_context ctx(x509Store.get());
174 std::shared_ptr<persistent_data::UserSession> session =
175 verifyMtlsUser(ip, ctx);
176 ASSERT_THAT(session, IsNull());
177 }
178
TEST(GetCommonNameFromCert,EmptyCommonName)179 TEST(GetCommonNameFromCert, EmptyCommonName)
180 {
181 OSSLX509 x509;
182 std::string commonName = getCommonNameFromCert(x509.get());
183 EXPECT_THAT(commonName, "");
184 }
185
TEST(GetCommonNameFromCert,ValidCommonName)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
TEST(GetUPNFromCert,EmptySubjectAlternativeName)194 TEST(GetUPNFromCert, EmptySubjectAlternativeName)
195 {
196 OSSLX509 x509;
197 std::string upn = getUPNFromCert(x509.get(), "");
198 EXPECT_THAT(upn, "");
199 }
200
TEST(GetUPNFromCert,NonOthernameSubjectAlternativeName)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
TEST(GetUPNFromCert,NonUPNSubjectAlternativeName)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
TEST(GetUPNFromCert,NonUTF8UPNSubjectAlternativeName)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
TEST(GetUPNFromCert,ValidUPN)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
TEST(IsUPNMatch,MultipleCases)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 }
343 } // namespace
344