mirror of
https://github.com/arvidn/libtorrent.git
synced 2026-07-29 07:14:08 -04:00
291 lines
9.7 KiB
C++
291 lines
9.7 KiB
C++
/*
|
|
|
|
Copyright (c) 2026, Arvid Norberg
|
|
All rights reserved.
|
|
|
|
You may use, distribute and modify this code under the terms of the BSD license,
|
|
see LICENSE file.
|
|
*/
|
|
|
|
// this test suite is only meaningful when built with TLS support, since it
|
|
// exercises sim::http_server's https flag and the certificate/hostname/
|
|
// protocol validation an HTTPS client is expected to perform.
|
|
#include "libtorrent/config.hpp"
|
|
#include "test.hpp"
|
|
|
|
#if TORRENT_USE_SSL
|
|
|
|
#include "simulator/simulator.hpp"
|
|
#include "http_server.hpp"
|
|
#include "libtorrent/aux_/http_connection.hpp"
|
|
#include "libtorrent/aux_/resolver.hpp"
|
|
#include "libtorrent/aux_/ssl.hpp"
|
|
|
|
#include "make_proxy_settings.hpp"
|
|
|
|
using namespace lt;
|
|
using namespace sim;
|
|
|
|
namespace {
|
|
|
|
// "test-hostname.com" is used by the hostname/CN tests: it's the name
|
|
// baked into test/ssl/hostname_cert.pem's subject and SAN.
|
|
struct sim_config : sim::default_config
|
|
{
|
|
chrono::high_resolution_clock::duration hostname_lookup(asio::ip::address const&,
|
|
std::string hostname,
|
|
std::vector<asio::ip::address>& result,
|
|
boost::system::error_code& ec) override
|
|
{
|
|
if (hostname == "test-hostname.com")
|
|
{
|
|
result.push_back(make_address_v4("10.0.0.2"));
|
|
return chrono::milliseconds(100);
|
|
}
|
|
return default_config::hostname_lookup({}, hostname, result, ec);
|
|
}
|
|
};
|
|
|
|
// bundles the sim/server/resolver/client-ssl-context scaffolding shared by
|
|
// every test below, and registers the "/test_file" 200-OK handler they
|
|
// all rely on. client_trust_file, if non-empty, is loaded as the client's
|
|
// trust anchor (left empty, ssl_ctx keeps an empty trust store); the
|
|
// client always starts out in verify_peer, since only one test needs
|
|
// verify_none, and that test overrides ssl_ctx itself afterward -- same
|
|
// as the other test-specific tweaks (a restricted protocol range, a fake
|
|
// verification time) that don't fit here because they only apply to one
|
|
// or two tests.
|
|
struct https_fixture
|
|
{
|
|
explicit https_fixture(std::string cert_file = "server.pem",
|
|
std::string key_file = "server.pem",
|
|
std::string client_trust_file = "",
|
|
std::function<void(lt::aux::ssl::context&)> ssl_setup = nullptr)
|
|
: sim(network_cfg)
|
|
, web_server(sim, make_address_v4("10.0.0.2"))
|
|
, client_ios(sim, make_address_v4("10.0.0.1"))
|
|
, res(client_ios)
|
|
, http(web_server,
|
|
8080,
|
|
sim::http_server::https,
|
|
std::move(cert_file),
|
|
std::move(key_file),
|
|
std::move(ssl_setup))
|
|
, ssl_ctx(lt::aux::ssl::context::sslv23_client)
|
|
{
|
|
http.register_handler(
|
|
"/test_file", [](std::string, std::string, std::map<std::string, std::string>&) {
|
|
return sim::send_response(200, "OK", 0);
|
|
});
|
|
|
|
ssl_ctx.set_verify_mode(lt::aux::ssl::context::verify_peer);
|
|
if (!client_trust_file.empty())
|
|
{
|
|
error_code ec;
|
|
ssl_ctx.load_verify_file(ssl_fixture_path(client_trust_file), ec);
|
|
TEST_CHECK(!ec);
|
|
}
|
|
}
|
|
|
|
// starts a single GET request (with no proxy) over ssl_ctx. The
|
|
// outcome is recorded into connected/result_ec/handler_called, which
|
|
// the caller inspects after sim.run(). conn is kept alive as a member
|
|
// (rather than returned) since it must outlive this call, through
|
|
// sim.run(), for as long as the fixture itself does.
|
|
void attempt(std::string const& url)
|
|
{
|
|
lt::aux::proxy_settings const ps = make_proxy_settings(settings_pack::none);
|
|
|
|
conn = std::make_shared<lt::aux::http_connection>(
|
|
client_ios,
|
|
res,
|
|
[this, url](error_code const& ec,
|
|
lt::aux::http_parser const&,
|
|
span<char const>,
|
|
lt::aux::http_connection&) {
|
|
++handler_called;
|
|
result_ec = ec;
|
|
std::printf("RESPONSE: %s (%s)\n", url.c_str(), ec.message().c_str());
|
|
},
|
|
1024 * 1024,
|
|
[this, url](lt::aux::http_connection&) {
|
|
connected = true;
|
|
std::printf("CONNECTED: %s\n", url.c_str());
|
|
},
|
|
lt::aux::http_filter_handler(),
|
|
lt::aux::hostname_filter_handler(),
|
|
&ssl_ctx);
|
|
|
|
conn->get(url,
|
|
seconds(1),
|
|
&ps,
|
|
5,
|
|
"test/user-agent",
|
|
std::nullopt,
|
|
lt::aux::resolver_flags{},
|
|
std::string(),
|
|
#if TORRENT_USE_I2P
|
|
nullptr,
|
|
#endif
|
|
false);
|
|
}
|
|
|
|
sim_config network_cfg;
|
|
sim::simulation sim;
|
|
sim::asio::io_context web_server;
|
|
sim::asio::io_context client_ios;
|
|
lt::aux::resolver res;
|
|
sim::http_server http;
|
|
lt::aux::ssl::context ssl_ctx;
|
|
|
|
std::shared_ptr<lt::aux::http_connection> conn;
|
|
bool connected = false;
|
|
error_code result_ec;
|
|
int handler_called = 0;
|
|
};
|
|
|
|
} // anonymous namespace
|
|
|
|
// a well-configured client (trusting the server's cert directly, and
|
|
// requesting the hostname the cert actually covers) must succeed. This is
|
|
// the positive control for the negative tests below: it proves rejection in
|
|
// those tests comes from the specific defect under test, not from some
|
|
// unrelated misconfiguration of the harness itself.
|
|
TORRENT_TEST(https_accept_valid_connection)
|
|
{
|
|
https_fixture f("hostname_cert.pem", "hostname_key.pem", "hostname_cert.pem");
|
|
f.attempt("https://test-hostname.com:8080/test_file");
|
|
f.sim.run();
|
|
|
|
TEST_EQUAL(f.handler_called, 1);
|
|
TEST_CHECK(f.connected);
|
|
TEST_CHECK(!f.result_ec);
|
|
}
|
|
|
|
// the server presents a self-signed certificate that isn't signed by any
|
|
// CA the client trusts (the client's trust store is empty, as if the
|
|
// server's certificate came from an unknown/unrecognized issuer).
|
|
TORRENT_TEST(https_reject_unknown_root_cert)
|
|
{
|
|
https_fixture f;
|
|
f.attempt("https://10.0.0.2:8080/test_file");
|
|
f.sim.run();
|
|
|
|
TEST_EQUAL(f.handler_called, 1);
|
|
TEST_CHECK(!f.connected);
|
|
TEST_CHECK(is_ssl_error(f.result_ec));
|
|
}
|
|
|
|
// the server's certificate does not validate against the CA the client
|
|
// trusts (test/ssl/invalid_peer_certificate.pem shares its issuer's name
|
|
// with test/ssl/root_ca_cert.pem, but was not actually signed by it, and is
|
|
// also expired).
|
|
TORRENT_TEST(https_reject_invalid_certificate)
|
|
{
|
|
https_fixture f("invalid_peer_certificate.pem",
|
|
"invalid_peer_private_key.pem",
|
|
"root_ca_cert.pem",
|
|
[](lt::aux::ssl::context& ctx) {
|
|
// invalid_peer_private_key.pem is encrypted (passphrase "test",
|
|
// see test/ssl/regenerate_test_certificate.sh)
|
|
error_code ec;
|
|
ctx.set_password_callback(
|
|
[](std::size_t, lt::aux::ssl::context::password_purpose) { return "test"; }, ec);
|
|
TEST_CHECK(!ec);
|
|
});
|
|
f.attempt("https://10.0.0.2:8080/test_file");
|
|
f.sim.run();
|
|
|
|
TEST_EQUAL(f.handler_called, 1);
|
|
TEST_CHECK(!f.connected);
|
|
TEST_CHECK(is_ssl_error(f.result_ec));
|
|
}
|
|
|
|
// the certificate chains to a CA the client trusts (it trusts the exact
|
|
// self-signed certificate the server presents), but the requested hostname
|
|
// isn't the one the certificate covers, a raw IP address, rather than
|
|
// test-hostname.com (the certificate's CN/SAN).
|
|
TORRENT_TEST(https_reject_hostname_mismatch)
|
|
{
|
|
https_fixture f("hostname_cert.pem", "hostname_key.pem", "hostname_cert.pem");
|
|
f.attempt("https://10.0.0.2:8080/test_file");
|
|
f.sim.run();
|
|
|
|
TEST_EQUAL(f.handler_called, 1);
|
|
TEST_CHECK(!f.connected);
|
|
TEST_CHECK(is_ssl_error(f.result_ec));
|
|
}
|
|
|
|
#if defined TORRENT_USE_OPENSSL
|
|
// the certificate is otherwise valid (trusted directly, hostname matches),
|
|
// but is checked against a time far past its notAfter, isolating "expired"
|
|
// as the rejection reason independent of the chain-trust and hostname
|
|
// checks the other tests exercise. Only run under OpenSSL: overriding the
|
|
// verification time requires the raw API in sim::set_verification_time()
|
|
// (see http_server.hpp).
|
|
TORRENT_TEST(https_reject_expired_certificate)
|
|
{
|
|
https_fixture f("hostname_cert.pem", "hostname_key.pem", "hostname_cert.pem");
|
|
// hostname_cert.pem's notAfter is in the year 2300; validate as though
|
|
// it's the year 2400 instead
|
|
set_verification_time(f.ssl_ctx, 13569465600); // 2400-01-01T00:00:00Z
|
|
f.attempt("https://test-hostname.com:8080/test_file");
|
|
f.sim.run();
|
|
|
|
TEST_EQUAL(f.handler_called, 1);
|
|
TEST_CHECK(!f.connected);
|
|
TEST_CHECK(is_ssl_error(f.result_ec));
|
|
}
|
|
|
|
// same as above, but with the certificate not yet valid: hostname_cert.pem's
|
|
// notBefore is the day this test was written, so validating as though it's
|
|
// the year 2020 puts the check time before the certificate's validity
|
|
// window starts, rather than after it ends.
|
|
TORRENT_TEST(https_reject_not_yet_valid_certificate)
|
|
{
|
|
https_fixture f("hostname_cert.pem", "hostname_key.pem", "hostname_cert.pem");
|
|
set_verification_time(f.ssl_ctx, 1577836800); // 2020-01-01T00:00:00Z
|
|
f.attempt("https://test-hostname.com:8080/test_file");
|
|
f.sim.run();
|
|
|
|
TEST_EQUAL(f.handler_called, 1);
|
|
TEST_CHECK(!f.connected);
|
|
TEST_CHECK(is_ssl_error(f.result_ec));
|
|
}
|
|
#endif // TORRENT_USE_OPENSSL
|
|
|
|
// the client and server have no TLS protocol version in common (the server
|
|
// only offers TLS 1.3, the client only offers up to TLS 1.2), so the
|
|
// handshake fails during negotiation, before any certificate is even
|
|
// exchanged. This stands in for "unsupported crypto algorithm" more broadly:
|
|
// a real cipher-suite mismatch surfaces the same way, but restricting cipher
|
|
// lists is OpenSSL-version-specific, whereas restricting protocol versions
|
|
// is a portable way to force the same class of negotiation failure.
|
|
TORRENT_TEST(https_reject_unsupported_protocol)
|
|
{
|
|
https_fixture f("server.pem", "server.pem", "", [](lt::aux::ssl::context& ctx) {
|
|
// server: TLS 1.3 only
|
|
ctx.set_options(lt::aux::ssl::context::no_tlsv1 | lt::aux::ssl::context::no_tlsv1_1
|
|
| lt::aux::ssl::context::no_tlsv1_2);
|
|
});
|
|
|
|
// certificate validation is irrelevant to this test, only the protocol
|
|
// negotiation is under test
|
|
f.ssl_ctx.set_verify_mode(lt::aux::ssl::context::verify_none);
|
|
// client: at most TLS 1.2
|
|
f.ssl_ctx.set_options(lt::aux::ssl::context::no_tlsv1_3);
|
|
|
|
f.attempt("https://10.0.0.2:8080/test_file");
|
|
f.sim.run();
|
|
|
|
TEST_EQUAL(f.handler_called, 1);
|
|
TEST_CHECK(!f.connected);
|
|
TEST_CHECK(is_ssl_error(f.result_ec));
|
|
}
|
|
|
|
#else
|
|
|
|
TORRENT_TEST(disabled) {}
|
|
|
|
#endif // TORRENT_USE_SSL
|