Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
2.2.0 not released

* restrict sharing files across SSL torrents
* reject invalid controlURL from UPnP router
* deprecate file_storage::file_absolute_path()
* avoid copying file path strings out of the info-section
Expand Down
1 change: 1 addition & 0 deletions bindings/c/include/libtorrent_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions bindings/c/src/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions docs/hunspell/libtorrent.dic
Original file line number Diff line number Diff line change
Expand Up @@ -677,3 +677,4 @@ sl
hasher256
ffff
RFC1918
exfiltrate
37 changes: 37 additions & 0 deletions docs/manual.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------

Expand Down
31 changes: 24 additions & 7 deletions include/libtorrent/aux_/resolve_links.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,22 @@ namespace libtorrent::aux {
// in other torrents.
struct TORRENT_EXTRA_EXPORT resolve_links
{
explicit resolve_links(std::shared_ptr<torrent_info const> 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<torrent_info const> 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<std::string, file_index_t> const& get_links() const&
{ return m_links; }
Expand All @@ -63,6 +72,14 @@ namespace libtorrent::aux {

// maps file root hash to file index, in m_torrent_file
std::unordered_multimap<sha256_hash, file_index_t> 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

Expand Down
8 changes: 7 additions & 1 deletion include/libtorrent/aux_/ssl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -167,7 +168,12 @@ context_handle_type get_context_handle(stream<T>& 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);
Expand Down
40 changes: 40 additions & 0 deletions include/libtorrent/aux_/torrent.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1335,6 +1335,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
Expand Down Expand Up @@ -1535,6 +1567,14 @@ namespace libtorrent::aux {
// cycle, and not in the constructor. So we need to save it here
std::unique_ptr<add_torrent_params> 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<sha256_hash> 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
Expand Down
7 changes: 7 additions & 0 deletions include/libtorrent/aux_/torrent_internal_flags.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions include/libtorrent/settings_pack.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down
19 changes: 17 additions & 2 deletions src/resolve_links.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ see LICENSE file.
namespace libtorrent::aux {

#ifndef TORRENT_DISABLE_MUTABLE_TORRENTS
resolve_links::resolve_links(std::shared_ptr<torrent_info const> ti)

resolve_links::resolve_links(std::shared_ptr<torrent_info const> 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);

Expand Down Expand Up @@ -45,8 +50,18 @@ resolve_links::resolve_links(std::shared_ptr<torrent_info const> 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);
Expand Down
1 change: 1 addition & 0 deletions src/settings_pack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 70 additions & 11 deletions src/ssl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ see LICENSE file.

#if TORRENT_USE_SSL

#include <array>
#include "libtorrent/aux_/scope_end.hpp"

#ifdef TORRENT_USE_OPENSSL
#include <openssl/x509v3.h> // for GENERAL_NAME
#ifndef TORRENT_DISABLE_MUTABLE_TORRENTS
#include <openssl/evp.h> // for EVP_sha256, EVP_MAX_MD_SIZE
#endif
#endif

#ifdef TORRENT_USE_GNUTLS
Expand All @@ -24,15 +30,15 @@ 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
X509_STORE* cert_store = X509_STORE_new();
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
Expand All @@ -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<unsigned char, EVP_MAX_MD_SIZE> 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<unsigned int>(sha256_hash::size()));
fingerprint.assign(reinterpret_cast<char const*>(md.data()));
#endif
return fingerprint;

#elif defined TORRENT_USE_GNUTLS
gnutls_datum_t ca;
ca.data = reinterpret_cast<unsigned char*>(const_cast<char*>(pem.data()));
ca.size = unsigned(pem.size());
gnutls_datum_t ca;
ca.data = reinterpret_cast<unsigned char*>(const_cast<char*>(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<unsigned char, 32> 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<char const*>(buf.data()));
#endif
return fingerprint;
#endif
}

Expand Down
Loading
Loading