Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/digest_auth_fail_response.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<MHD_DigestAuthAlgorithm>(algorithm));
}

} // namespace httpserver
Expand Down
29 changes: 29 additions & 0 deletions src/http_request.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<MHD_DigestAuthAlgorithm>(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 {
Expand Down
9 changes: 7 additions & 2 deletions src/httpserver/digest_auth_fail_response.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
20 changes: 20 additions & 0 deletions src/httpserver/http_request.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

#include <stddef.h>
#include <algorithm>
#include <array>
#include <iosfwd>
#include <limits>
#include <map>
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions src/httpserver/http_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
226 changes: 226 additions & 0 deletions test/integ/authentication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<http_response> render_GET(const http_request& req) {
if (req.get_digested_user() == "") {
return std::make_shared<digest_auth_fail_response>(
"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<digest_auth_fail_response>(
"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<string_response>("SUCCESS", 200, "text/plain");
}
};

class digest_ha1_sha256_resource : public http_resource {
public:
shared_ptr<http_response> render_GET(const http_request& req) {
if (req.get_digested_user() == "") {
return std::make_shared<digest_auth_fail_response>(
"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<digest_auth_fail_response>(
"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<string_response>("SUCCESS", 200, "text/plain");
}
};

LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth)
webserver ws = create_webserver(PORT)
.digest_auth_random("myrandom")
Expand Down Expand Up @@ -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
Expand Down
Loading