What changed, and why it matters
This change upgrades the Monero RPC server's HTTP Digest Authentication to support SHA-256 and adds a command-line flag to disable the older MD5 algorithm. MD5 is considered weak by modern security standards, so this gives node operators a way to enforce stronger authentication hashing. By default, both MD5 and SHA-256 are still offered, so the change is backwards-compatible and does not by itself remove the weaker option.
Operators who require stronger authentication should run monerod and monero-wallet-rpc with --disable-md5 once all clients support SHA-256 Digest Auth. Reviewers should verify that the SHA-256 implementation matches RFC 7616 and that the disable_md5 flag correctly suppresses both MD5 and MD5-sess in challenges and verification.
Security signals we found
Adds SHA-256 support to HTTP Digest Auth (RFC 7616)
Adds --disable-md5 flag to allow operators to reject MD5
MD5 remains enabled by default for backwards compatibility
Refactors digest code to use OpenSSL EVP_MD templated abstraction
Includes unit tests for new algorithms and disable behavior
Evidence from the diff
The commit merges PR #11345. It refactors the digest implementation in contrib/epee/src/http_auth.cpp into a templated digest_base_ using OpenSSL EVP APIs, adds a sha256_ algorithm, and updates http_server_auth/http_client_auth to prefer and accept SHA-256 per RFC 7616. A new –disable-md5 CLI flag is threaded through rpc_args, core_rpc_server, wallet_rpc_server, and the HTTP server config so that, when set, the server will neither advertise nor accept MD5/MD5-sess challenges. Unit tests cover RFC 7616 SHA-256 vectors, algorithm advertisement order, and rejection of MD5 when disabled.
Changed components
contrib/epee/include/net/http_auth.hcontrib/epee/include/net/http_protocol_handler.hcontrib/epee/include/net/http_server_impl_base.hcontrib/epee/src/http_auth.cppsrc/rpc/core_rpc_server.cppsrc/rpc/rpc_args.cppsrc/rpc/rpc_args.hsrc/wallet/wallet_rpc_server.cpptests/unit_tests/http.cppInspect captured patch +525 / −23
### contrib/epee/include/net/http_auth.h
@@ -56,7 +56,7 @@ namespace net_utils
wipeable_string password;
};
- //! Implements RFC 2617 digest auth. Digests from RFC 7616 can be added.
+ //! Implements RFC 7616 Digest Auth. Supports SHA-256 and MD5
class http_server_auth
{
public:
@@ -71,8 +71,8 @@ namespace net_utils
std::uint32_t counter;
};
- http_server_auth() : user(), rng() {}
- http_server_auth(login credentials, std::function<void(size_t, uint8_t*)> r);
+ http_server_auth() : user(), rng(), disable_md5(false) {}
+ http_server_auth(login credentials, std::function<void(size_t, uint8_t*)> r, bool disable_md5 = false);
//! \return Auth response, or `boost::none` iff `request` had valid auth.
boost::optional<http_response_info> get_response(const http_request_info& request)
@@ -88,9 +88,10 @@ namespace net_utils
boost::optional<session> user;
std::function<void(size_t, uint8_t*)> rng;
+ bool disable_md5;
};
- //! Implements RFC 2617 digest auth. Digests from RFC 7616 can be added.
+ //! Implements RFC 7616 Digest Auth. Supports SHA-256 and MD5
class http_client_auth
{
public:
### contrib/epee/include/net/http_protocol_handler.h
@@ -63,6 +63,7 @@ namespace net_utils
std::size_t m_max_public_ip_connections{3};
std::size_t m_max_private_ip_connections{25};
std::size_t m_max_connections{100};
+ bool m_disable_md5{false};
critical_section m_lock;
template<typename T>
@@ -197,7 +198,7 @@ namespace net_utils
http_custom_handler(i_service_endpoint* psnd_hndlr, config_type& config, t_connection_context& conn_context)
: simple_http_connection_handler<t_connection_context>(psnd_hndlr, config, conn_context),
m_config(config),
- m_auth(m_config.m_user ? http_server_auth{*m_config.m_user, config.rng} : http_server_auth{})
+ m_auth(m_config.m_user ? http_server_auth{*m_config.m_user, config.rng, m_config.m_disable_md5} : http_server_auth{})
{}
inline bool handle_request(const http_request_info& query_info, http_response_info& response)
{
### contrib/epee/include/net/http_server_impl_base.h
@@ -66,7 +66,8 @@ namespace epee
const std::size_t max_public_ip_connections = DEFAULT_RPC_MAX_CONNECTIONS_PER_PUBLIC_IP,
const std::size_t max_private_ip_connections = DEFAULT_RPC_MAX_CONNECTIONS_PER_PRIVATE_IP,
const std::size_t max_connections = DEFAULT_RPC_MAX_CONNECTIONS,
- const std::size_t response_soft_limit = DEFAULT_RPC_SOFT_LIMIT_SIZE)
+ const std::size_t response_soft_limit = DEFAULT_RPC_SOFT_LIMIT_SIZE,
+ const bool disable_md5 = false)
{
if (max_connections < max_public_ip_connections)
throw std::invalid_argument{"Max public IP connections cannot be more than max connections"};
@@ -88,6 +89,7 @@ namespace epee
m_net_server.get_config_object().m_max_public_ip_connections = max_public_ip_connections;
m_net_server.get_config_object().m_max_private_ip_connections = max_private_ip_connections;
m_net_server.get_config_object().m_max_connections = max_connections;
+ m_net_server.get_config_object().m_disable_md5 = disable_md5;
m_net_server.set_response_soft_limit(response_soft_limit);
m_net_server.set_connection_limit(this);
### contrib/epee/src/http_auth.cpp
@@ -106,10 +106,9 @@ namespace
//// Digest Algorithms
- struct md5_
+ template<const EVP_MD* (*DigestFunc)(), std::size_t DigestSize>
+ struct digest_base_
{
- static constexpr const boost::string_ref name = ceref(u8"MD5");
-
struct update
{
template<typename T>
@@ -139,13 +138,13 @@ namespace
};
template<typename... T>
- boost::optional<std::array<char, 32>> operator()(const T&... args) const
+ boost::optional<std::array<char, DigestSize * 2>> operator()(const T&... args) const
{
std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> ctx(EVP_MD_CTX_new(), &EVP_MD_CTX_free);
if (!ctx)
return boost::none;
- if (EVP_DigestInit_ex(ctx.get(), EVP_md5(), nullptr) != 1)
+ if (EVP_DigestInit_ex(ctx.get(), DigestFunc(), nullptr) != 1)
return boost::none;
bool ok = true;
@@ -156,16 +155,29 @@ namespace
if (!ok)
return boost::none;
- std::array<std::uint8_t, 16> digest{{}};
+ std::array<std::uint8_t, DigestSize> digest{{}};
if (EVP_DigestFinal_ex(ctx.get(), digest.data(), nullptr) != 1)
return boost::none;
return epee::to_hex::array(digest);
}
};
+
+ //! MD5 algo. Marked "historic" by RFC 7616; retained for backwards compatibility
+ struct md5_ : digest_base_<EVP_md5, 16>
+ {
+ static constexpr const boost::string_ref name = ceref(u8"MD5");
+ };
constexpr const boost::string_ref md5_::name;
+ //! SHA-256 algo
+ struct sha256_ : digest_base_<EVP_sha256, 32>
+ {
+ static constexpr const boost::string_ref name = ceref(u8"SHA-256");
+ };
+ constexpr const boost::string_ref sha256_::name;
+
//! Digest Algorithms available for HTTP Digest Auth. Sort better algos to the left
- constexpr const std::tuple<md5_> digest_algorithms{};
+ constexpr const std::tuple<sha256_, md5_> digest_algorithms{};
//// Various String Utilities
@@ -359,13 +371,13 @@ namespace
//! \return Status of the `response` field from the client
static status verify(const boost::string_ref method, const boost::string_ref uri,
- const boost::string_ref request, const http::http_server_auth::session& user)
+ const boost::string_ref request, const http::http_server_auth::session& user, const bool disable_md5)
{
const auto parsed = parse(request);
if (parsed &&
boost::equals(parsed->uri, uri) &&
boost::equals(parsed->username, user.credentials.username) &&
- boost::fusion::any(digest_algorithms, has_valid_response{*parsed, user, method}))
+ boost::fusion::any(digest_algorithms, has_valid_response{*parsed, user, method, disable_md5}))
{
if (boost::equals(parsed->nonce, user.nonce))
{
@@ -574,6 +586,8 @@ namespace
template<typename Digest>
bool operator()(const Digest& digest) const
{
+ if (disable_md5 && std::is_same<Digest, md5_>::value)
+ return false;
if (boost::starts_with(request.algorithm, Digest::name, ascii_iequal) ||
(request.algorithm.empty() && std::is_same<md5_, Digest>::value))
{
@@ -607,6 +621,7 @@ namespace
const auth_message& request;
const http::http_server_auth::session& user;
const boost::string_ref method;
+ const bool disable_md5;
};
boost::optional<std::uint32_t> counter() const
@@ -709,6 +724,9 @@ namespace
template<typename Digest>
void operator()(const Digest& digest) const
{
+ if (disable_md5 && std::is_same<Digest, md5_>::value)
+ return;
+
static constexpr const auto fvalue = ceref(u8"Digest qop=\"auth\"");
for (unsigned i = 0; i < 2; ++i)
@@ -730,9 +748,10 @@ namespace
const boost::string_ref nonce;
std::list<std::pair<std::string, std::string>>& fields;
const bool is_stale;
+ const bool disable_md5;
};
- http::http_response_info create_digest_response(const boost::string_ref nonce, const bool is_stale)
+ http::http_response_info create_digest_response(const boost::string_ref nonce, const bool is_stale, const bool disable_md5)
{
epee::net_utils::http::http_response_info rc{};
rc.m_response_code = 401;
@@ -742,7 +761,7 @@ namespace
u8"<html><head><title>Unauthorized Access</title></head><body><h1>401 Unauthorized</h1></body></html>";
boost::fusion::for_each(
- digest_algorithms, add_challenge{nonce, rc.m_additional_fields, is_stale}
+ digest_algorithms, add_challenge{nonce, rc.m_additional_fields, is_stale, disable_md5}
);
return rc;
@@ -755,8 +774,8 @@ namespace epee
{
namespace http
{
- http_server_auth::http_server_auth(login credentials, std::function<void(size_t, uint8_t*)> r)
- : user(session{std::move(credentials)}), rng(std::move(r)) {
+ http_server_auth::http_server_auth(login credentials, std::function<void(size_t, uint8_t*)> r, bool disable_md5_)
+ : user(session{std::move(credentials)}), rng(std::move(r)), disable_md5(disable_md5_) {
}
boost::optional<http_response_info> http_server_auth::do_get_response(const http_request_info& request)
@@ -773,7 +792,7 @@ namespace epee
if (auth != fields.end())
{
++(user->counter);
- switch (auth_message::verify(request.m_http_method_str, request.m_URI, auth->second, *user))
+ switch (auth_message::verify(request.m_http_method_str, request.m_URI, auth->second, *user, disable_md5))
{
case auth_message::kPass:
return boost::none;
@@ -793,7 +812,7 @@ namespace epee
rng(rand_128bit.size(), rand_128bit.data());
user->nonce = string_encoding::base64_encode(rand_128bit.data(), rand_128bit.size());
}
- return create_digest_response(user->nonce, is_stale);
+ return create_digest_response(user->nonce, is_stale, disable_md5);
}
http_client_auth::http_client_auth(login credentials)
### src/rpc/core_rpc_server.cpp
@@ -236,7 +236,8 @@ namespace cryptonote
std::move(bind_ipv6_str), std::move(rpc_config->use_ipv6), std::move(rpc_config->require_ipv4),
std::move(rpc_config->access_control_origins), std::move(http_login), std::move(rpc_config->ssl_options),
max_connections_public, max_connections_private, max_connections,
- command_line::get_arg(vm, arg_rpc_response_soft_limit)
+ command_line::get_arg(vm, arg_rpc_response_soft_limit),
+ rpc_config->disable_md5
);
m_net_server.get_config_object().m_max_content_length = MAX_RPC_CONTENT_LENGTH;
### src/rpc/rpc_args.cpp
@@ -106,6 +106,7 @@ namespace cryptonote
, rpc_ssl_allow_chained({"rpc-ssl-allow-chained", rpc_args::tr("Allow user (via --rpc-ssl-certificates) chain certificates"), false})
, rpc_ssl_allow_any_cert({"rpc-ssl-allow-any-cert", rpc_args::tr("Allow any peer certificate"), false})
, disable_rpc_ban({"disable-rpc-ban", rpc_args::tr("Do not ban hosts on RPC errors"), false, false})
+ , disable_md5({"disable-md5", rpc_args::tr("Disable MD5 algorithm for HTTP Digest authentication (only SHA-256 will be offered and accepted)"), false})
{}
const char* rpc_args::tr(const char* str) { return i18n_translate(str, "cryptonote::rpc_args"); }
@@ -129,6 +130,7 @@ namespace cryptonote
command_line::add_arg(desc, arg.rpc_ssl_allowed_fingerprints);
command_line::add_arg(desc, arg.rpc_ssl_allow_chained);
command_line::add_arg(desc, arg.disable_rpc_ban);
+ command_line::add_arg(desc, arg.disable_md5);
if (any_cert_option)
command_line::add_arg(desc, arg.rpc_ssl_allow_any_cert);
}
@@ -145,6 +147,7 @@ namespace cryptonote
config.use_ipv6 = command_line::get_arg(vm, arg.rpc_use_ipv6);
config.require_ipv4 = !command_line::get_arg(vm, arg.rpc_ignore_ipv4);
config.disable_rpc_ban = command_line::get_arg(vm, arg.disable_rpc_ban);
+ config.disable_md5 = command_line::get_arg(vm, arg.disable_md5);
if (!config.bind_ip.empty())
{
// always parse IP here for error consistency
### src/rpc/rpc_args.h
@@ -68,6 +68,7 @@ namespace cryptonote
const command_line::arg_descriptor<bool> rpc_ssl_allow_chained;
const command_line::arg_descriptor<bool> rpc_ssl_allow_any_cert;
const command_line::arg_descriptor<bool> disable_rpc_ban;
+ const command_line::arg_descriptor<bool> disable_md5;
};
// `allow_any_cert` bool toggles `--rpc-ssl-allow-any-cert` configuration
@@ -91,5 +92,6 @@ namespace cryptonote
boost::optional<tools::login> login; // currently `boost::none` if unspecified by user
epee::net_utils::ssl_options_t ssl_options = epee::net_utils::ssl_support_t::e_ssl_support_enabled;
bool disable_rpc_ban = false;
+ bool disable_md5 = false;
};
}
### src/wallet/wallet_rpc_server.cpp
@@ -455,7 +455,8 @@ namespace tools
std::move(rpc_config->access_control_origins), std::move(http_login),
std::move(rpc_config->ssl_options),
max_connections_public, max_connections_private, max_connections,
- command_line::get_arg(vm, arg_rpc_response_soft_limit)
+ command_line::get_arg(vm, arg_rpc_response_soft_limit),
+ rpc_config->disable_md5
);
}
//------------------------------------------------------------------------------------------------------------------------------
### tests/unit_tests/http.cpp
@@ -64,6 +64,7 @@
#include <vector>
#include "string_tools.h"
+#include "common/util.h"
#include "crypto/crypto.h"
namespace {
@@ -323,6 +324,14 @@ std::string md5_hex(const std::string& in)
return epee::string_tools::pod_to_hex(digest);
}
+std::string sha256_hex(const std::string& in)
+{
+ crypto::hash hash;
+ if (!tools::sha256sum(reinterpret_cast<const uint8_t*>(in.data()), in.size(), hash))
+ return {};
+ return epee::string_tools::pod_to_hex(hash);
+}
+
std::string get_a1(const http::login& user, const fields& src)
{
const std::string& realm = src.at(u8"realm");
@@ -344,6 +353,14 @@ std::string get_a1_sess(const http::login& user, const std::string& cnonce, cons
);
}
+std::string get_sha256_a1_sess(const http::login& user, const std::string& cnonce, const auth_responses& responses)
+{
+ const std::string& nonce = responses.at(0).at(u8"nonce");
+ return boost::join(
+ std::vector<std::string>{sha256_hex(get_a1(user, responses)), nonce, cnonce}, u8":"
+ );
+}
+
std::string get_a2(const std::string& uri)
{
return boost::join(std::vector<std::string>{"NOP", uri}, u8":");
@@ -700,6 +717,396 @@ TEST(HTTP_Server_Auth, MD5_sess_auth)
EXPECT_STREQ(u8"true", parsed_replay[0].at(u8"stale").c_str());
}
+TEST(HTTP_Server_Auth, Algorithms)
+{
+ // RFC 7616 3.7: a server may send multiple challenges; strongest first
+ http::login user{"foo", "bar"};
+ http::http_server_auth auth{user, rng};
+
+ const auto response = auth.get_response(make_request(fields{}));
+ ASSERT_TRUE(bool(response));
+
+ const auto parsed = parse_response(*response);
+ ASSERT_EQ(4u, parsed.size()); // sha256, sha256-sess, md5, md5-sess
+ EXPECT_STREQ(u8"SHA-256", parsed[0].at(u8"algorithm").c_str());
+ EXPECT_STREQ(u8"SHA-256-sess", parsed[1].at(u8"algorithm").c_str());
+ EXPECT_STREQ(u8"MD5", parsed[2].at(u8"algorithm").c_str());
+ EXPECT_STREQ(u8"MD5-sess", parsed[3].at(u8"algorithm").c_str());
+
+ for (const auto& challenge : parsed)
+ {
+ EXPECT_STREQ(u8"auth", challenge.at(u8"qop").c_str()); // RFC 7616: qop MUST be used
+ EXPECT_STREQ(u8"monero-rpc", challenge.at(u8"realm").c_str());
+ EXPECT_EQ(24u, challenge.at(u8"nonce").size());
+ EXPECT_STREQ(u8"false", challenge.at(u8"stale").c_str());
+ }
+}
+
+TEST(HTTP_Server_Auth, DisableMD5_Algorithms)
+{
+ // With "--disable-md5", only SHA-256 and SHA-256-sess should be advertised
+ http::login user{"foo", "bar"};
+ http::http_server_auth auth{user, rng, true};
+
+ const auto response = auth.get_response(make_request(fields{}));
+ ASSERT_TRUE(bool(response));
+
+ const auto parsed = parse_response(*response);
+ ASSERT_EQ(2u, parsed.size()); // sha256, sha256-sess only
+ EXPECT_STREQ(u8"SHA-256", parsed[0].at(u8"algorithm").c_str());
+ EXPECT_STREQ(u8"SHA-256-sess", parsed[1].at(u8"algorithm").c_str());
+
+ for (const auto& challenge : parsed)
+ {
+ EXPECT_STREQ(u8"auth", challenge.at(u8"qop").c_str());
+ EXPECT_STREQ(u8"monero-rpc", challenge.at(u8"realm").c_str());
+ EXPECT_EQ(24u, challenge.at(u8"nonce").size());
+ EXPECT_STREQ(u8"false", challenge.at(u8"stale").c_str());
+ }
+}
+
+TEST(HTTP_Server_Auth, DisableMD5_RejectsMD5)
+{
+ // With "--disable-md5", MD5 auth response must be rejected
+ http::login user{"foo", "bar"};
+ http::http_server_auth auth{user, rng, true};
+
+ const auto response = auth.get_response(make_request(fields{}));
+ ASSERT_TRUE(bool(response));
+
+ const auto fields = parse_response(*response);
+ ASSERT_EQ(2u, fields.size());
+
+ const std::string& nonce = fields[0].at(u8"nonce");
+ const std::string uri{"/some_foo_thing"};
+
+ const std::string a1 = get_a1(user, fields);
+ const std::string a2 = get_a2(uri);
+
+ const std::string auth_code = md5_hex(
+ boost::join(std::vector<std::string>{md5_hex(a1), nonce, md5_hex(a2)}, u8":")
+ );
+
+ const auto request = make_request({
+ {u8"algorithm", u8"md5"},
+ {u8"nonce", quoted(nonce)},
+ {u8"realm", quoted(fields[0].at(u8"realm"))},
+ {u8"response", quoted(auth_code)},
+ {u8"uri", quoted(uri)},
+ {u8"username", quoted(user.username)}
+ }, uri);
+
+ // MD5 auth must be rejected
+ const auto rejected = auth.get_response(request);
+ ASSERT_TRUE(bool(rejected));
+ EXPECT_TRUE(is_unauthorized(*rejected));
+
+ const auto request_default = make_request({
+ {u8"nonce", quoted(nonce)},
+ {u8"realm", quoted(fields[0].at(u8"realm"))},
+ {u8"response", quoted(auth_code)},
+ {u8"uri", quoted(uri)},
+ {u8"username", quoted(user.username)}
+ }, uri);
+
+ const auto rejected_default = auth.get_response(request_default);
+ ASSERT_TRUE(bool(rejected_default));
+ EXPECT_TRUE(is_unauthorized(*rejected_default));
+}
+
+TEST(HTTP_Server_Auth, DisableMD5_AcceptsSHA256)
+{
+ // With "--disable-md5", SHA-256 auth must still succeed
+ http::login user{"foo", "bar"};
+ http::http_server_auth auth{user, rng, true};
+
+ const auto response = auth.get_response(make_request(fields{}));
+ ASSERT_TRUE(bool(response));
+
+ const auto fields = parse_response(*response);
+ ASSERT_EQ(2u, fields.size());
+
+ const std::string& nonce = fields[0].at(u8"nonce");
+ const std::string uri{"/some_foo_thing"};
+
+ const std::string a1 = get_a1(user, fields);
+ const std::string a2 = get_a2(uri);
+
+ const std::string auth_code = sha256_hex(
+ boost::join(std::vector<std::string>{sha256_hex(a1), nonce, sha256_hex(a2)}, u8":")
+ );
+
+ const auto request = make_request({
+ {u8"algorithm", u8"sha-256"},
+ {u8"nonce", quoted(nonce)},
+ {u8"realm", quoted(fields[0].at(u8"realm"))},
+ {u8"response", quoted(auth_code)},
+ {u8"uri", quoted(uri)},
+ {u8"username", quoted(user.username)}
+ }, uri);
+
+ // SHA-256 auth must succeed
+ EXPECT_FALSE(bool(auth.get_response(request)));
+}
+
+TEST(HTTP_Server_Auth, SHA256)
+{
+ http::login user{"foo", "bar"};
+ http::http_server_auth auth{user, rng};
+
+ const auto response = auth.get_response(make_request(fields{}));
+ ASSERT_TRUE(bool(response));
+ EXPECT_TRUE(is_unauthorized(*response));
+
+ const auto fields = parse_response(*response);
+ ASSERT_EQ(4u, fields.size());
+ EXPECT_TRUE(has_same_fields(fields));
+
+ const std::string& nonce = fields[0].at(u8"nonce");
+ EXPECT_EQ(24, nonce.size());
+
+ const std::string uri{"/some_foo_thing"};
+
+ const std::string a1 = get_a1(user, fields);
+ const std::string a2 = get_a2(uri);
+
+ const std::string auth_code = sha256_hex(
+ boost::join(std::vector<std::string>{sha256_hex(a1), nonce, sha256_hex(a2)}, u8":")
+ );
+
+ const auto request = make_request({
+ {u8"algorithm", u8"sha-256"},
+ {u8"nonce", quoted(nonce)},
+ {u8"realm", quoted(fields[0].at(u8"realm"))},
+ {u8"response", quoted(auth_code)},
+ {u8"uri", quoted(uri)},
+ {u8"username", quoted(user.username)}
+ }, uri);
+
+ EXPECT_FALSE(bool(auth.get_response(request)));
+
+ const auto response2 = auth.get_response(request);
+ ASSERT_TRUE(bool(response2));
+ EXPECT_TRUE(is_unauthorized(*response2));
+
+ const auto fields2 = parse_response(*response2);
+ ASSERT_EQ(4u, fields2.size());
+ EXPECT_TRUE(has_same_fields(fields2));
+
+ EXPECT_NE(nonce, fields2[0].at(u8"nonce"));
+ EXPECT_STREQ(u8"true", fields2[0].at(u8"stale").c_str());
+}
+
+TEST(HTTP_Server_Auth, SHA256_sess)
+{
+ constexpr const char cnonce[] = "not a good cnonce";
+
+ http::login user{"foo", "bar"};
+ http::http_server_auth auth{user, rng};
+
+ const auto response = auth.get_response(make_request(fields{}));
+ ASSERT_TRUE(bool(response));
+ EXPECT_TRUE(is_unauthorized(*response));
+
+ const auto fields = parse_response(*response);
+ ASSERT_EQ(4u, fields.size());
+ EXPECT_TRUE(has_same_fields(fields));
+
+ const std::string& nonce = fields[0].at(u8"nonce");
+ EXPECT_EQ(24, nonce.size());
+
+ const std::string uri{"/some_foo_thing"};
+
+ const std::string a1 = get_sha256_a1_sess(user, cnonce, fields);
+ const std::string a2 = get_a2(uri);
+
+ const std::string auth_code = sha256_hex(
+ boost::join(std::vector<std::string>{sha256_hex(a1), nonce, sha256_hex(a2)}, u8":")
+ );
+
+ const auto request = make_request({
+ {u8"algorithm", u8"sha-256-sess"},
+ {u8"cnonce", quoted(cnonce)},
+ {u8"nonce", quoted(nonce)},
+ {u8"realm", quoted(fields[0].at(u8"realm"))},
+ {u8"response", quoted(auth_code)},
+ {u8"uri", quoted(uri)},
+ {u8"username", quoted(user.username)}
+ }, uri);
+
+ EXPECT_FALSE(bool(auth.get_response(request)));
+
+ const auto response2 = auth.get_response(request);
+ ASSERT_TRUE(bool(response2));
+ EXPECT_TRUE(is_unauthorized(*response2));
+
+ const auto fields2 = parse_response(*response2);
+ ASSERT_EQ(4u, fields2.size());
+ EXPECT_TRUE(has_same_fields(fields2));
+
+ EXPECT_NE(nonce, fields2[0].at(u8"nonce"));
+ EXPECT_STREQ(u8"true", fields2[0].at(u8"stale").c_str());
+}
+
+TEST(HTTP_Server_Auth, SHA256_auth)
+{
+ constexpr const char cnonce[] = "not a nonce";
+ constexpr const char qop[] = "auth";
+
+ http::login user{"foo", "bar"};
+ http::http_server_auth auth{user, rng};
+
+ const auto response = auth.get_response(make_request(fields{}));
+ ASSERT_TRUE(bool(response));
+ EXPECT_TRUE(is_unauthorized(*response));
+
+ const auto parsed = parse_response(*response);
+ ASSERT_EQ(4u, parsed.size());
+ EXPECT_TRUE(has_same_fields(parsed));
+
+ const std::string& nonce = parsed[0].at(u8"nonce");
+ EXPECT_EQ(24, nonce.size());
+
+ const std::string uri{"/some_foo_thing"};
+
+ const std::string a1 = get_a1(user, parsed);
+ const std::string a2 = get_a2(uri);
+ std::string nc = get_nc(1);
+
+ const auto generate_auth = [&] {
+ return sha256_hex(
+ boost::join(
+ std::vector<std::string>{sha256_hex(a1), nonce, nc, cnonce, qop, sha256_hex(a2)}, u8":"
+ )
+ );
+ };
+
+ fields args{
+ {u8"algorithm", quoted(u8"sha-256")},
+ {u8"cnonce", quoted(cnonce)},
+ {u8"nc", nc},
+ {u8"nonce", quoted(nonce)},
+ {u8"qop", quoted(qop)},
+ {u8"realm", quoted(parsed[0].at(u8"realm"))},
+ {u8"response", quoted(generate_auth())},
+ {u8"uri", quoted(uri)},
+ {u8"username", quoted(user.username)}
+ };
+
+ const auto request = make_request(args, uri);
+ EXPECT_FALSE(bool(auth.get_response(request)));
+
+ for (unsigned i = 2; i < 20; ++i)
+ {
+ nc = get_nc(i);
+ args.at(u8"nc") = nc;
+ args.at(u8"response") = quoted(generate_auth());
+ EXPECT_FALSE(auth.get_response(make_request(args, uri)));
+ }
+
+ const auto replay = auth.get_response(request);
+ ASSERT_TRUE(bool(replay));
+ EXPECT_TRUE(is_unauthorized(*replay));
+
+ const auto parsed_replay = parse_response(*replay);
+ ASSERT_EQ(4u, parsed_replay.size());
+ EXPECT_TRUE(has_same_fields(parsed_replay));
+
+ EXPECT_NE(nonce, parsed_replay[0].at(u8"nonce"));
+ EXPECT_STREQ(u8"true", parsed_replay[0].at(u8"stale").c_str());
+}
+
+TEST(HTTP_Server_Auth, SHA256_sess_auth)
+{
+ constexpr const char cnonce[] = "not a nonce";
+ constexpr const char qop[] = "auth";
+
+ http::login user{"foo", "bar"};
+ http::http_server_auth auth{user, rng};
+
+ const auto response = auth.get_response(make_request(fields{}));
+ ASSERT_TRUE(bool(response));
+ EXPECT_TRUE(is_unauthorized(*response));
+
+ const auto parsed = parse_response(*response);
+ ASSERT_EQ(4u, parsed.size());
+ EXPECT_TRUE(has_same_fields(parsed));
+
+ const std::string& nonce = parsed[0].at(u8"nonce");
+ EXPECT_EQ(24, nonce.size());
+
+ const std::string uri{"/some_foo_thing"};
+
+ const std::string a1 = get_sha256_a1_sess(user, cnonce, parsed);
+ const std::string a2 = get_a2(uri);
+ std::string nc = get_nc(1);
+
+ const auto generate_auth = [&] {
+ return sha256_hex(
+ boost::join(
+ std::vector<std::string>{sha256_hex(a1), nonce, nc, cnonce, qop, sha256_hex(a2)}, u8":"
+ )
+ );
+ };
+
+ fields args{
+ {u8"algorithm", u8"sha-256-sess"},
+ {u8"cnonce", quoted(cnonce)},
+ {u8"nc", nc},
+ {u8"nonce", quoted(nonce)},
+ {u8"qop", qop},
+ {u8"realm", quoted(parsed[0].at(u8"realm"))},
+ {u8"response", quoted(generate_auth())},
+ {u8"uri", quoted(uri)},
+ {u8"username", quoted(user.username)}
+ };
+
+ const auto request = make_request(args, uri);
+ EXPECT_FALSE(bool(auth.get_response(request)));
+
+ for (unsigned i = 2; i < 20; ++i)
+ {
+ nc = get_nc(i);
+ args.at(u8"nc") = nc;
+ args.at(u8"response") = quoted(generate_auth());
+ EXPECT_FALSE(auth.get_response(make_request(args, uri)));
+ }
+
+ const auto replay = auth.get_response(request);
+ ASSERT_TRUE(bool(replay));
+ EXPECT_TRUE(is_unauthorized(*replay));
+
+ const auto parsed_replay = parse_response(*replay);
+ ASSERT_EQ(4u, parsed_replay.size());
+ EXPECT_TRUE(has_same_fields(parsed_replay));
+
+ EXPECT_NE(nonce, parsed_replay[0].at(u8"nonce"));
+ EXPECT_STREQ(u8"true", parsed_replay[0].at(u8"stale").c_str());
+}
+
+TEST(HTTP_Auth, RFC7616_SHA256_Vector)
+{
+ // Official test vector from RFC 7616 section 3.9.1
+ constexpr char username[] = "Mufasa";
+ constexpr char realm[] = "http-auth@example.org";
+ constexpr char password[] = "Circle of Life";
+ constexpr char nonce[] = "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v";
+ constexpr char cnonce[] = "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ";
+ constexpr char uri[] = "/dir/index.html";
+ constexpr char expected_response[] =
+ "753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1";
+
+ const std::string ha1 = sha256_hex(
+ boost::join(std::vector<std::string>{username, realm, password}, u8":")
+ );
+ const std::string ha2 = sha256_hex(
+ boost::join(std::vector<std::string>{"GET", uri}, u8":")
+ );
+ const std::string response = sha256_hex(
+ boost::join(std::vector<std::string>{ha1, nonce, u8"00000001", cnonce, u8"auth", ha2}, u8":")
+ );
+ EXPECT_STREQ(expected_response, response.c_str());
+}
TEST(HTTP_Auth, RequestTargetBinding)
{
@@ -930,6 +1337,71 @@ TEST(HTTP_Client_Auth, MD5_auth)
EXPECT_EQ(http::http_client_auth::kSuccess, auth.handle_401(response));
}
+TEST(HTTP_Client_Auth, SHA256_auth_RFC7616)
+{
+ // Inputs taken from RFC 7616 section 3.9.1; the client must select
+ constexpr char method[] = "GET";
+ constexpr char nonce[] = "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v";
+ constexpr char opaque[] = "FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS";
+ constexpr char realm[] = "http-auth@example.org";
+ constexpr char uri[] = "/dir/index.html";
+
+ const http::login user{"Mufasa", "Circle of Life"};
+ http::http_client_auth auth{user};
+
+ auto response = make_response({
+ {
+ {u8"algorithm", u8"MD5"},
+ {u8"nonce", quoted(std::string{"e"} + nonce)},
+ {u8"opaque", quoted(std::string{"e"} + opaque)},
+ {u8"realm", quoted(std::string{"e"} + realm)},
+ {u8"qop", quoted(u8"auth")}
+ },
+ {
+ {u8"algorithm", u8"SHA-256"},
+ {u8"nonce", quoted(nonce)},
+ {u8"opaque", quoted(opaque)},
+ {u8"realm", quoted(realm)},
+ {u8"qop", quoted(u8"auth, auth-int")}
+ }
+ });
+
+ EXPECT_EQ(http::http_client_auth::kSuccess, auth.handle_401(response));
+
+ const std::string a1 =
+ boost::join(std::vector<std::string>{user.username, realm, std::string(user.password.data(), user.password.size())}, u8":");
+ const std::string a2 = boost::join(std::vector<std::string>{method, uri}, u8":");
+
+ for (unsigned i = 1; i <= 10; ++i)
+ {
+ const std::string nc = get_nc(i);
+
+ const auto auth_field = auth.get_auth_field(method, uri);
+ ASSERT_TRUE(bool(auth_field));
+
+ const auto parsed = parse_fields(auth_field->second);
+ EXPECT_STREQ(u8"Authorization", auth_field->first.c_str());
+ EXPECT_STREQ(u8"SHA-256", parsed.at(u8"algorithm").c_str());
+ EXPECT_STREQ(nonce, parsed.at(u8"nonce").c_str());
+ EXPECT_STREQ(opaque, parsed.at(u8"opaque").c_str());
+ EXPECT_STREQ(u8"auth", parsed.at(u8"qop").c_str());
+ EXPECT_STREQ(uri, parsed.at(u8"uri").c_str());
+ EXPECT_EQ(user.username, parsed.at(u8"username"));
+ EXPECT_STREQ(realm, parsed.at(u8"realm").c_str());
+ EXPECT_EQ(nc, parsed.at(u8"nc"));
+
+ const std::string auth_code = sha256_hex(
+ boost::join(
+ std::vector<std::string>{sha256_hex(a1), nonce, nc, parsed.at(u8"cnonce"), u8"auth", sha256_hex(a2)},
+ u8":")
+ );
+ EXPECT_TRUE(boost::iequals(auth_code, parsed.at(u8"response")));
+ }
+
+ EXPECT_EQ(http::http_client_auth::kBadPassword, auth.handle_401(response));
+ response.m_header_info.m_etc_fields.back().second.append(u8"," + write_fields({{u8"stale", u8"true"}}));
+ EXPECT_EQ(http::http_client_auth::kSuccess, auth.handle_401(response));
+}
TEST(HTTP, Parse_Header_Line)
{Why this scored 36/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.