diff --git a/test/ssl/invalid_peer_certificate.pem b/test/ssl/invalid_peer_certificate.pem new file mode 100644 index 000000000..0e97de291 --- /dev/null +++ b/test/ssl/invalid_peer_certificate.pem @@ -0,0 +1,60 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 15441482899666218084 (0xd64b2ae27fdfac64) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=AU, ST=Some-State, O=Internet Widgits Pty Ltd, CN=test CA + Validity + Not Before: Dec 15 20:42:32 2013 GMT + Not After : Dec 15 20:42:32 2014 GMT + Subject: C=AU, ST=Some-State, O=Internet Widgits Pty Ltd, CN=* + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (1024 bit) + Modulus: + 00:df:4a:a7:c3:0b:23:3a:16:e3:aa:66:5d:f4:57: + 54:f5:94:92:55:4e:38:f3:32:a9:1d:7a:cf:f4:35: + 4e:25:28:7f:3a:3d:f1:c9:9f:99:f7:42:fc:8c:8c: + ac:f3:74:91:0e:80:5c:1e:8c:ff:26:94:1f:7e:2d: + d2:55:33:e9:69:e3:74:03:a0:78:43:45:25:5f:bb: + 7d:59:e4:8c:46:ff:7a:4a:7f:51:89:71:f1:6b:07: + 24:9b:0b:37:a5:53:c4:ed:57:05:cf:08:2c:64:43: + 95:a3:93:6d:bd:98:f5:57:4b:7b:7a:c9:3f:47:a5: + 7c:20:fa:91:55:58:df:24:2d + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + EA:FC:F9:50:A2:50:BB:33:DC:FC:15:17:CE:68:D2:3C:03:FD:5B:FF + X509v3 Authority Key Identifier: + keyid:36:A1:4C:8C:07:84:35:8E:76:DF:22:90:AD:63:21:F5:4A:BD:34:E4 + + Signature Algorithm: sha1WithRSAEncryption + 2c:69:84:c4:cd:0d:43:20:6c:7a:60:9d:9d:ed:9c:e0:22:10: + 8c:d1:9e:08:78:bd:ad:d5:03:9b:b2:4f:93:a1:6f:5b:38:47: + ed:30:9a:2e:e8:ea:64:e7:6c:dd:df:fd:40:29:4d:0f:2e:e3: + 1f:5c:b8:ac:f4:37:28:05:22:35:70:3a:04:63:f8:88:d6:cb: + 33:f2:bc:05:b4:d8:4f:0d:eb:4c:e1:a6:55:69:3b:c3:90:7a: + d5:ef:2b:0d:91:17:67:8e:08:79:bb:7d:50:f9:1f:36:91:c8: + 22:0d:d6:f9:f2:5e:c5:66:bf:27:bc:b2:32:4d:2a:cd:7c:a4: + 2a:ee +-----BEGIN CERTIFICATE----- +MIICoTCCAgqgAwIBAgIJANZLKuJ/36xkMA0GCSqGSIb3DQEBBQUAMFcxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxEDAOBgNVBAMMB3Rlc3QgQ0EwHhcNMTMxMjE1MjA0MjMy +WhcNMTQxMjE1MjA0MjMyWjBRMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1T +dGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQowCAYDVQQD +DAEqMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDfSqfDCyM6FuOqZl30V1T1 +lJJVTjjzMqkdes/0NU4lKH86PfHJn5n3QvyMjKzzdJEOgFwejP8mlB9+LdJVM+lp +43QDoHhDRSVfu31Z5IxG/3pKf1GJcfFrBySbCzelU8TtVwXPCCxkQ5Wjk229mPVX +S3t6yT9HpXwg+pFVWN8kLQIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIB +DQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU6vz5 +UKJQuzPc/BUXzmjSPAP9W/8wHwYDVR0jBBgwFoAUNqFMjAeENY523yKQrWMh9Uq9 +NOQwDQYJKoZIhvcNAQEFBQADgYEALGmExM0NQyBsemCdne2c4CIQjNGeCHi9rdUD +m7JPk6FvWzhH7TCaLujqZOds3d/9QClNDy7jH1y4rPQ3KAUiNXA6BGP4iNbLM/K8 +BbTYTw3rTOGmVWk7w5B61e8rDZEXZ44Iebt9UPkfNpHIIg3W+fJexWa/J7yyMk0q +zXykKu4= +-----END CERTIFICATE----- diff --git a/test/ssl/invalid_peer_private_key.pem b/test/ssl/invalid_peer_private_key.pem new file mode 100644 index 000000000..03332bf4c --- /dev/null +++ b/test/ssl/invalid_peer_private_key.pem @@ -0,0 +1,17 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIsLJkydWZi7ACAggA +MBQGCCqGSIb3DQMHBAicrqPGKfsAVQSCAoBNO9Cjpr1Zuw5jKLyVkGFfTLLtvT4Z +b+zAYNiqhD3idw8wSkPKgs4JXbkCJtoOMUyUXPNSczsifjbIDewZbYBOxZXHFgtc +JS1nKCpwy5vktWzhl+PHhtJDOdAehK2igwkv5TwwXF4jJYc4pUNMjwebygJImqeG +9LhCd2LjBb3tk1qy5lqGRL4X+FQslSp0IGGNiBehQDskrAXE1rf8NoscJFO4UJfb +M0Ad59MVB620xgQ3rUxQ40RR2ZYZQ+o0/Bhe2hvSZyoeB+M1DnKbCvgi45qWYy25 +NzT7wohQb1OAJ3EEYhRgnW3E4NtuUuXxUyRAK9M94dxlOR//QE2W32V7evV9Elgd +H16BcdObKqBycuqdLHkJ7jGcwJXbCD4eK+T9iLccH9G3/FO1n98yLQ67AnSJ0IaV +wBOENhJ14qIqr3dwFZIcEo/TzQqKApKef6GULlHQ7Ime7spJvVr9uNcnLa6yq8y0 +2EjeAKitEtgFpLvt0V5xd+NPzyznsKybbT0va39/C1KA94eqniOG3rc8dDGsjd6k +ZFyNXR/aiT/FLmP84qukcSFsap+6PSyFJv5VVMcB4V2RtxPU5sD0HirYOpY/Wuj9 +qG2ONflvh2HrGEn5snmD7Bm7k0eidQBOWCiYn8rNHVCjBV1aN+LxYtDAFHhzLKUB +BXD6wZoHzHZof5jdLbr6S58KHxQYnd0vFlC3n42HNtQrbWNgaFs33z1PrnGsgKma +bCJHBk4+e35ee8aGCpv9zEDGc1l5l8zpc4+SSOOkChKdfihdyj1+HaWx9q2StEPz +49G76d3X82SDpRYxchwl2S30eH3b+X2vLsQg3Z2gRi+LYOBFXD0tUuXF +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/test_ssl.cpp b/test/test_ssl.cpp index b24a6cbe5..47aa7730b 100644 --- a/test/test_ssl.cpp +++ b/test/test_ssl.cpp @@ -41,9 +41,11 @@ POSSIBILITY OF SUCH DAMAGE. #include "setup_transfer.hpp" #include #include +#include #ifdef TORRENT_USE_OPENSSL #include // for asio::error::get_ssl_category() +#include #endif using namespace libtorrent; @@ -154,6 +156,7 @@ void test_ssl(int test_idx, bool use_utp) file.close(); add_torrent_params addp; + addp.save_path = "."; addp.flags &= ~add_torrent_params::flag_paused; addp.flags &= ~add_torrent_params::flag_auto_managed; @@ -165,7 +168,7 @@ void test_ssl(int test_idx, bool use_utp) peer_errors = 0; boost::tie(tor1, tor2, ignore) = setup_transfer(&ses1, &ses2, 0 - , true, false, true, "_ssl", 16 * 1024, &t, false, NULL, true, test.use_ssl_ports); + , true, false, true, "_ssl", 16 * 1024, &t, false, &addp, true, test.use_ssl_ports); if (test.seed_has_cert) { @@ -253,10 +256,283 @@ void test_ssl(int test_idx, bool use_utp) p2 = ses2.abort(); } +std::string password_callback(int length, boost::asio::ssl::context::password_purpose p + , std::string pw) +{ + if (p != boost::asio::ssl::context::for_reading) return ""; + return pw; +} + +struct attack_t +{ + // flags controlling the connection attempt + boost::uint32_t flags; + // whether or not we expect to be able to connect + bool expect; +}; + +enum attack_flags_t +{ + valid_certificate = 1, + invalid_certificate = 2, + valid_sni_hash = 4, + invalid_sni_hash = 8, + valid_bittorrent_hash = 16, +}; + +attack_t attacks[] = +{ + // positive test + { valid_certificate | valid_sni_hash | valid_bittorrent_hash, true}, + + // SNI + { valid_certificate | invalid_sni_hash | valid_bittorrent_hash, false}, + { valid_certificate | valid_bittorrent_hash, false}, + + // certificate + { valid_sni_hash | valid_bittorrent_hash, false}, + { invalid_certificate | valid_sni_hash | valid_bittorrent_hash, false}, + + // bittorrent hash + { valid_certificate | valid_sni_hash, false}, +}; + +const int num_attacks = sizeof(attacks)/sizeof(attacks[0]); + +bool try_connect(session& ses1, int port + , boost::intrusive_ptr const& t, boost::uint32_t flags) +{ + using boost::asio::ssl::context; + + fprintf(stderr, "\nMALICIOUS PEER TEST: "); + if (flags & invalid_certificate) fprintf(stderr, "invalid-certificate "); + else if (flags & valid_certificate) fprintf(stderr, "valid-certificate "); + else fprintf(stderr, "no-certificate "); + + if (flags & invalid_sni_hash) fprintf(stderr, "invalid-SNI-hash "); + else if (flags & valid_sni_hash) fprintf(stderr, "valid-SNI-hash "); + else fprintf(stderr, "no-SNI-hash "); + + if (flags & valid_bittorrent_hash) fprintf(stderr, "valid-bittorrent-hash "); + else fprintf(stderr, "invalid-bittorrent-hash "); + + fprintf(stderr, "\n"); + + error_code ec; + boost::asio::io_service ios; + + // create the SSL context for this torrent. We need to + // inject the root certificate, and no other, to + // verify other peers against + context ctx(ios, context::sslv23); + + ctx.set_options(context::default_workarounds + | boost::asio::ssl::context::no_sslv2 + | boost::asio::ssl::context::single_dh_use); + + // we're a malicious peer, we don't have any interest + // in verifying peers + ctx.set_verify_mode(context::verify_none, ec); + if (ec) + { + fprintf(stderr, "Failed to set SSL verify mode: %s\n" + , ec.message().c_str()); + TEST_CHECK(!ec); + return false; + } + + std::string certificate = combine_path("ssl", "peer_certificate.pem"); + std::string private_key = combine_path("ssl", "peer_private_key.pem"); + std::string dh_params = combine_path("ssl", "dhparams.pem"); + + if (flags & invalid_certificate) + { + certificate = combine_path("ssl", "invalid_peer_certificate.pem"); + private_key = combine_path("ssl", "invalid_peer_private_key.pem"); + } + + // TODO: test using a signed certificate with the wrong info-hash in DN + + if (flags & (valid_certificate | invalid_certificate)) + { + ctx.set_password_callback(boost::bind(&password_callback, _1, _2, "test"), ec); + if (ec) + { + fprintf(stderr, "Failed to set certificate password callback: %s\n" + , ec.message().c_str()); + TEST_CHECK(!ec); + return false; + } + ctx.use_certificate_file(certificate, context::pem, ec); + if (ec) + { + fprintf(stderr, "Failed to set certificate file: %s\n" + , ec.message().c_str()); + TEST_CHECK(!ec); + return false; + } + ctx.use_private_key_file(private_key, context::pem, ec); + if (ec) + { + fprintf(stderr, "Failed to set private key: %s\n" + , ec.message().c_str()); + TEST_CHECK(!ec); + return false; + } + ctx.use_tmp_dh_file(dh_params, ec); + if (ec) + { + fprintf(stderr, "Failed to set DH params: %s\n" + , ec.message().c_str()); + TEST_CHECK(!ec); + return false; + } + } + + boost::asio::ssl::stream ssl_sock(ios, ctx); + + ssl_sock.lowest_layer().connect(tcp::endpoint( + address_v4::from_string("127.0.0.1"), port), ec); + print_alerts(ses1, "ses1", true, true, true, &on_alert); + + if (ec) + { + fprintf(stderr, "Failed to connect: %s\n" + , ec.message().c_str()); + TEST_CHECK(!ec); + return false; + } + + if (flags & valid_sni_hash) + { + std::string name = to_hex(t->info_hash().to_string()); + fprintf(stderr, "SNI: %s\n", name.c_str()); + SSL_set_tlsext_host_name(ssl_sock.native_handle(), name.c_str()); + } + else if (flags & invalid_sni_hash) + { + char const hex_alphabet[] = "0123456789abcdef"; + std::string name; + name.reserve(40); + for (int i = 0; i < 40; ++i) + name += hex_alphabet[rand() % 16]; + + fprintf(stderr, "SNI: %s\n", name.c_str()); + SSL_set_tlsext_host_name(ssl_sock.native_handle(), name.c_str()); + } + + ssl_sock.handshake(asio::ssl::stream_base::client, ec); + + print_alerts(ses1, "ses1", true, true, true, &on_alert); + if (ec) + { + fprintf(stderr, "Failed SSL handshake: %s\n" + , ec.message().c_str()); + return false; + } + + char handshake[] = "\x13" "BitTorrent protocol\0\0\0\0\0\0\0\x04" + " " // space for info-hash + "aaaaaaaaaaaaaaaaaaaa" // peer-id + "\0\0\0\x01\x02"; // interested + + // fill in the info-hash + if (flags & valid_bittorrent_hash) + { + std::memcpy(handshake + 28, &t->info_hash()[0], 20); + } + else + { + // TODO: also test using a hash that refers to a valid torrent + // but that differs from the SNI hash + std::generate(handshake + 28, handshake + 48, &rand); + } + + // fill in the peer-id + std::generate(handshake + 48, handshake + 68, &rand); + boost::asio::write(ssl_sock, libtorrent::asio::buffer(handshake, (sizeof(handshake) - 1)), ec); + if (ec) + { + fprintf(stderr, "failed to write bittorrent handshake: %s\n" + , ec.message().c_str()); + return false; + } + + char buf[68]; + boost::asio::read(ssl_sock, libtorrent::asio::buffer(buf, sizeof(buf)), ec); + if (ec) + { + fprintf(stderr, "failed to read bittorrent handshake: %s\n" + , ec.message().c_str()); + return false; + } + + if (memcmp(buf, "\x13" "BitTorrent protocol", 20) != 0) + { + fprintf(stderr, "invalid bittorrent handshake\n"); + return false; + } + + if (memcmp(buf + 28, &t->info_hash()[0], 20) != 0) + { + fprintf(stderr, "invalid info-hash in bittorrent handshake\n"); + return false; + } + + fprintf(stderr, "successfully connected over SSL and shook hand over bittorrent\n"); + + return true; +} + +void test_malicious_peer() +{ + error_code ec; + remove_all("tmp3_ssl", ec); + + // set up session + session ses1(fingerprint("LT", 0, 1, 0, 0) + , std::make_pair(48075, 49000), "0.0.0.0", 0, alert_mask); + wait_for_listen(ses1, "ses1"); + session_settings sett; + sett.ssl_listen = 1024 + rand() % 50000; + ses1.set_settings(sett); + + // create torrent + create_directory("tmp3_ssl", ec); + std::ofstream file("tmp3_ssl/temporary"); + boost::intrusive_ptr t = ::create_torrent(&file + , 16 * 1024, 13, false, "ssl/root_ca_cert.pem"); + file.close(); + + add_torrent_params addp; + addp.save_path = "."; + addp.flags &= ~add_torrent_params::flag_paused; + addp.flags &= ~add_torrent_params::flag_auto_managed; + addp.ti = t; + + torrent_handle tor1 = ses1.add_torrent(addp, ec); + + tor1.set_ssl_certificate( + combine_path("ssl", "peer_certificate.pem") + , combine_path("ssl", "peer_private_key.pem") + , combine_path("ssl", "dhparams.pem") + , "test"); + + wait_for_listen(ses1, "ses1"); + + for (int i = 0; i < num_attacks; ++i) + { + bool success = try_connect(ses1, sett.ssl_listen, t, attacks[i].flags); + TEST_EQUAL(attacks[i].expect, success); + } +} + int test_main() { using namespace libtorrent; + test_malicious_peer(); + // No support for SSL/uTP yet, so always pass in false for (int i = 0; i < sizeof(test_config)/sizeof(test_config[0]); ++i) test_ssl(i, false);