Add mutual tls unit test

Mutual TLS paths were not tested.  Add some unit tests.

Because CI doesn't actually compile dependent libraries with ASAN
enabled, and these tests call into openssl, we need to add a check for
if we're compiling with asan enabled.

Tested: unit tests pass.

Change-Id: I02dcb69708619cc00fffd840738c608db3ae8bdf
Signed-off-by: Ed Tanous <ed@tanous.net>
diff --git a/test/http/mutual_tls.cpp b/test/http/mutual_tls.cpp
new file mode 100644
index 0000000..b1b7878
--- /dev/null
+++ b/test/http/mutual_tls.cpp
@@ -0,0 +1,174 @@
+#include "mutual_tls.hpp"
+
+#include <boost/asio/ip/address.hpp>
+#include <boost/asio/ssl/verify_context.hpp>
+
+#include <memory>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h> // IWYU pragma: keep
+
+using ::testing::IsNull;
+using ::testing::NotNull;
+
+namespace
+{
+class OSSLX509
+{
+    X509* ptr = X509_new();
+
+  public:
+    OSSLX509& operator=(const OSSLX509&) = delete;
+    OSSLX509& operator=(OSSLX509&&) = delete;
+
+    OSSLX509(const OSSLX509&) = delete;
+    OSSLX509(OSSLX509&&) = delete;
+
+    OSSLX509() = default;
+    X509* get()
+    {
+        return ptr;
+    }
+    ~OSSLX509()
+    {
+        X509_free(ptr);
+    }
+};
+
+class OSSLX509StoreCTX
+{
+    X509_STORE_CTX* ptr = X509_STORE_CTX_new();
+
+  public:
+    OSSLX509StoreCTX& operator=(const OSSLX509StoreCTX&) = delete;
+    OSSLX509StoreCTX& operator=(OSSLX509StoreCTX&&) = delete;
+
+    OSSLX509StoreCTX(const OSSLX509StoreCTX&) = delete;
+    OSSLX509StoreCTX(OSSLX509StoreCTX&&) = delete;
+
+    OSSLX509StoreCTX() = default;
+    X509_STORE_CTX* get()
+    {
+        return ptr;
+    }
+    ~OSSLX509StoreCTX()
+    {
+        X509_STORE_CTX_free(ptr);
+    }
+};
+
+TEST(MutualTLS, GoodCert)
+{
+    OSSLX509 x509;
+
+    X509_NAME* name = X509_get_subject_name(x509.get());
+    std::array<unsigned char, 5> user = {'u', 's', 'e', 'r', '\0'};
+    X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, user.data(), -1, -1,
+                               0);
+
+    X509_EXTENSION* ex = X509V3_EXT_conf_nid(nullptr, nullptr, NID_key_usage,
+                                             "digitalSignature, keyAgreement");
+    ASSERT_THAT(ex, NotNull());
+    ASSERT_EQ(X509_add_ext(x509.get(), ex, -1), 1);
+    X509_EXTENSION_free(ex);
+    ex = X509V3_EXT_conf_nid(nullptr, nullptr, NID_ext_key_usage, "clientAuth");
+    ASSERT_THAT(ex, NotNull());
+    ASSERT_EQ(X509_add_ext(x509.get(), ex, -1), 1);
+    X509_EXTENSION_free(ex);
+
+    OSSLX509StoreCTX x509Store;
+    X509_STORE_CTX_set_current_cert(x509Store.get(), x509.get());
+
+    boost::asio::ip::address ip;
+    boost::asio::ssl::verify_context ctx(x509Store.get());
+    std::shared_ptr<persistent_data::UserSession> session = verifyMtlsUser(ip,
+                                                                           ctx);
+    ASSERT_THAT(session, NotNull());
+    EXPECT_THAT(session->username, "user");
+}
+
+TEST(MutualTLS, MissingSubject)
+{
+    OSSLX509 x509;
+
+    X509_EXTENSION* ex = X509V3_EXT_conf_nid(nullptr, nullptr, NID_key_usage,
+                                             "digitalSignature, keyAgreement");
+    ASSERT_THAT(ex, NotNull());
+    ASSERT_EQ(X509_add_ext(x509.get(), ex, -1), 1);
+    X509_EXTENSION_free(ex);
+    ex = X509V3_EXT_conf_nid(nullptr, nullptr, NID_ext_key_usage, "clientAuth");
+    ASSERT_THAT(ex, NotNull());
+    ASSERT_EQ(X509_add_ext(x509.get(), ex, -1), 1);
+    X509_EXTENSION_free(ex);
+
+    OSSLX509StoreCTX x509Store;
+    X509_STORE_CTX_set_current_cert(x509Store.get(), x509.get());
+
+    boost::asio::ip::address ip;
+    boost::asio::ssl::verify_context ctx(x509Store.get());
+    std::shared_ptr<persistent_data::UserSession> session = verifyMtlsUser(ip,
+                                                                           ctx);
+    ASSERT_THAT(session, IsNull());
+}
+
+TEST(MutualTLS, MissingKeyUsage)
+{
+    for (const char* usageString : {"digitalSignature", "keyAgreement"})
+    {
+        OSSLX509 x509;
+
+        X509_EXTENSION* ex = X509V3_EXT_conf_nid(nullptr, nullptr,
+                                                 NID_key_usage, usageString);
+
+        ASSERT_THAT(ex, NotNull());
+        ASSERT_EQ(X509_add_ext(x509.get(), ex, -1), 1);
+        X509_EXTENSION_free(ex);
+        ex = X509V3_EXT_conf_nid(nullptr, nullptr, NID_ext_key_usage,
+                                 "clientAuth");
+        ASSERT_THAT(ex, NotNull());
+        ASSERT_EQ(X509_add_ext(x509.get(), ex, -1), 1);
+        X509_EXTENSION_free(ex);
+
+        OSSLX509StoreCTX x509Store;
+        X509_STORE_CTX_set_current_cert(x509Store.get(), x509.get());
+
+        boost::asio::ip::address ip;
+        boost::asio::ssl::verify_context ctx(x509Store.get());
+        std::shared_ptr<persistent_data::UserSession> session =
+            verifyMtlsUser(ip, ctx);
+        ASSERT_THAT(session, IsNull());
+    }
+}
+
+TEST(MutualTLS, MissingExtKeyUsage)
+{
+    OSSLX509 x509;
+
+    X509_EXTENSION* ex = X509V3_EXT_conf_nid(nullptr, nullptr, NID_key_usage,
+                                             "digitalSignature, keyAgreement");
+
+    ASSERT_THAT(ex, NotNull());
+    ASSERT_EQ(X509_add_ext(x509.get(), ex, -1), 1);
+    X509_EXTENSION_free(ex);
+
+    OSSLX509StoreCTX x509Store;
+    X509_STORE_CTX_set_current_cert(x509Store.get(), x509.get());
+
+    boost::asio::ip::address ip;
+    boost::asio::ssl::verify_context ctx(x509Store.get());
+    std::shared_ptr<persistent_data::UserSession> session = verifyMtlsUser(ip,
+                                                                           ctx);
+    ASSERT_THAT(session, IsNull());
+}
+
+TEST(MutualTLS, MissingCert)
+{
+    OSSLX509StoreCTX x509Store;
+
+    boost::asio::ip::address ip;
+    boost::asio::ssl::verify_context ctx(x509Store.get());
+    std::shared_ptr<persistent_data::UserSession> session = verifyMtlsUser(ip,
+                                                                           ctx);
+    ASSERT_THAT(session, IsNull());
+}
+} // namespace