What changed, and why it matters
This commit tightens how Monero's built-in HTTP client handles password-based authentication (HTTP Digest). It removes support for an older, weaker mode that did not use a client nonce ('cnonce'), and now always generates a random cnonce using OpenSSL's secure random generator. This makes replay and certain man-in-the-middle attacks against the HTTP client harder. The change also updates the tests to expect the new, stricter behavior.
Treat as a security hardening fix. Users and downstreams should apply the patch. Review whether any legitimate server configurations relied on the removed qop-less RFC 2069 mode, since those will now fail authentication. No immediate incident response is indicated beyond normal patching.
Security signals we found
Use of cryptographically secure random (OpenSSL RAND_bytes) for cnonce generation
Removal of fallback to RFC 2069 digest mode lacking client nonce
Addition of cnonce to Authorization header and response digest
Test expectations changed to reject qop-less digest authentication
Evidence from the diff
The patch modifies contrib/epee/src/http_auth.cpp. Previously the client could fall back to RFC 2069-style digest authentication when the server did not offer a qop directive, and the qop=auth path used an empty cnonce. The new code: (1) always generates a 16-byte random cnonce via RAND_bytes and base64-encodes it; (2) includes that cnonce in the digest response calculation and the Authorization header; (3) rejects server 401 responses that omit qop by setting index out of range (effectively disabling the old_algorithm path). Unit tests are updated: the MD5 test now expects kParseFailure for a qop-less 401, and the MD5_auth test verifies the random cnonce is used in the response hash.
Changed components
contrib/epee/src/http_auth.cpptests/unit_tests/http.cppMonero HTTP client digest authenticationInspect captured patch +13 / −37
diff --git a/contrib/epee/src/http_auth.cpp b/contrib/epee/src/http_auth.cpp
index ec430c8..5e21d5d 100644
--- a/contrib/epee/src/http_auth.cpp
+++ b/contrib/epee/src/http_auth.cpp
@@ -64,6 +64,7 @@
#include <iterator>
#include <limits>
#include <openssl/evp.h>
+#include <openssl/rand.h>
#include <tuple>
#include <type_traits>
@@ -294,13 +295,21 @@ namespace
std::array<char, 8> nc{{}};
boost::copy(out, nc.data());
+
+ std::array<uint8_t, 16> rbuf{{}};
+ if (RAND_bytes(rbuf.data(), rbuf.size()) != 1)
+ return {};
+
+ const std::string cnonce = epee::string_encoding::base64_encode(rbuf.data(), rbuf.size());
const auto response = digest(
- generate_a1(digest, user), u8":", user.server.nonce, u8":", nc, u8"::auth:", digest(method, u8":", uri)
+ generate_a1(digest, user), u8":", user.server.nonce, u8":", nc, u8":", cnonce, u8":auth:", digest(method, u8":", uri)
);
+
out.clear();
init_client_value(out, Digest::name, user, uri, response);
add_field(out, u8"qop", ceref(u8"auth"));
add_field(out, u8"nc", nc);
+ add_field(out, u8"cnonce", quoted_(cnonce));
return out;
}
@@ -590,7 +599,7 @@ namespace
boost::equals((*digest).name, request.algorithm, ascii_iequal)
);
if (request.qop.empty())
- value_generator = old_algorithm<digest_type>{*digest};
+ index = boost::fusion::size(digest_algorithms);
else
{
for (auto elem = boost::make_split_iterator(request.qop, boost::token_finder(http_list_separator));
diff --git a/tests/unit_tests/http.cpp b/tests/unit_tests/http.cpp
index 947bd3c..4ded76c 100644
--- a/tests/unit_tests/http.cpp
+++ b/tests/unit_tests/http.cpp
@@ -614,10 +614,8 @@ TEST(HTTP_Client_Auth, BadSyntax)
TEST(HTTP_Client_Auth, MD5)
{
- constexpr char method[] = "NOP";
constexpr char nonce[] = "some crazy nonce";
constexpr char realm[] = "the only realm";
- constexpr char uri[] = "/some_file";
const http::login user{"foo", "bar"};
http::http_client_auth auth{user};
@@ -636,42 +634,11 @@ TEST(HTTP_Client_Auth, MD5)
},
});
- EXPECT_EQ(http::http_client_auth::kSuccess, auth.handle_401(response));
- 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_EQ(parsed.end(), parsed.find(u8"opaque"));
- EXPECT_EQ(parsed.end(), parsed.find(u8"qop"));
- EXPECT_EQ(parsed.end(), parsed.find(u8"nc"));
- EXPECT_STREQ(u8"MD5", parsed.at(u8"algorithm").c_str());
- EXPECT_STREQ(nonce, parsed.at(u8"nonce").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());
-
- const std::string a1 = get_a1(user, parsed);
- 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":")
- );
- EXPECT_TRUE(boost::iequals(auth_code, parsed.at(u8"response")));
- {
- const auto auth_field_dup = auth.get_auth_field(method, uri);
- ASSERT_TRUE(bool(auth_field_dup));
- EXPECT_EQ(*auth_field, *auth_field_dup);
- }
-
-
- EXPECT_EQ(http::http_client_auth::kBadPassword, auth.handle_401(response));
- response.m_header_info.m_etc_fields.front().second.append(u8"," + write_fields({{u8"stale", u8"TRUE"}}));
- EXPECT_EQ(http::http_client_auth::kSuccess, auth.handle_401(response));
+ EXPECT_EQ(http::http_client_auth::kParseFailure, auth.handle_401(response));
}
TEST(HTTP_Client_Auth, MD5_auth)
{
- constexpr char cnonce[] = "";
constexpr char method[] = "NOP";
constexpr char nonce[] = "some crazy nonce";
constexpr char opaque[] = "this is the opaque";
@@ -723,7 +690,7 @@ TEST(HTTP_Client_Auth, MD5_auth)
const std::string a1 = get_a1(user, parsed);
const std::string a2 = get_a2(uri);
const std::string auth_code = md5_hex(
- boost::join(std::vector<std::string>{md5_hex(a1), nonce, nc, cnonce, u8"auth", md5_hex(a2)}, u8":")
+ boost::join(std::vector<std::string>{md5_hex(a1), nonce, nc, parsed.at(u8"cnonce"), u8"auth", md5_hex(a2)}, u8":")
);
EXPECT_TRUE(boost::iequals(auth_code, parsed.at(u8"response")));
}
Why this scored 59/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.