net/tor: improve address parsing

This commit is contained in:
jpk68
2026-05-08 15:53:25 -04:00
committed by SNeedlewoods
parent be4493d1b0
commit 0c22769a03
8 changed files with 150 additions and 8 deletions
+1 -1
View File
@@ -538,7 +538,7 @@ if (APPLE AND NOT IOS)
endif()
endif()
find_package(OpenSSL REQUIRED)
find_package(OpenSSL 1.1.1 REQUIRED)
message(STATUS "Using OpenSSL include dir at ${OPENSSL_INCLUDE_DIR}")
include_directories(${OPENSSL_INCLUDE_DIR})
if(STATIC AND NOT IOS)
+1 -1
View File
@@ -168,7 +168,7 @@ library archives (`.a`).
| CMake | 3.10 | NO | `cmake` | `cmake` | `cmake` | `cmake` | NO | |
| pkg-config | any | NO | `pkg-config` | `base-devel` | `base-devel` | `pkgconf` | NO | |
| Boost | 1.66 | NO | `libboost-all-dev` | `boost` | `boost-devel` | `boost-devel` | NO | C++ libraries |
| OpenSSL | basically any | NO | `libssl-dev` | `openssl` | `openssl-devel` | `openssl-devel` | NO | sha256 sum |
| OpenSSL | 1.1.1 | NO | `libssl-dev` | `openssl` | `openssl-devel` | `openssl-devel` | NO | sha256 sum |
| libzmq | 4.2.0 | NO | `libzmq3-dev` | `zeromq` | `zeromq-devel` | `zeromq-devel` | NO | ZeroMQ library |
| libunbound | 1.4.16 | NO | `libunbound-dev` | `unbound` | `unbound-devel` | `unbound-devel` | NO | DNS resolver |
| libsodium | ? | NO | `libsodium-dev` | `libsodium` | `libsodium-devel` | `libsodium-devel` | NO | cryptography |
+1 -1
View File
@@ -32,5 +32,5 @@ set(net_sources dandelionpp.cpp error.cpp http.cpp i2p_address.cpp parse.cpp res
monero_find_all_headers(net_headers "${CMAKE_CURRENT_SOURCE_DIR}")
monero_add_library(net ${net_sources} ${net_headers})
target_link_libraries(net common epee PkgConfig::libzmq ${Boost_ASIO_LIBRARY})
target_link_libraries(net common epee cncrypto PkgConfig::libzmq ${Boost_ASIO_LIBRARY})
+3
View File
@@ -68,6 +68,8 @@ namespace
return "Invalid/unsupported scheme was provided";
case net::error::invalid_tor_address:
return "Invalid Tor address";
case net::error::legacy_tor_address:
return "Unsupported Tor address version";
case net::error::unexpected_userinfo:
return "User or pass was provided unexpectedly";
case net::error::unsupported_address:
@@ -88,6 +90,7 @@ namespace
return std::errc::result_out_of_range;
case net::error::expected_tld:
case net::error::invalid_tor_address:
case net::error::legacy_tor_address:
default:
break;
}
+1
View File
@@ -48,6 +48,7 @@ namespace net
invalid_port, //!< Outside of 0-65535 range
invalid_scheme, //!< Provided URI scheme was unspported
invalid_tor_address,//!< Invalid base32 or length
legacy_tor_address, //!< Legacy address type; not supported
unexpected_userinfo,//!< User or pass was provided unexpectedly
unsupported_address,//!< Type not supported by `get_network_address`
+103 -4
View File
@@ -26,6 +26,8 @@
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Parts of this file are originally copyright (c) 2021-2026 SChernykh
#include "tor_address.h"
@@ -35,12 +37,19 @@
#include <cassert>
#include <cstring>
#include <limits>
#include <string_view>
#include "net/error.h"
#include "serialization/keyvalue_serialization.h"
#include "storages/portable_storage.h"
#include "string_tools_lexical.h"
#include <openssl/evp.h>
extern "C" {
#include "crypto/crypto-ops.h"
}
namespace net
{
namespace
@@ -48,6 +57,8 @@ namespace net
constexpr const char tld[] = u8".onion";
constexpr const char unknown_host[] = "<unknown tor host>";
//! Length of V1 and V2 onion addresses (in characters).
constexpr const unsigned legacy_length = 16;
constexpr const unsigned v3_length = 56;
constexpr const char base32_alphabet[] =
@@ -60,12 +71,24 @@ namespace net
host.remove_suffix(sizeof(tld) - 1);
//! \TODO v3 has checksum, base32 decoding is required to verify it
if (host.size() != v3_length)
return {net::error::invalid_tor_address};
if (host.find_first_not_of(base32_alphabet) != boost::string_ref::npos)
return {net::error::invalid_tor_address};
if (host.size() != v3_length)
return {host.size() == legacy_length
? net::error::legacy_tor_address
: net::error::invalid_tor_address};
const std::string_view tmp{host.data(), host.size()};
const auto bytes = from_onion_v3(tmp);
if (!validate_v3_onion_checksum(bytes))
return {net::error::invalid_tor_address};
ge_p3 point;
if (ge_frombytes_vartime(&point, bytes.data()) != 0)
return {net::error::invalid_tor_address};
return success();
}
@@ -199,4 +222,80 @@ namespace net
}
return out;
}
}
std::array<uint8_t, v3_onion_payload_size> from_onion_v3(const std::string_view address)
{
if (address.size() != v3_length) return {};
uint8_t buf[v3_onion_payload_size + 4] = {};
uint8_t* p = buf;
uint64_t data = 0;
uint64_t bit_size = 0;
for (size_t i = 0; i < v3_length; ++i) {
const char c = address[i];
uint64_t digit = 0;
if ('a' <= c && c <= 'z') {
digit = static_cast<uint64_t>(c - 'a');
}
else if ('A' <= c && c <= 'Z') {
digit = static_cast<uint64_t>(c - 'A');
}
else if ('2' <= c && c <= '7') {
digit = static_cast<uint64_t>(c - '2') + 26;
}
else {
return {};
}
data = (data << 5) | digit;
bit_size += 5;
while (bit_size >= 8) {
bit_size -= 8;
*(p++) = static_cast<uint8_t>(data >> bit_size);
}
}
std::array<uint8_t, v3_onion_payload_size> result{};
for (size_t i = 0; i < v3_onion_payload_size; ++i) {
result[i] = buf[i];
}
return result;
}
bool validate_v3_onion_checksum(const std::array<std::uint8_t, v3_onion_payload_size>& decoded)
{
constexpr const std::uint8_t prefix[] = ".onion checksum";
constexpr const std::uint8_t version = 3;
if (decoded[34] != version) return false;
std::array<std::uint8_t, sizeof(prefix) - 1 + v3_onion_pubkey_size + 1> hash{};
std::memcpy(hash.data(), prefix, sizeof(prefix) - 1);
std::memcpy(hash.data() + (sizeof(prefix) - 1), decoded.data(), v3_onion_pubkey_size);
hash.back() = version;
std::uint8_t digest[EVP_MAX_MD_SIZE];
unsigned int digest_len = 0;
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
if (!ctx) return false;
bool result =
EVP_DigestInit_ex(ctx, EVP_sha3_256(), nullptr) == 1 &&
EVP_DigestUpdate(ctx, hash.data(), hash.size()) == 1 &&
EVP_DigestFinal_ex(ctx, digest, &digest_len) == 1;
EVP_MD_CTX_free(ctx);
if (!result) return false;
return decoded[32] == digest[0] && decoded[33] == digest[1];
}
} // net
+28
View File
@@ -32,6 +32,7 @@
#include <boost/utility/string_ref.hpp>
#include <cstdint>
#include <string>
#include <array>
#include "common/expect.h"
#include "net/enums.h"
@@ -48,6 +49,19 @@ namespace serialization
namespace net
{
//! Length in bytes of a V3 onion address pubkey.
constexpr std::size_t v3_onion_pubkey_size = 32;
//! Length in bytes of a V3 onion address checksum.
constexpr std::size_t v3_onion_checksum_size = 2;
/**
* Length in bytes of a V3 onion address.
* 32 bytes (pubkey) + 2 (checksum) + 1 (version) = 35
*/
constexpr std::size_t v3_onion_payload_size =
v3_onion_pubkey_size + v3_onion_checksum_size + 1;
//! Tor onion address; internal format not condensed/decoded.
class tor_address
{
@@ -138,4 +152,18 @@ namespace net
{
return lhs.less(rhs);
}
/**
* @brief Decodes an onion address payload from Base32.
* @param A string of exactly 56 characters.
* @return The decoded address payload.
*/
std::array<uint8_t, v3_onion_payload_size> from_onion_v3(const std::string_view address);
/**
* @brief Validate the checksum of a V3 onion address.
* @param The decoded address payload (from Base32).
* @return Whether or not validation succeeded, as a boolean.
*/
bool validate_v3_onion_checksum(const std::array<std::uint8_t, v3_onion_payload_size>& decoded);
} // net
+12 -1
View File
@@ -76,6 +76,13 @@ namespace
"vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion";
static constexpr const char v3_onion_2[] =
"zpv4fa3szgel7vf6jdjeugizdclq2vzkelscs2bhbgnlldzzggcen3ad.onion";
static constexpr const char v3_onion_bad_checksum[] =
"wrongchecksum777777777777777777777777777777777777777777d.onion";
static constexpr const char v3_onion_bad_pubkey[] =
"civ5tgldg3yx73ytse6hvvk3nm6q3zctbqvytpszihm35b33ze73kxad.onion";
static constexpr const char v3_onion_bad_version[] =
"zpv4fa3szgel7vf6jdjeugizdclq2vzkelscs2bhbgnlldzzggcen3ac.onion";
}
TEST(tor_address, constants)
@@ -106,6 +113,10 @@ TEST(tor_address, invalid)
std::string onion{v3_onion};
onion.at(10) = 1;
EXPECT_TRUE(net::tor_address::make(onion).has_error());
EXPECT_TRUE(net::tor_address::make(v3_onion_bad_checksum).has_error());
EXPECT_TRUE(net::tor_address::make(v3_onion_bad_pubkey).has_error());
EXPECT_TRUE(net::tor_address::make(v3_onion_bad_version).has_error());
}
TEST(tor_address, unblockable_types)
@@ -426,7 +437,7 @@ TEST(get_network_address, onion)
EXPECT_EQ(net::error::invalid_tor_address, address);
address = net::get_network_address(v2_onion, 1000);
EXPECT_EQ(net::error::invalid_tor_address, address);
EXPECT_EQ(net::error::legacy_tor_address, address);
address = net::get_network_address(v3_onion, 1000);
ASSERT_TRUE(bool(address));