Skip to content

Commit 2a4e0ea

Browse files
committed
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
1 parent 441f671 commit 2a4e0ea

File tree

4 files changed

+179
-0
lines changed

4 files changed

+179
-0
lines changed

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/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: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,46 @@ 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_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+
}
185+
bool reload_nonce = false;
186+
// Try MD5 first (default), then SHA-256 if that fails
187+
if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_MD5, 16, 300, &reload_nonce,
188+
httpserver::http::http_utils::digest_algorithm::MD5)) {
189+
// Try SHA-256
190+
if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_SHA256, 32, 300, &reload_nonce,
191+
httpserver::http::http_utils::digest_algorithm::SHA256)) {
192+
return std::make_shared<digest_auth_fail_response>(
193+
"FAIL", "examplerealm", MY_OPAQUE, reload_nonce);
194+
}
195+
}
196+
return std::make_shared<string_response>("SUCCESS", 200, "text/plain");
197+
}
198+
};
199+
160200
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth)
161201
webserver ws = create_webserver(PORT)
162202
.digest_auth_random("myrandom")
@@ -237,6 +277,86 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_wrong_pass)
237277
ws.stop();
238278
LT_END_AUTO_TEST(digest_auth_wrong_pass)
239279

280+
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1)
281+
webserver ws = create_webserver(PORT)
282+
.digest_auth_random("myrandom")
283+
.nonce_nc_size(300);
284+
285+
digest_ha1_resource digest_ha1;
286+
LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1));
287+
ws.start(false);
288+
289+
#if defined(_WINDOWS)
290+
curl_global_init(CURL_GLOBAL_WIN32);
291+
#else
292+
curl_global_init(CURL_GLOBAL_ALL);
293+
#endif
294+
295+
std::string s;
296+
CURL *curl = curl_easy_init();
297+
CURLcode res;
298+
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
299+
#if defined(_WINDOWS)
300+
curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:mypass");
301+
#else
302+
curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:mypass");
303+
#endif
304+
curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base");
305+
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
306+
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);
307+
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s);
308+
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L);
309+
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L);
310+
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
311+
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
312+
res = curl_easy_perform(curl);
313+
LT_ASSERT_EQ(res, 0);
314+
LT_CHECK_EQ(s, "SUCCESS");
315+
curl_easy_cleanup(curl);
316+
317+
ws.stop();
318+
LT_END_AUTO_TEST(digest_auth_with_ha1)
319+
320+
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_wrong_pass)
321+
webserver ws = create_webserver(PORT)
322+
.digest_auth_random("myrandom")
323+
.nonce_nc_size(300);
324+
325+
digest_ha1_resource digest_ha1;
326+
LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1));
327+
ws.start(false);
328+
329+
#if defined(_WINDOWS)
330+
curl_global_init(CURL_GLOBAL_WIN32);
331+
#else
332+
curl_global_init(CURL_GLOBAL_ALL);
333+
#endif
334+
335+
std::string s;
336+
CURL *curl = curl_easy_init();
337+
CURLcode res;
338+
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
339+
#if defined(_WINDOWS)
340+
curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:wrongpass");
341+
#else
342+
curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:wrongpass");
343+
#endif
344+
curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base");
345+
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
346+
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);
347+
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s);
348+
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L);
349+
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L);
350+
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
351+
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
352+
res = curl_easy_perform(curl);
353+
LT_ASSERT_EQ(res, 0);
354+
LT_CHECK_EQ(s, "FAIL");
355+
curl_easy_cleanup(curl);
356+
357+
ws.stop();
358+
LT_END_AUTO_TEST(digest_auth_with_ha1_wrong_pass)
359+
240360
#endif
241361

242362
// Simple resource for centralized auth tests

0 commit comments

Comments
 (0)