diff --git a/ChangeLog b/ChangeLog index 6a8ccc510f..4940d9349d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,6 @@ 2.2.0 not released + * restrict sharing files across SSL torrents * add versioned path-sanitization * reject invalid controlURL from UPnP router * deprecate file_storage::file_absolute_path() diff --git a/bindings/c/include/libtorrent_settings.h b/bindings/c/include/libtorrent_settings.h index e5286f4a2c..5218da8bdb 100644 --- a/bindings/c/include/libtorrent_settings.h +++ b/bindings/c/include/libtorrent_settings.h @@ -86,6 +86,7 @@ enum settings_tags_t { SET_APPLY_FILTER_TO_DHT, // int (0 or 1) SET_DISABLE_TRACKER_CONNECTION_REUSE, // int (0 or 1) SET_ENABLE_SMART_BAN, // int (0 or 1) + SET_ENFORCE_TORRENT_TRUST_DOMAIN, // int (0 or 1) SET_TRACKER_COMPLETION_TIMEOUT = 0x2200, // int SET_TRACKER_RECEIVE_TIMEOUT, // int SET_STOP_TRACKER_TIMEOUT, // int diff --git a/bindings/c/src/settings.cpp b/bindings/c/src/settings.cpp index 3cd13f58e6..ee4e5b0fe0 100644 --- a/bindings/c/src/settings.cpp +++ b/bindings/c/src/settings.cpp @@ -91,6 +91,7 @@ int settings_key(int const tag) case SET_APPLY_FILTER_TO_DHT: return sp::apply_filter_to_dht; case SET_DISABLE_TRACKER_CONNECTION_REUSE: return sp::disable_tracker_connection_reuse; case SET_ENABLE_SMART_BAN: return sp::enable_smart_ban; + case SET_ENFORCE_TORRENT_TRUST_DOMAIN: return sp::enforce_torrent_trust_domain; case SET_TRACKER_COMPLETION_TIMEOUT: return sp::tracker_completion_timeout; case SET_TRACKER_RECEIVE_TIMEOUT: return sp::tracker_receive_timeout; case SET_STOP_TRACKER_TIMEOUT: return sp::stop_tracker_timeout; diff --git a/docs/hunspell/libtorrent.dic b/docs/hunspell/libtorrent.dic index 732cb79299..a960f27034 100644 --- a/docs/hunspell/libtorrent.dic +++ b/docs/hunspell/libtorrent.dic @@ -688,3 +688,4 @@ LRM RLM sanitization ruleset +exfiltrate diff --git a/docs/manual.rst b/docs/manual.rst index f8e450b1e7..4b824bd253 100644 --- a/docs/manual.rst +++ b/docs/manual.rst @@ -1161,6 +1161,43 @@ torrents published by this root certificate (only if it has a "star cert"). .. _`RFC 2818`: https://www.ietf.org/rfc/rfc2818.txt +mutable torrents +---------------- + +libtorrent can link identical files between torrents that share the same +piece layout, to avoid downloading the same data twice (see +similar_torrents() and collections() on torrent_info, part of BEP 38). This +is done purely by comparing piece hashes, which by itself says nothing about +who published the data. + +For SSL torrents this matters. The point of an SSL torrent's root +certificate is to establish which publisher's peers are trusted to serve a +given piece of content. If file-linking reused file data across two SSL +torrents purely because their piece hashes happen to match, an attacker +could craft a torrent with a piece layout identical to some other, private +SSL torrent, whose root certificate the attacker does not control. Once +that crafted torrent is cross-referenced against the private one (via +similar_torrents() or a shared collection), the private torrent's file data +would end up seeded into the attacker's swarm, letting the attacker +exfiltrate content they were never issued a certificate for. + +To prevent this, an SSL torrent's root certificate identifies its *trust +domain*: its SHA-256 fingerprint, computed once when the torrent is added. +File-linking compares the trust domains of the two torrents and only +reuses file data when they're equal, never between an SSL torrent and a +non-SSL torrent, or between two SSL torrents with different root +certificates. Torrents without a root certificate share the same all-zero +trust domain and remain unrestricted among themselves, exactly as before. +This is controlled by settings_pack::enforce_torrent_trust_domain, which +is enabled by default; when disabled, file-linking reuses data purely by +piece hash, ignoring certificates entirely. + +An SSL torrent whose certificate has not been, or could not be, verified +(for example because it failed to parse) is excluded from file-linking +entirely, in either direction, rather than falling back to some default +trust domain. It also does not fall back to being treated as a non-SSL +torrent for any other purpose. + testing ------- diff --git a/include/libtorrent/aux_/resolve_links.hpp b/include/libtorrent/aux_/resolve_links.hpp index 84394a83b1..29ec8fb606 100644 --- a/include/libtorrent/aux_/resolve_links.hpp +++ b/include/libtorrent/aux_/resolve_links.hpp @@ -30,13 +30,22 @@ namespace libtorrent::aux { // in other torrents. struct TORRENT_EXTRA_EXPORT resolve_links { - explicit resolve_links(std::shared_ptr ti); - - // check to see if any files are shared with this torrent - void match( - torrent_info const& ti - , filenames const fs - , std::string const& save_path); + // trust_domain is this torrent's trust domain (see + // torrent::trust_domain()), an all-zero hash for torrents that + // aren't SSL torrents. The caller is responsible for not + // constructing a resolve_links, and not calling match(), for a + // torrent for which torrent::resolve_links_disabled() is true. + explicit resolve_links(std::shared_ptr ti, + bool enforce_trust_domain = true, + sha256_hash const& trust_domain = sha256_hash()); + + // check to see if any files are shared with this torrent. + // trust_domain is ti's owning torrent's trust domain, following the + // same convention as the constructor's. + void match(torrent_info const& ti, + filenames const fs, + std::string const& save_path, + sha256_hash const& trust_domain = sha256_hash()); aux::vector const& get_links() const& { return m_links; } @@ -63,6 +72,14 @@ namespace libtorrent::aux { // maps file root hash to file index, in m_torrent_file std::unordered_multimap m_file_roots; + + // when true, match() refuses to link files across a trust-domain + // boundary. Set from settings_pack::enforce_torrent_trust_domain + // by the caller. + bool m_enforce_trust_domain = true; + + // this torrent's trust domain (see the constructor's documentation) + sha256_hash m_trust_domain; }; #endif // TORRENT_DISABLE_MUTABLE_TORRENTS diff --git a/include/libtorrent/aux_/ssl.hpp b/include/libtorrent/aux_/ssl.hpp index aa3a04c3a5..eaf8f14d05 100644 --- a/include/libtorrent/aux_/ssl.hpp +++ b/include/libtorrent/aux_/ssl.hpp @@ -17,6 +17,7 @@ see LICENSE file. #include "libtorrent/config.hpp" #include "libtorrent/string_view.hpp" #include "libtorrent/aux_/export.hpp" +#include "libtorrent/sha1_hash.hpp" #if TORRENT_USE_SSL @@ -167,7 +168,12 @@ context_handle_type get_context_handle(stream& s) #endif } -TORRENT_EXTRA_EXPORT void set_trust_certificate(native_context_type nc, string_view pem, error_code &ec); +// sets the root certificate to trust for peer verification. Returns the +// SHA-256 fingerprint of that certificate (all-zero if TORRENT_DISABLE_MUTABLE_TORRENTS +// is defined, or on error), computed from the same parse used to set up +// trust rather than a second one; see torrent::trust_domain(). +TORRENT_EXTRA_EXPORT sha256_hash set_trust_certificate( + native_context_type nc, string_view pem, error_code& ec); TORRENT_EXTRA_EXPORT void set_server_name_callback(context_handle_type c, server_name_callback_type cb, void* arg, error_code& ec); TORRENT_EXTRA_EXPORT void set_host_name(stream_handle_type s, std::string const& name, error_code& ec); diff --git a/include/libtorrent/aux_/torrent.hpp b/include/libtorrent/aux_/torrent.hpp index d9a0daba1d..64dc9e9074 100644 --- a/include/libtorrent/aux_/torrent.hpp +++ b/include/libtorrent/aux_/torrent.hpp @@ -1343,6 +1343,38 @@ namespace libtorrent::aux { } bool is_ssl_torrent() const { return bool(m_flags & torrent_internal_flags::ssl_torrent); } + +#ifndef TORRENT_DISABLE_MUTABLE_TORRENTS + // identifies this torrent's trust domain: the SHA-256 fingerprint of + // its SSL root certificate (resolved from + // add_torrent_params::root_certificate if given, otherwise the + // certificate embedded in the .torrent file), computed once by + // init_ssl(). All-zero for torrents that aren't SSL torrents. + // resolve_links links file data between two torrents only when + // their trust domains are equal, without re-parsing either + // certificate on every comparison. Only meaningful when + // resolve_links_disabled() is false. + sha256_hash const& trust_domain() const + { + static sha256_hash const none; +#ifdef TORRENT_SSL_PEERS + return m_trust_domain ? *m_trust_domain : none; +#else + return none; +#endif + } + + // true if this torrent must not participate in aux::resolve_links, + // currently when it's an SSL torrent whose certificate has not (yet, + // or ever) been successfully verified, whether because the build + // lacks TORRENT_SSL_PEERS or the certificate failed to parse. While + // true, trust_domain() is not trustworthy and resolve_links must + // not link this torrent's storage with another torrent's. + bool resolve_links_disabled() const + { + return bool(m_flags & torrent_internal_flags::resolve_links_disabled); + } +#endif #ifdef TORRENT_SSL_PEERS void set_ssl_cert(std::string const& certificate , std::string const& private_key @@ -1543,6 +1575,14 @@ namespace libtorrent::aux { // cycle, and not in the constructor. So we need to save it here std::unique_ptr m_add_torrent_params; +#if !defined TORRENT_DISABLE_MUTABLE_TORRENTS && defined TORRENT_SSL_PEERS + // only ever assigned by init_ssl(), so this doesn't exist at all in + // builds without TORRENT_SSL_PEERS. Null for the vast majority of + // torrents that aren't SSL torrents, to avoid the extra 32 bytes per + // torrent; see trust_domain() + std::unique_ptr m_trust_domain; +#endif + // if the torrent is started without metadata, it may // still be given a name until the metadata is received // once the metadata is received this field will no diff --git a/include/libtorrent/aux_/torrent_internal_flags.hpp b/include/libtorrent/aux_/torrent_internal_flags.hpp index 8a82280c25..d2499cf982 100644 --- a/include/libtorrent/aux_/torrent_internal_flags.hpp +++ b/include/libtorrent/aux_/torrent_internal_flags.hpp @@ -67,6 +67,13 @@ namespace libtorrent::aux::torrent_internal_flags { // hashes instead, see torrent::get_smart_ban(). constexpr lt::torrent_flags_t smart_ban_enabled = 56_bit; + // set whenever this torrent must not participate in aux::resolve_links, + // i.e. it must never be linked with another torrent's storage, nor have + // another torrent linked with its own. Currently set for SSL torrents + // whose certificate hasn't (yet, or ever) been successfully verified, + // see torrent::init_ssl(). + constexpr lt::torrent_flags_t resolve_links_disabled = 57_bit; + // peer-side helpers combining the public bit with the internal bit so // "is the torrent operating in this mode right now?" is a single // flag test. diff --git a/include/libtorrent/settings_pack.hpp b/include/libtorrent/settings_pack.hpp index 116941464f..f19366412b 100644 --- a/include/libtorrent/settings_pack.hpp +++ b/include/libtorrent/settings_pack.hpp @@ -1121,6 +1121,18 @@ namespace aux { // afterwards, not ones already running. enable_smart_ban, + // when set to true (the default), mutable-torrent file linking + // (see similar_torrents() and collections() on torrent_info) + // compares the SHA-256 fingerprint of each SSL torrent's root + // certificate before reusing file data between two torrents: + // linking only happens when the fingerprints match, or between + // two torrents that both lack a certificate. This stops a + // torrent crafted with matching piece hashes, but signed by a + // different certificate authority, from pulling file data + // across an SSL torrent's trust boundary. Set to false to link + // purely by piece hash, ignoring certificates. + enforce_torrent_trust_domain, + max_bool_setting_internal }; diff --git a/src/resolve_links.cpp b/src/resolve_links.cpp index 25f93ce054..9df709b857 100644 --- a/src/resolve_links.cpp +++ b/src/resolve_links.cpp @@ -15,8 +15,13 @@ see LICENSE file. namespace libtorrent::aux { #ifndef TORRENT_DISABLE_MUTABLE_TORRENTS -resolve_links::resolve_links(std::shared_ptr ti) + +resolve_links::resolve_links(std::shared_ptr ti, + bool const enforce_trust_domain, + sha256_hash const& trust_domain) : m_torrent_file(std::move(ti)) + , m_enforce_trust_domain(enforce_trust_domain) + , m_trust_domain(trust_domain) { TORRENT_ASSERT(m_torrent_file); @@ -45,8 +50,18 @@ resolve_links::resolve_links(std::shared_ptr ti) m_links.resize(m_torrent_file->num_files()); } -void resolve_links::match(torrent_info const& ti, filenames const fs, std::string const& save_path) +void resolve_links::match(torrent_info const& ti, + filenames const fs, + std::string const& save_path, + sha256_hash const& trust_domain) { + // an SSL torrent's data must not end up seeded into a swarm trusting a + // different certificate authority just because the piece hashes match. + // Torrents that aren't SSL torrents share the same all-zero trust + // domain and are unrestricted by this check. + if (m_enforce_trust_domain && m_trust_domain != trust_domain) + return; + if (m_torrent_file->v2() && ti.v2()) { match_v2(fs, save_path); diff --git a/src/settings_pack.cpp b/src/settings_pack.cpp index 03e2f20af0..031cd4f2e0 100644 --- a/src/settings_pack.cpp +++ b/src/settings_pack.cpp @@ -251,6 +251,7 @@ namespace { SET(apply_filter_to_dht, true, nullptr), SET(disable_tracker_connection_reuse, false, nullptr), SET(enable_smart_ban, true, nullptr), + SET(enforce_torrent_trust_domain, true, nullptr), }}); CONSTEXPR_SETTINGS diff --git a/src/ssl.cpp b/src/ssl.cpp index 1fd23762c0..1da787bd85 100644 --- a/src/ssl.cpp +++ b/src/ssl.cpp @@ -14,8 +14,14 @@ see LICENSE file. #if TORRENT_USE_SSL +#include +#include "libtorrent/aux_/scope_end.hpp" + #ifdef TORRENT_USE_OPENSSL #include // for GENERAL_NAME +#ifndef TORRENT_DISABLE_MUTABLE_TORRENTS +#include // for EVP_sha256, EVP_MAX_MD_SIZE +#endif #endif #ifdef TORRENT_USE_GNUTLS @@ -24,7 +30,7 @@ see LICENSE file. namespace libtorrent::aux::ssl { -void set_trust_certificate(native_context_type nc, string_view pem, error_code &ec) +sha256_hash set_trust_certificate(native_context_type nc, string_view pem, error_code& ec) { #if defined TORRENT_USE_OPENSSL // create a new X.509 certificate store @@ -32,7 +38,7 @@ void set_trust_certificate(native_context_type nc, string_view pem, error_code & if (!cert_store) { ec = error_code(int(ERR_get_error()), error::get_ssl_category()); - return; + return {}; } // wrap the PEM certificate in a BIO, for openssl to read @@ -48,25 +54,78 @@ void set_trust_certificate(native_context_type nc, string_view pem, error_code & { X509_STORE_free(cert_store); ec = error_code(int(ERR_get_error()), error::get_ssl_category()); - return; + return {}; } + auto se = aux::scope_end([&] { X509_free(cert); }); // add cert to cert_store X509_STORE_add_cert(cert_store, cert); - X509_free(cert); - // and lastly, replace the default cert store with ours + // and replace the default cert store with ours SSL_CTX_set_cert_store(nc, cert_store); + sha256_hash fingerprint; +#ifndef TORRENT_DISABLE_MUTABLE_TORRENTS + // fingerprint the same parsed certificate used for the trust store, + // rather than re-parsing the PEM text + std::array md; + unsigned int md_len = 0; + if (X509_digest(cert, EVP_sha256(), md.data(), &md_len) != 1) + { + ec = error_code(int(ERR_get_error()), error::get_ssl_category()); + return {}; + } + TORRENT_ASSERT(md_len == static_cast(sha256_hash::size())); + fingerprint.assign(reinterpret_cast(md.data())); +#endif + return fingerprint; + #elif defined TORRENT_USE_GNUTLS - gnutls_datum_t ca; - ca.data = reinterpret_cast(const_cast(pem.data())); - ca.size = unsigned(pem.size()); + gnutls_datum_t ca; + ca.data = reinterpret_cast(const_cast(pem.data())); + ca.size = unsigned(pem.size()); + + // parse a single certificate, rather than using + // gnutls_certificate_set_x509_trust_mem() directly, so the certificate + // that's trusted is the exact same one that's fingerprinted below. A + // PEM blob containing more than one certificate only has its first + // certificate trusted, matching the OpenSSL branch above. + gnutls_x509_crt_t crt; + int ret = gnutls_x509_crt_init(&crt); + if (ret < 0) + { + ec = error_code(ret, error::get_ssl_category()); + return {}; + } + auto se = aux::scope_end([&] { gnutls_x509_crt_deinit(crt); }); - // Warning: returns the number of certificates processed or a negative error code on error - int ret = gnutls_certificate_set_x509_trust_mem(nc, &ca, GNUTLS_X509_FMT_PEM); - if(ret < 0) + ret = gnutls_x509_crt_import(crt, &ca, GNUTLS_X509_FMT_PEM); + if (ret < 0) + { ec = error_code(ret, error::get_ssl_category()); + return {}; + } + + ret = gnutls_certificate_set_x509_trust(nc, &crt, 1); + if (ret < 0) + { + ec = error_code(ret, error::get_ssl_category()); + return {}; + } + + sha256_hash fingerprint; +#ifndef TORRENT_DISABLE_MUTABLE_TORRENTS + std::array buf; + size_t buf_size = buf.size(); + ret = gnutls_x509_crt_get_fingerprint(crt, GNUTLS_DIG_SHA256, buf.data(), &buf_size); + if (ret < 0) + { + ec = error_code(ret, error::get_ssl_category()); + return {}; + } + fingerprint.assign(reinterpret_cast(buf.data())); +#endif + return fingerprint; #endif } diff --git a/src/torrent.cpp b/src/torrent.cpp index bdd60eaa37..f059902729 100644 --- a/src/torrent.cpp +++ b/src/torrent.cpp @@ -573,6 +573,9 @@ aux::vector file_to_piece_prio( update_want_tick(); + bool const has_root_cert = + m_add_torrent_params && !m_add_torrent_params->root_certificate.empty(); + // Some of these calls may log to the torrent debug log, which requires a // call to get_handle(), which requires the torrent object to be fully // constructed, as it relies on get_shared_from_this() @@ -606,12 +609,18 @@ aux::vector file_to_piece_prio( do_connect_boost(); } - if (!p.root_certificate.empty()) + if (has_root_cert) { m_flags |= torrent_internal_flags::ssl_torrent; -#ifdef TORRENT_SSL_PEERS - init_ssl(p.root_certificate); +#ifndef TORRENT_DISABLE_MUTABLE_TORRENTS + // cleared by init_ssl() below once the certificate is + // verified; see resolve_links + m_flags |= torrent_internal_flags::resolve_links_disabled; #endif + // the init_ssl() call itself is deferred until after + // update_state_list() below: it may fail and call pause(), + // whose invariant check requires this torrent's state-list + // membership to already be established } #ifndef TORRENT_DISABLE_LOGGING @@ -684,6 +693,13 @@ aux::vector file_to_piece_prio( update_want_scrape(); update_state_list(); +#ifdef TORRENT_SSL_PEERS + if (has_root_cert) + { + init_ssl(m_add_torrent_params->root_certificate); + } +#endif + if (m_torrent_file->is_valid()) { init(); @@ -1838,8 +1854,14 @@ aux::vector file_to_piece_prio( return; } - // set the root certificate as trust + // set the root certificate as trust; this also fingerprints the + // certificate for resolve_links, see trust_domain() +#ifndef TORRENT_DISABLE_MUTABLE_TORRENTS + m_trust_domain = std::make_unique( + ssl::set_trust_certificate(ctx->native_handle(), cert, ec)); +#else ssl::set_trust_certificate(ctx->native_handle(), cert, ec); +#endif if (ec) { set_error(ec, torrent_status::error_file_ssl_ctx); @@ -1858,6 +1880,11 @@ aux::vector file_to_piece_prio( // if all went well, set the torrent ssl context to this one m_ssl_ctx = std::move(ctx); +#ifndef TORRENT_DISABLE_MUTABLE_TORRENTS + // the certificate is now verified and the fingerprint above is + // trustworthy; resolve_links may consider this torrent + m_flags &= ~torrent_internal_flags::resolve_links_disabled; +#endif // tell the client we need a cert for this torrent alerts().emplace_alert(get_handle()); } @@ -1936,10 +1963,20 @@ aux::vector file_to_piece_prio( if (!is_ssl_torrent() && !cert.empty()) { m_flags |= torrent_internal_flags::ssl_torrent; +#ifndef TORRENT_DISABLE_MUTABLE_TORRENTS + m_flags |= torrent_internal_flags::resolve_links_disabled; +#endif #ifdef TORRENT_SSL_PEERS init_ssl(cert); #endif } +#ifdef TORRENT_SSL_PEERS + // covers both a certificate that just failed to verify above, and + // one that failed earlier in start(), which left the flag already + // set so the block above was skipped + if (is_ssl_torrent() && m_error) + return; +#endif if (m_torrent_file->num_pieces() > piece_picker::max_pieces) { @@ -2073,15 +2110,21 @@ aux::vector file_to_piece_prio( aux::vector links; #ifndef TORRENT_DISABLE_MUTABLE_TORRENTS - if (!m_torrent_file->similar_torrents().empty() - || !m_torrent_file->collections().empty()) + if (!resolve_links_disabled() + && (!m_torrent_file->similar_torrents().empty() + || !m_torrent_file->collections().empty())) { - resolve_links res(m_torrent_file); + TORRENT_ASSERT(!resolve_links_disabled()); + resolve_links res(m_torrent_file, + settings().get_bool(settings_pack::enforce_torrent_trust_domain), + trust_domain()); for (auto const& ih : m_torrent_file->similar_torrents()) { std::shared_ptr t = m_ses.find_torrent(info_hash_t(ih)).lock(); if (!t) continue; + if (t->resolve_links_disabled()) + continue; // Only attempt to reuse files from torrents that are seeding. // TODO: this could be optimized by looking up which files are @@ -2089,7 +2132,11 @@ aux::vector file_to_piece_prio( if (!t->is_seed()) continue; auto ti = t->get_torrent_file(); - res.match(*ti, filenames(ti->layout(), t->m_renamed_files), t->save_path()); + TORRENT_ASSERT(!t->resolve_links_disabled()); + res.match(*ti, + filenames(ti->layout(), t->m_renamed_files), + t->save_path(), + t->trust_domain()); } for (auto const& c : m_torrent_file->collections()) { @@ -2097,13 +2144,20 @@ aux::vector file_to_piece_prio( for (auto const& t : ts) { + if (t->resolve_links_disabled()) + continue; + // Only attempt to reuse files from torrents that are seeding. // TODO: this could be optimized by looking up which files are // complete and just look at those if (!t->is_seed()) continue; auto ti = t->get_torrent_file(); - res.match(*ti, filenames(ti->layout(), t->m_renamed_files), t->save_path()); + TORRENT_ASSERT(!t->resolve_links_disabled()); + res.match(*ti, + filenames(ti->layout(), t->m_renamed_files), + t->save_path(), + t->trust_domain()); } } diff --git a/test/test_resolve_links.cpp b/test/test_resolve_links.cpp index 1f93bbd7f8..09862fd2e8 100644 --- a/test/test_resolve_links.cpp +++ b/test/test_resolve_links.cpp @@ -17,6 +17,7 @@ see LICENSE file. #include "libtorrent/torrent_info.hpp" #include "libtorrent/aux_/resolve_links.hpp" +#include "libtorrent/aux_/torrent.hpp" #include "libtorrent/aux_/path.hpp" // for combine_path #include "libtorrent/hex.hpp" // to_hex #include "libtorrent/create_torrent.hpp" @@ -28,6 +29,7 @@ see LICENSE file. #include "settings.hpp" #include +#include using namespace lt; using namespace std::placeholders; @@ -159,6 +161,116 @@ TORRENT_TEST(range_lookup_duplicated_files) TEST_EQUAL(num_matches, 1); } +namespace { + +std::vector single_file_1024() +{ + std::vector fs; + fs.emplace_back("test_resolve_links_dir/tmp1", 1024); + return fs; +} + +std::size_t count_matches(aux::resolve_links const& l) +{ + aux::vector const& links = l.get_links(); + return std::size_t(std::count_if( + links.begin(), links.end(), [](std::string const& link) { return !link.empty(); })); +} + +struct trust_domain_test_t +{ + sha256_hash fingerprint1; + sha256_hash fingerprint2; + bool enforce_trust_domain; + std::size_t expected_matches; +}; + +} // anonymous namespace + +// resolve_links treats the SSL fingerprint as an opaque hash: it only +// compares fingerprint1 against fingerprint2 for equality, never re-derives +// either from a certificate. So this exercises that comparison directly, +// with arbitrary distinct hashes, rather than through real certificates and +// ssl::set_trust_certificate() (that parsing/fingerprinting behavior is +// covered separately, see ssl_cert_fingerprint below). +TORRENT_TEST(resolve_links_trust_domain) +{ + sha256_hash const trust_domain_a = sha256_hash::max(); + sha256_hash const trust_domain_b = sha256_hash("01234567890123456789012345678901"); + + std::vector const trust_domain_tests = { + // same trust domain: an SSL torrent's files may be linked from + // another SSL torrent trusting the same certificate authority + {trust_domain_a, trust_domain_a, true, 1}, + + // different trust domain: matching piece hashes must not link file + // data across an SSL torrent's trust boundary + {trust_domain_a, trust_domain_b, true, 0}, + + // settings_pack::enforce_torrent_trust_domain = false links + // regardless of trust domain + {trust_domain_a, trust_domain_b, false, 1}, + }; + + for (trust_domain_test_t const& e : trust_domain_tests) + { + lt::create_torrent t1(single_file_1024(), 1024, lt::create_torrent::v1_only); + lt::create_torrent t2(single_file_1024(), 1024, lt::create_torrent::v1_only); + t1.set_hash(0_piece, sha1_hash::max()); + t2.set_hash(0_piece, sha1_hash::max()); + + auto ti1 = load_torrent_buffer(bencode(t1.generate())).ti; + auto atp2 = load_torrent_buffer(bencode(t2.generate())); + renamed_files rf; + rf.import_filenames(atp2.ti->layout(), atp2.renamed_files); + + aux::resolve_links l(ti1, e.enforce_trust_domain, e.fingerprint1); + l.match(*atp2.ti, filenames(atp2.ti->layout(), rf), ".", e.fingerprint2); + + TEST_EQUAL(count_matches(l), e.expected_matches); + } +} + +#ifdef TORRENT_SSL_PEERS + +// an SSL torrent whose certificate fails to parse must not be silently +// treated as a plain (non-SSL) torrent: is_ssl_torrent() must stay true (so +// it doesn't fall back to un-encrypted peer connections), and it must be +// excluded from resolve_links (so its files can never be linked with +// another torrent's storage based on the resulting all-zero fingerprint). +TORRENT_TEST(ssl_torrent_invalid_certificate) +{ + lt::create_torrent t(single_file_1024(), 1024, lt::create_torrent::v1_only); + t.set_hash(0_piece, sha1_hash::max()); + + add_torrent_params atp = load_torrent_buffer(bencode(t.generate())); + atp.save_path = "."; + atp.root_certificate = "not a certificate"; + + lt::session ses(settings()); + torrent_handle const h = ses.add_torrent(atp); + auto const tor = h.native_handle(); + + // session_handle::add_torrent() is a synchronous call into the network + // thread, so torrent::start() (and its init_ssl() call) has already run + // by the time it returns; this just marshals the read of internal state + // onto the network thread, it isn't waiting for anything to happen + std::promise> result; + post(ses.get_context(), [tor, &result] { + result.set_value({tor->is_ssl_torrent(), tor->resolve_links_disabled()}); + }); + auto const [is_ssl, disabled] = result.get_future().get(); + + TEST_CHECK(is_ssl); + TEST_CHECK(disabled); + + torrent_status const st = h.status(); + TEST_CHECK(st.errc); + TEST_CHECK(st.error_file == torrent_status::error_file_ssl_ctx); +} + +#endif // TORRENT_SSL_PEERS + TORRENT_TEST(pick_up_existing_file) { lt::session ses(settings()); diff --git a/test/test_ssl.cpp b/test/test_ssl.cpp index db7b109385..a98066cd4f 100644 --- a/test/test_ssl.cpp +++ b/test/test_ssl.cpp @@ -33,6 +33,7 @@ see LICENSE file. #include #include #include +#include #include #ifdef TORRENT_UTP_LOG_ENABLE @@ -583,6 +584,16 @@ void test_malicious_peer() } +// reads a fixture certificate from test/ssl/ +std::string read_test_ssl_file(std::string const& name) +{ + std::string const path = combine_path("..", combine_path("ssl", name)); + std::ifstream f(path); + std::stringstream buf; + buf << f.rdbuf(); + return buf.str(); +} + void test_ssl_magnet(bool const seed_has_cert) { error_code ec; @@ -655,10 +666,7 @@ void test_ssl_magnet(bool const seed_has_cert) addp2.flags &= ~torrent_flags::paused; addp2.flags &= ~torrent_flags::auto_managed; - std::ifstream rootf(combine_path("..", combine_path("ssl", "root_ca_cert.pem"))); - std::stringstream buffer; - buffer << rootf.rdbuf(); - addp2.root_certificate = buffer.str(); + addp2.root_certificate = read_test_ssl_file("root_ca_cert.pem"); torrent_handle tor2 = ses2.add_torrent(addp2, ec); @@ -702,10 +710,62 @@ void test_ssl_magnet(bool const seed_has_cert) } } +// the fixture .pem files are "openssl x509 -text" dumps: a human-readable +// description of the certificate precedes the actual PEM block. Returns just +// the PEM block, a differently-formatted (but equivalent) encoding of the +// same certificate. +std::string strip_pem_preamble(std::string const& pem) +{ + auto const pos = pem.find("-----BEGIN CERTIFICATE-----"); + TEST_CHECK(pos != std::string::npos); + return pem.substr(pos); +} +// mirrors what torrent::init_ssl() does: set up a throwaway trust store just +// to get at the fingerprint set_trust_certificate() computes as a side effect +sha256_hash fingerprint_cert(std::string const& pem) +{ + aux::ssl::context ctx(aux::ssl::context::tls); + error_code ec; + sha256_hash const fp = aux::ssl::set_trust_certificate(ctx.native_handle(), pem, ec); + if (ec) + { + std::printf("set_trust_certificate() failed: %s (pem size: %d)\n", + ec.message().c_str(), + int(pem.size())); + TEST_CHECK(!ec); + } + return fp; +} } // anonymous namespace +TORRENT_TEST(ssl_cert_fingerprint) +{ + std::string const cert_a = read_test_ssl_file("root_ca_cert.pem"); + std::string const cert_b = read_test_ssl_file("server.pem"); + + // deterministic: the same certificate fingerprints the same way twice + TEST_CHECK(fingerprint_cert(cert_a) == fingerprint_cert(cert_a)); + + // PEM formatting (here, a human-readable text dump preceding the actual + // PEM block) does not affect the fingerprint, since it's derived from + // the parsed certificate's DER encoding, not the raw PEM text + TEST_CHECK(fingerprint_cert(cert_a) == fingerprint_cert(strip_pem_preamble(cert_a))); + + // different certificates fingerprint differently + TEST_CHECK(fingerprint_cert(cert_a) != fingerprint_cert(cert_b)); + + // invalid input is reported as an error, with an all-zero fingerprint, + // not a crash or a bogus hash + aux::ssl::context ctx(aux::ssl::context::tls); + error_code ec; + sha256_hash const fp = + aux::ssl::set_trust_certificate(ctx.native_handle(), "not a certificate", ec); + TEST_CHECK(ec); + TEST_CHECK(fp.is_all_zeros()); +} + TORRENT_TEST(malicious_peer) { test_malicious_peer(); diff --git a/test/test_torrent.cpp b/test/test_torrent.cpp index df4bbb88c4..3232f90fe3 100644 --- a/test/test_torrent.cpp +++ b/test/test_torrent.cpp @@ -23,7 +23,7 @@ see LICENSE file. #include "libtorrent/aux_/session_impl.hpp" #include "libtorrent/peer_info.hpp" #include "libtorrent/extensions.hpp" -#include "libtorrent/aux_/path.hpp" // for combine_path, current_working_directory +#include "libtorrent/aux_/path.hpp" // for combine_path, current_path #include "libtorrent/magnet_uri.hpp" #include "libtorrent/announce_entry.hpp" #include "libtorrent/span.hpp"