diff --git a/src/digest_auth_fail_response.cpp b/src/digest_auth_fail_response.cpp index cb24325f..1fb8307c 100644 --- a/src/digest_auth_fail_response.cpp +++ b/src/digest_auth_fail_response.cpp @@ -30,7 +30,13 @@ struct MHD_Response; namespace httpserver { int digest_auth_fail_response::enqueue_response(MHD_Connection* connection, MHD_Response* response) { - return MHD_queue_auth_fail_response(connection, realm.c_str(), opaque.c_str(), response, reload_nonce ? MHD_YES : MHD_NO); + return MHD_queue_auth_fail_response2( + connection, + realm.c_str(), + opaque.c_str(), + response, + reload_nonce ? MHD_YES : MHD_NO, + static_cast(algorithm)); } } // namespace httpserver diff --git a/src/http_request.cpp b/src/http_request.cpp index 1a2b7c14..be532637 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -58,6 +58,35 @@ bool http_request::check_digest_auth(const std::string& realm, const std::string *reload_nonce = false; return true; } + +bool http_request::check_digest_auth_ha1( + const std::string& realm, + const unsigned char* digest, + size_t digest_size, + int nonce_timeout, + bool* reload_nonce, + http::http_utils::digest_algorithm algo) const { + std::string_view digested_user = get_digested_user(); + + int val = MHD_digest_auth_check_digest2( + underlying_connection, + realm.c_str(), + digested_user.data(), + digest, + digest_size, + nonce_timeout, + static_cast(algo)); + + if (val == MHD_INVALID_NONCE) { + *reload_nonce = true; + return false; + } else if (val == MHD_NO) { + *reload_nonce = false; + return false; + } + *reload_nonce = false; + return true; +} #endif // HAVE_DAUTH std::string_view http_request::get_connection_value(std::string_view key, enum MHD_ValueKind kind) const { diff --git a/src/httpserver/digest_auth_fail_response.hpp b/src/httpserver/digest_auth_fail_response.hpp index f3697c31..2eb044dc 100644 --- a/src/httpserver/digest_auth_fail_response.hpp +++ b/src/httpserver/digest_auth_fail_response.hpp @@ -45,11 +45,14 @@ class digest_auth_fail_response : public string_response { const std::string& opaque = "", bool reload_nonce = false, int response_code = http::http_utils::http_ok, - const std::string& content_type = http::http_utils::text_plain): + const std::string& content_type = http::http_utils::text_plain, + http::http_utils::digest_algorithm algorithm = + http::http_utils::digest_algorithm::MD5): string_response(content, response_code, content_type), realm(realm), opaque(opaque), - reload_nonce(reload_nonce) { } + reload_nonce(reload_nonce), + algorithm(algorithm) { } digest_auth_fail_response(const digest_auth_fail_response& other) = default; digest_auth_fail_response(digest_auth_fail_response&& other) noexcept = default; @@ -64,6 +67,8 @@ class digest_auth_fail_response : public string_response { std::string realm = ""; std::string opaque = ""; bool reload_nonce = false; + http::http_utils::digest_algorithm algorithm = + http::http_utils::digest_algorithm::MD5; }; } // namespace httpserver diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index 749e49ab..4c1c3323 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -254,6 +255,25 @@ class http_request { #ifdef HAVE_DAUTH bool check_digest_auth(const std::string& realm, const std::string& password, int nonce_timeout, bool* reload_nonce) const; + + /** + * Check digest authentication using a pre-computed HA1 hash. + * The HA1 hash is computed as: hash(username:realm:password) using the specified algorithm. + * @param realm The authentication realm. + * @param digest Pointer to the pre-computed HA1 hash bytes. + * @param digest_size Size of the digest (16 for MD5, 32 for SHA-256). + * @param nonce_timeout Nonce validity timeout in seconds. + * @param reload_nonce Output: set to true if nonce should be regenerated. + * @param algo The digest algorithm (defaults to MD5). + * @return true if authenticated, false otherwise. + */ + bool check_digest_auth_ha1( + const std::string& realm, + const unsigned char* digest, + size_t digest_size, + int nonce_timeout, + bool* reload_nonce, + http::http_utils::digest_algorithm algo = http::http_utils::digest_algorithm::MD5) const; #endif // HAVE_DAUTH friend std::ostream &operator<< (std::ostream &os, http_request &r); diff --git a/src/httpserver/http_utils.hpp b/src/httpserver/http_utils.hpp index d95c0eb8..35a5a45a 100644 --- a/src/httpserver/http_utils.hpp +++ b/src/httpserver/http_utils.hpp @@ -117,6 +117,16 @@ class http_utils { IPV6 = 16 }; +#ifdef HAVE_DAUTH + enum class digest_algorithm { + MD5 = MHD_DIGEST_ALG_MD5, + SHA256 = MHD_DIGEST_ALG_SHA256 + }; + + static constexpr size_t md5_digest_size = 16; + static constexpr size_t sha256_digest_size = 32; +#endif // HAVE_DAUTH + static const uint16_t http_method_connect_code; static const uint16_t http_method_delete_code; static const uint16_t http_method_get_code; diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index 05d1f254..202936ef 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -157,6 +157,72 @@ LT_END_AUTO_TEST(base_auth_fail) // Also skip if libmicrohttpd was built without digest auth support #if !defined(_WINDOWS) && defined(HAVE_DAUTH) +// Pre-computed MD5 hash of "myuser:examplerealm:mypass" +// printf "myuser:examplerealm:mypass" | md5sum +// 6ceef750e0130d6528b938c3abd94110 +static const unsigned char PRECOMPUTED_HA1_MD5[16] = { + 0x6c, 0xee, 0xf7, 0x50, 0xe0, 0x13, 0x0d, 0x65, + 0x28, 0xb9, 0x38, 0xc3, 0xab, 0xd9, 0x41, 0x10 +}; + +// Pre-computed SHA-256 hash of "myuser:examplerealm:mypass" +// printf "myuser:examplerealm:mypass" | sha256sum +// d4ff5b1795b23b4c625975959f3276526f3f4f4ef7d22083207e02d7c4bd8a05 +static const unsigned char PRECOMPUTED_HA1_SHA256[32] = { + 0xd4, 0xff, 0x5b, 0x17, 0x95, 0xb2, 0x3b, 0x4c, + 0x62, 0x59, 0x75, 0x95, 0x9f, 0x32, 0x76, 0x52, + 0x6f, 0x3f, 0x4f, 0x4e, 0xf7, 0xd2, 0x20, 0x83, + 0x20, 0x7e, 0x02, 0xd7, 0xc4, 0xbd, 0x8a, 0x05 +}; + +class digest_ha1_md5_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + if (req.get_digested_user() == "") { + return std::make_shared( + "FAIL", "examplerealm", MY_OPAQUE, true, + httpserver::http::http_utils::http_ok, + httpserver::http::http_utils::text_plain, + httpserver::http::http_utils::digest_algorithm::MD5); + } + bool reload_nonce = false; + if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_MD5, + httpserver::http::http_utils::md5_digest_size, 300, &reload_nonce, + httpserver::http::http_utils::digest_algorithm::MD5)) { + return std::make_shared( + "FAIL", "examplerealm", MY_OPAQUE, reload_nonce, + httpserver::http::http_utils::http_ok, + httpserver::http::http_utils::text_plain, + httpserver::http::http_utils::digest_algorithm::MD5); + } + return std::make_shared("SUCCESS", 200, "text/plain"); + } +}; + +class digest_ha1_sha256_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + if (req.get_digested_user() == "") { + return std::make_shared( + "FAIL", "examplerealm", MY_OPAQUE, true, + httpserver::http::http_utils::http_ok, + httpserver::http::http_utils::text_plain, + httpserver::http::http_utils::digest_algorithm::SHA256); + } + bool reload_nonce = false; + if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_SHA256, + httpserver::http::http_utils::sha256_digest_size, 300, &reload_nonce, + httpserver::http::http_utils::digest_algorithm::SHA256)) { + return std::make_shared( + "FAIL", "examplerealm", MY_OPAQUE, reload_nonce, + httpserver::http::http_utils::http_ok, + httpserver::http::http_utils::text_plain, + httpserver::http::http_utils::digest_algorithm::SHA256); + } + return std::make_shared("SUCCESS", 200, "text/plain"); + } +}; + LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth) webserver ws = create_webserver(PORT) .digest_auth_random("myrandom") @@ -237,6 +303,166 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_wrong_pass) ws.stop(); LT_END_AUTO_TEST(digest_auth_wrong_pass) +LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5) + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); + + digest_ha1_md5_resource digest_ha1; + LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1)); + ws.start(false); + +#if defined(_WINDOWS) + curl_global_init(CURL_GLOBAL_WIN32); +#else + curl_global_init(CURL_GLOBAL_ALL); +#endif + + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); +#if defined(_WINDOWS) + curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:mypass"); +#else + curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:mypass"); +#endif + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(digest_auth_with_ha1_md5) + +LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5_wrong_pass) + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); + + digest_ha1_md5_resource digest_ha1; + LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1)); + ws.start(false); + +#if defined(_WINDOWS) + curl_global_init(CURL_GLOBAL_WIN32); +#else + curl_global_init(CURL_GLOBAL_ALL); +#endif + + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); +#if defined(_WINDOWS) + curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:wrongpass"); +#else + curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:wrongpass"); +#endif + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "FAIL"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(digest_auth_with_ha1_md5_wrong_pass) + +LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256) + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); + + digest_ha1_sha256_resource digest_ha1; + LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1)); + ws.start(false); + +#if defined(_WINDOWS) + curl_global_init(CURL_GLOBAL_WIN32); +#else + curl_global_init(CURL_GLOBAL_ALL); +#endif + + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); +#if defined(_WINDOWS) + curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:mypass"); +#else + curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:mypass"); +#endif + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(digest_auth_with_ha1_sha256) + +LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256_wrong_pass) + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); + + digest_ha1_sha256_resource digest_ha1; + LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1)); + ws.start(false); + +#if defined(_WINDOWS) + curl_global_init(CURL_GLOBAL_WIN32); +#else + curl_global_init(CURL_GLOBAL_ALL); +#endif + + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); +#if defined(_WINDOWS) + curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:wrongpass"); +#else + curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:wrongpass"); +#endif + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "FAIL"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(digest_auth_with_ha1_sha256_wrong_pass) + #endif // Simple resource for centralized auth tests