Skip to content

Commit e06f83a

Browse files
authored
Add HA1-based digest authentication support (#360)
* 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 * 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.
1 parent 441f671 commit e06f83a

File tree

6 files changed

+299
-3
lines changed

6 files changed

+299
-3
lines changed

src/digest_auth_fail_response.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ struct MHD_Response;
3030
namespace httpserver {
3131

3232
int digest_auth_fail_response::enqueue_response(MHD_Connection* connection, MHD_Response* response) {
33-
return MHD_queue_auth_fail_response(connection, realm.c_str(), opaque.c_str(), response, reload_nonce ? MHD_YES : MHD_NO);
33+
return MHD_queue_auth_fail_response2(
34+
connection,
35+
realm.c_str(),
36+
opaque.c_str(),
37+
response,
38+
reload_nonce ? MHD_YES : MHD_NO,
39+
static_cast<MHD_DigestAuthAlgorithm>(algorithm));
3440
}
3541

3642
} // namespace httpserver

src/http_request.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,35 @@ bool http_request::check_digest_auth(const std::string& realm, const std::string
5858
*reload_nonce = false;
5959
return true;
6060
}
61+
62+
bool http_request::check_digest_auth_ha1(
63+
const std::string& realm,
64+
const unsigned char* digest,
65+
size_t digest_size,
66+
int nonce_timeout,
67+
bool* reload_nonce,
68+
http::http_utils::digest_algorithm algo) const {
69+
std::string_view digested_user = get_digested_user();
70+
71+
int val = MHD_digest_auth_check_digest2(
72+
underlying_connection,
73+
realm.c_str(),
74+
digested_user.data(),
75+
digest,
76+
digest_size,
77+
nonce_timeout,
78+
static_cast<MHD_DigestAuthAlgorithm>(algo));
79+
80+
if (val == MHD_INVALID_NONCE) {
81+
*reload_nonce = true;
82+
return false;
83+
} else if (val == MHD_NO) {
84+
*reload_nonce = false;
85+
return false;
86+
}
87+
*reload_nonce = false;
88+
return true;
89+
}
6190
#endif // HAVE_DAUTH
6291

6392
std::string_view http_request::get_connection_value(std::string_view key, enum MHD_ValueKind kind) const {

src/httpserver/digest_auth_fail_response.hpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@ class digest_auth_fail_response : public string_response {
4545
const std::string& opaque = "",
4646
bool reload_nonce = false,
4747
int response_code = http::http_utils::http_ok,
48-
const std::string& content_type = http::http_utils::text_plain):
48+
const std::string& content_type = http::http_utils::text_plain,
49+
http::http_utils::digest_algorithm algorithm =
50+
http::http_utils::digest_algorithm::MD5):
4951
string_response(content, response_code, content_type),
5052
realm(realm),
5153
opaque(opaque),
52-
reload_nonce(reload_nonce) { }
54+
reload_nonce(reload_nonce),
55+
algorithm(algorithm) { }
5356

5457
digest_auth_fail_response(const digest_auth_fail_response& other) = default;
5558
digest_auth_fail_response(digest_auth_fail_response&& other) noexcept = default;
@@ -64,6 +67,8 @@ class digest_auth_fail_response : public string_response {
6467
std::string realm = "";
6568
std::string opaque = "";
6669
bool reload_nonce = false;
70+
http::http_utils::digest_algorithm algorithm =
71+
http::http_utils::digest_algorithm::MD5;
6772
};
6873

6974
} // namespace httpserver

src/httpserver/http_request.hpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
#include <stddef.h>
3535
#include <algorithm>
36+
#include <array>
3637
#include <iosfwd>
3738
#include <limits>
3839
#include <map>
@@ -254,6 +255,25 @@ class http_request {
254255

255256
#ifdef HAVE_DAUTH
256257
bool check_digest_auth(const std::string& realm, const std::string& password, int nonce_timeout, bool* reload_nonce) const;
258+
259+
/**
260+
* Check digest authentication using a pre-computed HA1 hash.
261+
* The HA1 hash is computed as: hash(username:realm:password) using the specified algorithm.
262+
* @param realm The authentication realm.
263+
* @param digest Pointer to the pre-computed HA1 hash bytes.
264+
* @param digest_size Size of the digest (16 for MD5, 32 for SHA-256).
265+
* @param nonce_timeout Nonce validity timeout in seconds.
266+
* @param reload_nonce Output: set to true if nonce should be regenerated.
267+
* @param algo The digest algorithm (defaults to MD5).
268+
* @return true if authenticated, false otherwise.
269+
*/
270+
bool check_digest_auth_ha1(
271+
const std::string& realm,
272+
const unsigned char* digest,
273+
size_t digest_size,
274+
int nonce_timeout,
275+
bool* reload_nonce,
276+
http::http_utils::digest_algorithm algo = http::http_utils::digest_algorithm::MD5) const;
257277
#endif // HAVE_DAUTH
258278

259279
friend std::ostream &operator<< (std::ostream &os, http_request &r);

src/httpserver/http_utils.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,16 @@ class http_utils {
117117
IPV6 = 16
118118
};
119119

120+
#ifdef HAVE_DAUTH
121+
enum class digest_algorithm {
122+
MD5 = MHD_DIGEST_ALG_MD5,
123+
SHA256 = MHD_DIGEST_ALG_SHA256
124+
};
125+
126+
static constexpr size_t md5_digest_size = 16;
127+
static constexpr size_t sha256_digest_size = 32;
128+
#endif // HAVE_DAUTH
129+
120130
static const uint16_t http_method_connect_code;
121131
static const uint16_t http_method_delete_code;
122132
static const uint16_t http_method_get_code;

test/integ/authentication.cpp

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,72 @@ LT_END_AUTO_TEST(base_auth_fail)
157157
// Also skip if libmicrohttpd was built without digest auth support
158158
#if !defined(_WINDOWS) && defined(HAVE_DAUTH)
159159

160+
// Pre-computed MD5 hash of "myuser:examplerealm:mypass"
161+
// printf "myuser:examplerealm:mypass" | md5sum
162+
// 6ceef750e0130d6528b938c3abd94110
163+
static const unsigned char PRECOMPUTED_HA1_MD5[16] = {
164+
0x6c, 0xee, 0xf7, 0x50, 0xe0, 0x13, 0x0d, 0x65,
165+
0x28, 0xb9, 0x38, 0xc3, 0xab, 0xd9, 0x41, 0x10
166+
};
167+
168+
// Pre-computed SHA-256 hash of "myuser:examplerealm:mypass"
169+
// printf "myuser:examplerealm:mypass" | sha256sum
170+
// d4ff5b1795b23b4c625975959f3276526f3f4f4ef7d22083207e02d7c4bd8a05
171+
static const unsigned char PRECOMPUTED_HA1_SHA256[32] = {
172+
0xd4, 0xff, 0x5b, 0x17, 0x95, 0xb2, 0x3b, 0x4c,
173+
0x62, 0x59, 0x75, 0x95, 0x9f, 0x32, 0x76, 0x52,
174+
0x6f, 0x3f, 0x4f, 0x4e, 0xf7, 0xd2, 0x20, 0x83,
175+
0x20, 0x7e, 0x02, 0xd7, 0xc4, 0xbd, 0x8a, 0x05
176+
};
177+
178+
class digest_ha1_md5_resource : public http_resource {
179+
public:
180+
shared_ptr<http_response> render_GET(const http_request& req) {
181+
if (req.get_digested_user() == "") {
182+
return std::make_shared<digest_auth_fail_response>(
183+
"FAIL", "examplerealm", MY_OPAQUE, true,
184+
httpserver::http::http_utils::http_ok,
185+
httpserver::http::http_utils::text_plain,
186+
httpserver::http::http_utils::digest_algorithm::MD5);
187+
}
188+
bool reload_nonce = false;
189+
if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_MD5,
190+
httpserver::http::http_utils::md5_digest_size, 300, &reload_nonce,
191+
httpserver::http::http_utils::digest_algorithm::MD5)) {
192+
return std::make_shared<digest_auth_fail_response>(
193+
"FAIL", "examplerealm", MY_OPAQUE, reload_nonce,
194+
httpserver::http::http_utils::http_ok,
195+
httpserver::http::http_utils::text_plain,
196+
httpserver::http::http_utils::digest_algorithm::MD5);
197+
}
198+
return std::make_shared<string_response>("SUCCESS", 200, "text/plain");
199+
}
200+
};
201+
202+
class digest_ha1_sha256_resource : public http_resource {
203+
public:
204+
shared_ptr<http_response> render_GET(const http_request& req) {
205+
if (req.get_digested_user() == "") {
206+
return std::make_shared<digest_auth_fail_response>(
207+
"FAIL", "examplerealm", MY_OPAQUE, true,
208+
httpserver::http::http_utils::http_ok,
209+
httpserver::http::http_utils::text_plain,
210+
httpserver::http::http_utils::digest_algorithm::SHA256);
211+
}
212+
bool reload_nonce = false;
213+
if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_SHA256,
214+
httpserver::http::http_utils::sha256_digest_size, 300, &reload_nonce,
215+
httpserver::http::http_utils::digest_algorithm::SHA256)) {
216+
return std::make_shared<digest_auth_fail_response>(
217+
"FAIL", "examplerealm", MY_OPAQUE, reload_nonce,
218+
httpserver::http::http_utils::http_ok,
219+
httpserver::http::http_utils::text_plain,
220+
httpserver::http::http_utils::digest_algorithm::SHA256);
221+
}
222+
return std::make_shared<string_response>("SUCCESS", 200, "text/plain");
223+
}
224+
};
225+
160226
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth)
161227
webserver ws = create_webserver(PORT)
162228
.digest_auth_random("myrandom")
@@ -237,6 +303,166 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_wrong_pass)
237303
ws.stop();
238304
LT_END_AUTO_TEST(digest_auth_wrong_pass)
239305

306+
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5)
307+
webserver ws = create_webserver(PORT)
308+
.digest_auth_random("myrandom")
309+
.nonce_nc_size(300);
310+
311+
digest_ha1_md5_resource digest_ha1;
312+
LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1));
313+
ws.start(false);
314+
315+
#if defined(_WINDOWS)
316+
curl_global_init(CURL_GLOBAL_WIN32);
317+
#else
318+
curl_global_init(CURL_GLOBAL_ALL);
319+
#endif
320+
321+
std::string s;
322+
CURL *curl = curl_easy_init();
323+
CURLcode res;
324+
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
325+
#if defined(_WINDOWS)
326+
curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:mypass");
327+
#else
328+
curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:mypass");
329+
#endif
330+
curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base");
331+
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
332+
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);
333+
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s);
334+
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L);
335+
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L);
336+
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
337+
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
338+
res = curl_easy_perform(curl);
339+
LT_ASSERT_EQ(res, 0);
340+
LT_CHECK_EQ(s, "SUCCESS");
341+
curl_easy_cleanup(curl);
342+
343+
ws.stop();
344+
LT_END_AUTO_TEST(digest_auth_with_ha1_md5)
345+
346+
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5_wrong_pass)
347+
webserver ws = create_webserver(PORT)
348+
.digest_auth_random("myrandom")
349+
.nonce_nc_size(300);
350+
351+
digest_ha1_md5_resource digest_ha1;
352+
LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1));
353+
ws.start(false);
354+
355+
#if defined(_WINDOWS)
356+
curl_global_init(CURL_GLOBAL_WIN32);
357+
#else
358+
curl_global_init(CURL_GLOBAL_ALL);
359+
#endif
360+
361+
std::string s;
362+
CURL *curl = curl_easy_init();
363+
CURLcode res;
364+
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
365+
#if defined(_WINDOWS)
366+
curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:wrongpass");
367+
#else
368+
curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:wrongpass");
369+
#endif
370+
curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base");
371+
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
372+
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);
373+
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s);
374+
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L);
375+
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L);
376+
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
377+
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
378+
res = curl_easy_perform(curl);
379+
LT_ASSERT_EQ(res, 0);
380+
LT_CHECK_EQ(s, "FAIL");
381+
curl_easy_cleanup(curl);
382+
383+
ws.stop();
384+
LT_END_AUTO_TEST(digest_auth_with_ha1_md5_wrong_pass)
385+
386+
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256)
387+
webserver ws = create_webserver(PORT)
388+
.digest_auth_random("myrandom")
389+
.nonce_nc_size(300);
390+
391+
digest_ha1_sha256_resource digest_ha1;
392+
LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1));
393+
ws.start(false);
394+
395+
#if defined(_WINDOWS)
396+
curl_global_init(CURL_GLOBAL_WIN32);
397+
#else
398+
curl_global_init(CURL_GLOBAL_ALL);
399+
#endif
400+
401+
std::string s;
402+
CURL *curl = curl_easy_init();
403+
CURLcode res;
404+
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
405+
#if defined(_WINDOWS)
406+
curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:mypass");
407+
#else
408+
curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:mypass");
409+
#endif
410+
curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base");
411+
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
412+
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);
413+
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s);
414+
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L);
415+
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L);
416+
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
417+
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
418+
res = curl_easy_perform(curl);
419+
LT_ASSERT_EQ(res, 0);
420+
LT_CHECK_EQ(s, "SUCCESS");
421+
curl_easy_cleanup(curl);
422+
423+
ws.stop();
424+
LT_END_AUTO_TEST(digest_auth_with_ha1_sha256)
425+
426+
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256_wrong_pass)
427+
webserver ws = create_webserver(PORT)
428+
.digest_auth_random("myrandom")
429+
.nonce_nc_size(300);
430+
431+
digest_ha1_sha256_resource digest_ha1;
432+
LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1));
433+
ws.start(false);
434+
435+
#if defined(_WINDOWS)
436+
curl_global_init(CURL_GLOBAL_WIN32);
437+
#else
438+
curl_global_init(CURL_GLOBAL_ALL);
439+
#endif
440+
441+
std::string s;
442+
CURL *curl = curl_easy_init();
443+
CURLcode res;
444+
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
445+
#if defined(_WINDOWS)
446+
curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:wrongpass");
447+
#else
448+
curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:wrongpass");
449+
#endif
450+
curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base");
451+
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
452+
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);
453+
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s);
454+
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L);
455+
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L);
456+
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
457+
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
458+
res = curl_easy_perform(curl);
459+
LT_ASSERT_EQ(res, 0);
460+
LT_CHECK_EQ(s, "FAIL");
461+
curl_easy_cleanup(curl);
462+
463+
ws.stop();
464+
LT_END_AUTO_TEST(digest_auth_with_ha1_sha256_wrong_pass)
465+
240466
#endif
241467

242468
// Simple resource for centralized auth tests

0 commit comments

Comments
 (0)