From 2a4e0ea125ed6c04522edbeec40dd560c29bd4bb Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Jan 2026 11:58:57 -0800 Subject: [PATCH 1/2] Add HA1-based digest authentication support Adds check_digest_auth_ha1() method that accepts pre-computed HA1 hash bytes instead of plaintext password. This allows secure storage of password hashes rather than plaintext passwords. Changes: - Add digest_algorithm enum (MD5, SHA256) without AUTO since libmicrohttpd cannot auto-detect algorithm from raw hash bytes - Add md5_digest_size and sha256_digest_size constants - Add check_digest_auth_ha1() to http_request - Add integration tests for HA1-based digest authentication --- src/http_request.cpp | 29 ++++++++ src/httpserver/http_request.hpp | 20 ++++++ src/httpserver/http_utils.hpp | 10 +++ test/integ/authentication.cpp | 120 ++++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+) 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/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..d61ef755 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -157,6 +157,46 @@ 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_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); + } + bool reload_nonce = false; + // Try MD5 first (default), then SHA-256 if that fails + if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_MD5, 16, 300, &reload_nonce, + httpserver::http::http_utils::digest_algorithm::MD5)) { + // Try SHA-256 + if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_SHA256, 32, 300, &reload_nonce, + httpserver::http::http_utils::digest_algorithm::SHA256)) { + return std::make_shared( + "FAIL", "examplerealm", MY_OPAQUE, reload_nonce); + } + } + 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 +277,86 @@ 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) + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); + + digest_ha1_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) + +LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_wrong_pass) + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); + + digest_ha1_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_wrong_pass) + #endif // Simple resource for centralized auth tests From 31c5cab6f3cb48d763cadc4ecfcee3a52d72ca62 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Jan 2026 15:23:29 -0800 Subject: [PATCH 2/2] Extend digest_auth_fail_response for algorithm specification - Add algorithm parameter to digest_auth_fail_response constructor (defaults to MD5 for backward compatibility) - Use MHD_queue_auth_fail_response2() to specify the algorithm in the WWW-Authenticate challenge header - Add separate MD5 and SHA256 test resources for deterministic testing - Add SHA256 digest auth tests alongside existing MD5 tests This enables server-driven algorithm selection, where the server requests a specific digest algorithm in the challenge and curl responds using that algorithm. --- src/digest_auth_fail_response.cpp | 8 +- src/httpserver/digest_auth_fail_response.hpp | 9 +- test/integ/authentication.cpp | 138 ++++++++++++++++--- 3 files changed, 136 insertions(+), 19 deletions(-) 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/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/test/integ/authentication.cpp b/test/integ/authentication.cpp index d61ef755..202936ef 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -175,23 +175,49 @@ static const unsigned char PRECOMPUTED_HA1_SHA256[32] = { 0x20, 0x7e, 0x02, 0xd7, 0xc4, 0xbd, 0x8a, 0x05 }; -class digest_ha1_resource : public http_resource { +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); + "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; - // Try MD5 first (default), then SHA-256 if that fails - if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_MD5, 16, 300, &reload_nonce, + 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)) { - // Try SHA-256 - if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_SHA256, 32, 300, &reload_nonce, - httpserver::http::http_utils::digest_algorithm::SHA256)) { - return std::make_shared( - "FAIL", "examplerealm", MY_OPAQUE, reload_nonce); - } + 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"); } @@ -277,12 +303,92 @@ 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) +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_resource digest_ha1; + digest_ha1_sha256_resource digest_ha1; LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1)); ws.start(false); @@ -315,14 +421,14 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1) curl_easy_cleanup(curl); ws.stop(); -LT_END_AUTO_TEST(digest_auth_with_ha1) +LT_END_AUTO_TEST(digest_auth_with_ha1_sha256) -LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_wrong_pass) +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_resource digest_ha1; + digest_ha1_sha256_resource digest_ha1; LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1)); ws.start(false); @@ -355,7 +461,7 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_wrong_pass) curl_easy_cleanup(curl); ws.stop(); -LT_END_AUTO_TEST(digest_auth_with_ha1_wrong_pass) +LT_END_AUTO_TEST(digest_auth_with_ha1_sha256_wrong_pass) #endif