diff --git a/CMakeLists.txt b/CMakeLists.txt index 389b5ccdc..2b0aedbdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md index b4b5213bc..54c9140e0 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/src/net/CMakeLists.txt b/src/net/CMakeLists.txt index 7b95a0650..204420a2f 100644 --- a/src/net/CMakeLists.txt +++ b/src/net/CMakeLists.txt @@ -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}) diff --git a/src/net/error.cpp b/src/net/error.cpp index f59bb520b..41f0ffd70 100644 --- a/src/net/error.cpp +++ b/src/net/error.cpp @@ -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; } diff --git a/src/net/error.h b/src/net/error.h index 1f1380a73..8e87db6f8 100644 --- a/src/net/error.h +++ b/src/net/error.h @@ -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` diff --git a/src/net/tor_address.cpp b/src/net/tor_address.cpp index e9f1b7a97..0697bc1cb 100644 --- a/src/net/tor_address.cpp +++ b/src/net/tor_address.cpp @@ -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 #include #include +#include #include "net/error.h" #include "serialization/keyvalue_serialization.h" #include "storages/portable_storage.h" #include "string_tools_lexical.h" +#include + +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[] = ""; + //! 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 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(c - 'a'); + } + else if ('A' <= c && c <= 'Z') { + digit = static_cast(c - 'A'); + } + else if ('2' <= c && c <= '7') { + digit = static_cast(c - '2') + 26; + } + else { + return {}; + } + + data = (data << 5) | digit; + bit_size += 5; + + while (bit_size >= 8) { + bit_size -= 8; + *(p++) = static_cast(data >> bit_size); + } + } + + std::array 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& decoded) + { + constexpr const std::uint8_t prefix[] = ".onion checksum"; + constexpr const std::uint8_t version = 3; + + if (decoded[34] != version) return false; + + std::array 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 diff --git a/src/net/tor_address.h b/src/net/tor_address.h index 2c6320871..c01e98a88 100644 --- a/src/net/tor_address.h +++ b/src/net/tor_address.h @@ -32,6 +32,7 @@ #include #include #include +#include #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 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& decoded); } // net diff --git a/tests/unit_tests/net.cpp b/tests/unit_tests/net.cpp index acb3b95bf..4f4399eac 100644 --- a/tests/unit_tests/net.cpp +++ b/tests/unit_tests/net.cpp @@ -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));