#include #include #include #include #include #include #include #include #include #include #include #include #include "dpf/compose.hpp" #include "dpf/log.hpp" #include "dpf/net/client_link.hpp" #include "dpf/net/connect.hpp" #include "dpf/net/identity.hpp" #include "dpf/net/party_session.hpp" #include "dpf/net/security.hpp" #include "dpf/net/tls.hpp" #include "dpf/online_session.hpp" #include "dpf/party_runner.hpp" #include "dpf/run_config.hpp" #include #include namespace { using dpf::net::identity; using dpf::net::public_key; std::string temp_path(const std::string & name) { return "/tmp/libdpf_security_" + std::to_string(::getpid()) + "_" + name; } /// Two connected TCP sockets, each on its own io_context. struct socket_pair { asio::io_context io_a, io_b; asio::ip::tcp::socket a{io_a}, b{io_b}; socket_pair() { asio::ip::tcp::acceptor acc(io_a); dpf::net::open_listener(acc, 0); const auto port = acc.local_endpoint().port(); std::thread t([&] { dpf::net::connect_until(b, "127.0.0.1", port, std::chrono::seconds(5)); }); dpf::net::accept_until(acc, a, std::chrono::seconds(5)); t.join(); a.set_option(asio::ip::tcp::no_delay(true)); b.set_option(asio::ip::tcp::no_delay(true)); } }; /// Run `io` until `done` or `deadline`, restarting it when it runs dry. void run_until(asio::io_context & io, const bool & done, std::chrono::steady_clock::time_point deadline) { while (!done && std::chrono::steady_clock::now() < deadline) { if (io.stopped()) io.restart(); io.run_one_for(std::chrono::milliseconds(10)); } } /// Run `server` and `client` on two threads; throw both failures' messages. void both(const std::function & server, const std::function & client) { std::string e1, e2; std::thread ts([&] { try { server(); } catch (const std::exception & e) { e1 = e.what(); } }); try { client(); } catch (const std::exception & e) { e2 = e.what(); } ts.join(); if (!e1.empty() || !e2.empty()) throw std::runtime_error("server: " + e1 + " | client: " + e2); } } // namespace TEST(Identity, KeyFileRoundTripAndMode) { const auto path = temp_path("id.key"); ::unlink(path.c_str()); const auto id = identity::generate(); id.save(path); struct stat st{}; ASSERT_EQ(::stat(path.c_str(), &st), 0); EXPECT_EQ(st.st_mode & 0777, 0600u); EXPECT_THROW(id.save(path), std::runtime_error); const auto back = identity::load(path); EXPECT_EQ(back.key(), id.key()); EXPECT_EQ(id.key().base64().size(), 44u); EXPECT_EQ(public_key::parse(id.key().base64()), id.key()); const auto pub = temp_path("id.pub"); { std::ofstream out(pub); out << "# peer 1\n" << id.key().base64() << "\n"; } EXPECT_EQ(public_key::parse("file:" + pub), id.key()); EXPECT_THROW(public_key::parse("not-a-key"), std::invalid_argument); EXPECT_THROW(public_key::parse(id.key().base64().substr(0, 40)), std::invalid_argument); ::unlink(path.c_str()); ::unlink(pub.c_str()); } TEST(Identity, DevelopmentIsFixedAndMarked) { const auto & a = identity::development(); const auto & b = identity::development(); EXPECT_TRUE(a.is_development()); EXPECT_EQ(a.key(), b.key()); EXPECT_FALSE(identity::generate().is_development()); EXPECT_NE(identity::generate().key(), identity::generate().key()); } namespace { /// Party-link handshake between `ida` (accepting) and `idb` (connecting). void peer_handshake(const identity & ida, const identity & idb, const dpf::net::peer_security & pa, const dpf::net::peer_security & pb, dpf::net::link_security & sa, dpf::net::link_security & sb) { socket_pair sp; auto ca = dpf::net::make_peer_tls_context(ida); auto cb = dpf::net::make_peer_tls_context(idb); dpf::net::tls_stream ta(std::move(sp.a), *ca); dpf::net::tls_stream tb(std::move(sp.b), *cb); both( [&] { dpf::net::tls_handshake(sp.io_a, ta, true, std::chrono::seconds(5), "p0"); sa = dpf::net::tls_describe(ta); try { dpf::net::check_peer(sa, pa, 1, "party 1"); } catch (...) { std::error_code ec; ta.lowest_layer().close(ec); throw; } std::uint8_t in[5]; dpf::net::tls_read(sp.io_a, ta, in, 5, std::chrono::seconds(5), "read"); dpf::net::tls_write(sp.io_a, ta, in, 5, std::chrono::seconds(5), "echo"); }, [&] { dpf::net::tls_handshake(sp.io_b, tb, false, std::chrono::seconds(5), "p1"); sb = dpf::net::tls_describe(tb); dpf::net::check_peer(sb, pb, 0, "party 0"); const std::uint8_t out[5] = {'h', 'e', 'l', 'l', 'o'}; std::uint8_t back[5] = {}; dpf::net::tls_write(sp.io_b, tb, out, 5, std::chrono::seconds(5), "write"); dpf::net::tls_read(sp.io_b, tb, back, 5, std::chrono::seconds(5), "read"); if (std::memcmp(out, back, 5) != 0) throw std::runtime_error("echo mismatch"); }); } } // namespace TEST(PeerTls, NoKeysEncryptsUnauthenticated) { const auto a = identity::generate(); const auto b = identity::generate(); dpf::net::peer_security pa, pb; dpf::net::link_security sa, sb; peer_handshake(a, b, pa, pb, sa, sb); EXPECT_TRUE(sa.encrypted); EXPECT_EQ(sa.protocol, "TLSv1.3"); EXPECT_EQ(sa.peer_auth, "none"); EXPECT_EQ(sb.peer_auth, "none"); ASSERT_TRUE(sa.peer_key.has_value()); EXPECT_EQ(*sa.peer_key, b.key()); EXPECT_EQ(*sb.peer_key, a.key()); } TEST(PeerTls, KeyAuthenticatesOneDirection) { const auto a = identity::generate(); const auto b = identity::generate(); dpf::net::peer_security pa, pb; pa.trusted[1] = b.key(); dpf::net::link_security sa, sb; peer_handshake(a, b, pa, pb, sa, sb); EXPECT_EQ(sa.peer_auth, "key"); EXPECT_EQ(sb.peer_auth, "none"); pb.trusted[0] = a.key(); peer_handshake(a, b, pa, pb, sa, sb); EXPECT_EQ(sa.peer_auth, "key"); EXPECT_EQ(sb.peer_auth, "key"); } TEST(PeerTls, WrongKeyNamesBothKeys) { const auto a = identity::generate(); const auto b = identity::generate(); const auto other = identity::generate(); dpf::net::peer_security pa, pb; pa.trusted[1] = other.key(); dpf::net::link_security sa, sb; try { peer_handshake(a, b, pa, pb, sa, sb); FAIL() << "a mismatched key must fail the link"; } catch (const std::exception & e) { const std::string what = e.what(); EXPECT_NE(what.find(b.key().base64()), std::string::npos) << what; EXPECT_NE(what.find(other.key().base64()), std::string::npos) << what; } } namespace { struct client_result { dpf::net::link_security client; dpf::net::link_security server; }; client_result client_handshake(const dpf::net::server_security & ss, const dpf::net::client_security & cs, const std::string & host = "") { socket_pair sp; bool dev = false; auto sctx = dpf::net::make_server_tls_context(ss, dev); auto cctx = dpf::net::make_client_tls_context(cs); dpf::net::tls_stream ts(std::move(sp.a), *sctx); dpf::net::tls_stream tc(std::move(sp.b), *cctx); const std::string name = cs.server_name.empty() ? host : cs.server_name; if (!cs.ca_file.empty()) dpf::net::tls_expect_host(tc, name); client_result r; both( [&] { dpf::net::tls_handshake(sp.io_a, ts, true, std::chrono::seconds(5), "server"); r.server = dpf::net::tls_describe(ts); dpf::net::check_client(r.server, ss); std::uint8_t in[1]; std::error_code ec; asio::read(ts, asio::buffer(in, 1), ec); }, [&] { dpf::net::tls_handshake(sp.io_b, tc, false, std::chrono::seconds(5), "client"); r.client = dpf::net::tls_describe(tc); std::error_code ec; try { dpf::net::check_server(r.client, tc, cs, "localhost"); } catch (...) { tc.lowest_layer().close(ec); throw; } const std::uint8_t b = 1; dpf::net::tls_write(sp.io_b, tc, &b, 1, std::chrono::seconds(5), "write"); }); return r; } /// A throwaway CA and a server certificate for `name`, as PEM files. void make_ca_chain(const std::string & name, const std::string & ca_pem, const std::string & cert_pem, const std::string & key_pem) { auto keygen = [] { EVP_PKEY * k = nullptr; EVP_PKEY_CTX * c = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, nullptr); EVP_PKEY_keygen_init(c); EVP_PKEY_keygen(c, &k); EVP_PKEY_CTX_free(c); return k; }; auto cert = [](EVP_PKEY * subject, EVP_PKEY * issuer_key, X509 * issuer, const char * cn, bool ca, const std::string & dns) { X509 * x = X509_new(); X509_set_version(x, 2); ASN1_INTEGER_set(X509_get_serialNumber(x), ca ? 1 : 2); X509_gmtime_adj(X509_getm_notBefore(x), -3600); X509_gmtime_adj(X509_getm_notAfter(x), 86400); X509_set_pubkey(x, subject); X509_NAME * n = X509_get_subject_name(x); X509_NAME_add_entry_by_txt(n, "CN", MBSTRING_ASC, reinterpret_cast(cn), -1, -1, 0); X509_set_issuer_name(x, issuer ? X509_get_subject_name(issuer) : n); X509V3_CTX v3; X509V3_set_ctx_nodb(&v3); X509V3_set_ctx(&v3, issuer ? issuer : x, x, nullptr, nullptr, 0); auto add = [&](int nid, const char * value) { X509_EXTENSION * e = X509V3_EXT_conf_nid(nullptr, &v3, nid, value); X509_add_ext(x, e, -1); X509_EXTENSION_free(e); }; add(NID_basic_constraints, ca ? "critical,CA:TRUE" : "CA:FALSE"); if (ca) add(NID_key_usage, "critical,keyCertSign,cRLSign"); else add(NID_subject_alt_name, ("DNS:" + dns).c_str()); X509_sign(x, issuer_key, nullptr); return x; }; EVP_PKEY * cak = keygen(); EVP_PKEY * srvk = keygen(); X509 * cacert = cert(cak, cak, nullptr, "test CA", true, ""); X509 * srv = cert(srvk, cak, cacert, name.c_str(), false, name); FILE * f = std::fopen(ca_pem.c_str(), "w"); PEM_write_X509(f, cacert); std::fclose(f); f = std::fopen(cert_pem.c_str(), "w"); PEM_write_X509(f, srv); std::fclose(f); f = std::fopen(key_pem.c_str(), "w"); PEM_write_PrivateKey(f, srvk, nullptr, nullptr, 0, nullptr, nullptr); std::fclose(f); X509_free(cacert); X509_free(srv); EVP_PKEY_free(cak); EVP_PKEY_free(srvk); } } // namespace namespace { /// Forwards bytes between a client and a server and keeps a copy of both /// directions, so a test can look at what crossed the wire. class recording_relay { public: explicit recording_relay(unsigned short upstream) : upstream_(upstream) { dpf::net::open_listener(acc_, 0); port_ = acc_.local_endpoint().port(); thread_ = std::thread([this] { run(); }); } ~recording_relay() { std::error_code ec; acc_.close(ec); { std::lock_guard lock(mu_); for (int fd : fds_) ::shutdown(fd, SHUT_RDWR); } if (thread_.joinable()) thread_.join(); } unsigned short port() const { return port_; } std::string seen() { std::lock_guard lock(mu_); return seen_; } private: void run() { try { asio::ip::tcp::socket in(io_), out(io_); dpf::net::accept_until(acc_, in, std::chrono::seconds(10)); dpf::net::connect_until(out, "127.0.0.1", upstream_, std::chrono::seconds(10)); { std::lock_guard lock(mu_); fds_ = {in.native_handle(), out.native_handle()}; } std::thread back([&] { pump(out, in); }); pump(in, out); back.join(); } catch (...) { } } void pump(asio::ip::tcp::socket & from, asio::ip::tcp::socket & to) { std::uint8_t buf[4096]; std::error_code ec; for (;;) { const std::size_t n = from.read_some(asio::buffer(buf), ec); if (ec || n == 0) break; { std::lock_guard lock(mu_); seen_.append(reinterpret_cast(buf), n); } asio::write(to, asio::buffer(buf, n), ec); if (ec) break; } std::error_code e; to.shutdown(asio::socket_base::shutdown_send, e); } unsigned short upstream_; asio::io_context io_; asio::ip::tcp::acceptor acc_{io_}; unsigned short port_ = 0; std::mutex mu_; std::string seen_; std::vector fds_; std::thread thread_; }; const std::string k_marker = "SECRET-SHARE-MARKER-0123456789"; /// Party 0 accepts (through a relay), party 1 connects; party 1 writes the /// marker on lane 1 and party 0 reads it. Returns what crossed the wire. template std::string relay_exchange() { asio::io_context io0, io1; asio::ip::tcp::acceptor acc(io0); dpf::net::open_listener(acc, 0); recording_relay relay(acc.local_endpoint().port()); const std::size_t lanes = Parallel ? 1 : 2; const std::size_t lane = Parallel ? 0 : 1; (void)lanes; std::string got; both( [&] { asio::ip::tcp::socket s(io0); dpf::net::accept_until(acc, s, std::chrono::seconds(5)); s.set_option(asio::ip::tcp::no_delay(true)); std::unique_ptr arr; auto id = identity::generate(); auto ctx = dpf::net::make_peer_tls_context(id); if constexpr (Tls) { dpf::net::tls_stream t(std::move(s), *ctx); dpf::net::tls_handshake(io0, t, true, std::chrono::seconds(5), "p0"); if constexpr (Parallel) { std::vector v; v.push_back(std::move(t)); arr = std::make_unique( io0, std::move(v)); } else arr = std::make_unique(io0, std::move(t), 0, 1, lanes); } else arr = std::make_unique(io0, std::move(s), 0, 1, lanes); std::string in(k_marker.size() * 4, '\0'); bool done = false; std::error_code rec; arr->async_read(lane, in.data(), in.size(), [&](const std::error_code & ec) { rec = ec; done = true; }); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); run_until(io0, done, deadline); if (!done || rec) throw std::runtime_error("read failed: " + rec.message() + " (done " + std::to_string(done) + ", relay saw " + std::to_string(relay.seen().size()) + " bytes)"); got = in; // The peer closes after writing: the next read ends cleanly. done = false; char extra = 0; arr->async_read(lane, &extra, 1, [&](const std::error_code & ec) { rec = ec; done = true; }); run_until(io0, done, deadline); if (rec != asio::error::eof) throw std::runtime_error("expected eof after peer close, got " + rec.message()); arr.reset(); for (int i = 0; i < 50; ++i) io0.poll(); }, [&] { asio::ip::tcp::socket s(io1); dpf::net::connect_until(s, "127.0.0.1", relay.port(), std::chrono::seconds(5)); s.set_option(asio::ip::tcp::no_delay(true)); std::unique_ptr arr; auto id = identity::generate(); auto ctx = dpf::net::make_peer_tls_context(id); if constexpr (Tls) { dpf::net::tls_stream t(std::move(s), *ctx); dpf::net::tls_handshake(io1, t, false, std::chrono::seconds(5), "p1"); if constexpr (Parallel) { std::vector v; v.push_back(std::move(t)); arr = std::make_unique( io1, std::move(v)); } else arr = std::make_unique(io1, std::move(t), 1, 0, lanes); } else arr = std::make_unique(io1, std::move(s), 1, 0, lanes); const std::string out = k_marker + k_marker + k_marker + k_marker; bool done = false; arr->async_write(lane, out.data(), out.size(), [&](const std::error_code &) { done = true; }); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); run_until(io1, done, deadline); arr.reset(); // graceful: drains, then closes for (int i = 0; i < 50; ++i) io1.poll(); }); if (got.find(k_marker) != 0) throw std::runtime_error("payload did not arrive intact"); return relay.seen(); } } // namespace TEST(TlsBackends, PlainThroughRelay) { const auto plain = relay_exchange(); EXPECT_NE(plain.find(k_marker), std::string::npos); } TEST(TlsBackends, TlsMuxDirect) { socket_pair sp; auto ca = dpf::net::make_peer_tls_context(identity::generate()); auto cb = dpf::net::make_peer_tls_context(identity::generate()); dpf::net::tls_stream ta(std::move(sp.a), *ca); dpf::net::tls_stream tb(std::move(sp.b), *cb); std::string got(8, '\0'); both( [&] { dpf::net::tls_handshake(sp.io_a, ta, true, std::chrono::seconds(5), "a"); dpf::net::async_tls_mux_stream_array m(sp.io_a, std::move(ta), 0, 1, 2); bool done = false; std::error_code rec; m.async_read(1, got.data(), 8, [&](const std::error_code & ec) { rec = ec; done = true; }); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); run_until(sp.io_a, done, deadline); if (!done) throw std::runtime_error("direct read timed out; stats in=" + std::to_string(m.stats().bytes_in)); // The kernel counts TLS records and the handshake too. const auto st = m.stats(); if (st.socket_bytes_in <= st.bytes_in + 22) throw std::runtime_error("socket bytes " + std::to_string(st.socket_bytes_in) + " vs frame bytes " + std::to_string(st.bytes_in)); }, [&] { dpf::net::tls_handshake(sp.io_b, tb, false, std::chrono::seconds(5), "b"); dpf::net::async_tls_mux_stream_array m(sp.io_b, std::move(tb), 1, 0, 2); bool done = false; m.async_write(1, "abcdefgh", 8, [&](const std::error_code &) { done = true; }); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); run_until(sp.io_b, done, deadline); for (int i = 0; i < 100; ++i) sp.io_b.run_one_for(std::chrono::milliseconds(5)); if (!done) throw std::runtime_error("direct write timed out; stats out=" + std::to_string(m.stats().bytes_out)); }); EXPECT_EQ(got, "abcdefgh"); } TEST(TlsBackends, MuxCiphertextOnTheWire) { const auto plain = relay_exchange(); EXPECT_NE(plain.find(k_marker), std::string::npos); const auto tls = relay_exchange(); EXPECT_EQ(tls.find(k_marker), std::string::npos); EXPECT_GT(tls.size(), k_marker.size() * 4); } TEST(TlsBackends, ParallelCiphertextOnTheWire) { const auto tls = relay_exchange(); EXPECT_EQ(tls.find(k_marker), std::string::npos); EXPECT_GT(tls.size(), k_marker.size() * 4); } TEST(ClientTls, DevelopmentCertificateIsTheDefault) { dpf::net::server_security ss; dpf::net::client_security cs; const auto r = client_handshake(ss, cs); EXPECT_EQ(r.client.peer_auth, "development"); EXPECT_EQ(r.server.peer_auth, "none"); EXPECT_EQ(r.client.protocol, "TLSv1.3"); } TEST(ClientTls, UnknownServerKeyIsRefusedUntilPinned) { const auto id = std::make_shared(identity::generate()); dpf::net::server_security ss; ss.self = id; dpf::net::client_security cs; try { (void)client_handshake(ss, cs); FAIL() << "a client with no pins must refuse a non-development key"; } catch (const std::exception & e) { const std::string what = e.what(); EXPECT_NE(what.find("does not trust"), std::string::npos) << what; EXPECT_NE(what.find(id->key().base64()), std::string::npos) << what; } cs.pins.push_back(id->key()); EXPECT_EQ(client_handshake(ss, cs).client.peer_auth, "key"); dpf::net::client_security off; off.verify = false; EXPECT_EQ(client_handshake(ss, off).client.peer_auth, "none"); // Once a client pins anything, the development certificate is refused. dpf::net::server_security dev; EXPECT_THROW((void)client_handshake(dev, cs), std::runtime_error); } TEST(ClientTls, CaChainAndHostName) { const auto ca = temp_path("ca.pem"); const auto crt = temp_path("srv.pem"); const auto key = temp_path("srv.key"); make_ca_chain("localhost", ca, crt, key); dpf::net::server_security ss; ss.cert_file = crt; ss.key_file = key; dpf::net::client_security cs; cs.ca_file = ca; cs.server_name = "localhost"; EXPECT_EQ(client_handshake(ss, cs).client.peer_auth, "ca"); cs.server_name = "elsewhere.example"; EXPECT_THROW((void)client_handshake(ss, cs), std::runtime_error); ::unlink(ca.c_str()); ::unlink(crt.c_str()); ::unlink(key.c_str()); } TEST(ClientTls, ServerChecksPinnedClientKeys) { const auto cid = std::make_shared(identity::generate()); dpf::net::server_security ss; ss.client_pins.push_back(cid->key()); dpf::net::client_security cs; cs.self = cid; const auto r = client_handshake(ss, cs); EXPECT_EQ(r.server.peer_auth, "key"); EXPECT_EQ(r.client.peer_auth, "development"); } // --------------------------------------------------------------------------- // Party sessions // --------------------------------------------------------------------------- namespace { struct session_pair { std::string error[2]; dpf::net::link_security sec[2]; std::uint64_t got[2] = {0, 0}; }; /// Two party_sessions join in-process with `opts[me]`, swap one word, and /// report each side's view of the link (or its error). session_pair join_two(const dpf::net::session_options (&opts)[2], const std::string & role_prefix = "") { auto ports = dpf::net::make_mesh_ports(2); session_pair out; auto run = [&](unsigned me) { dpf::log::role_scope role(role_prefix.empty() ? dpf::log::role() : role_prefix + std::to_string(me)); try { asio::io_context io; dpf::net::party_session s(io, me, 2, opts[me]); s.join("127.0.0.1", ports); out.sec[me] = s.edge_security(1 - me); std::uint64_t mine = 100 + me; bool wrote = false, read = false; s.peer(1 - me).async_write(0, &mine, 8, [&](const std::error_code &) { wrote = true; }); s.peer(1 - me).async_read(0, &out.got[me], 8, [&](const std::error_code &) { read = true; }); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (!(wrote && read) && std::chrono::steady_clock::now() < deadline) { if (io.stopped()) io.restart(); io.run_one_for(std::chrono::milliseconds(10)); } } catch (const std::exception & e) { out.error[me] = e.what(); } }; std::thread t0([&] { run(0); }); std::thread t1([&] { run(1); }); t0.join(); t1.join(); return out; } dpf::net::session_options with_limits(dpf::net::session_options o) { o.limits.accept = std::chrono::milliseconds(3000); o.limits.connect = std::chrono::milliseconds(3000); o.limits.handshake = std::chrono::milliseconds(3000); o.limits.join = std::chrono::milliseconds(3000); return o; } } // namespace TEST(Session, EncryptedByDefaultWithoutKeys) { dpf::net::session_options o[2] = {with_limits({}), with_limits({})}; const auto r = join_two(o); ASSERT_TRUE(r.error[0].empty() && r.error[1].empty()) << r.error[0] << r.error[1]; for (int me = 0; me < 2; ++me) { EXPECT_TRUE(r.sec[me].encrypted); EXPECT_EQ(r.sec[me].protocol, "TLSv1.3"); EXPECT_EQ(r.sec[me].peer_auth, "none"); EXPECT_FALSE(r.sec[me].peer_verified_us); EXPECT_EQ(r.got[me], 101u - me); } } TEST(Session, KeysAuthenticatePerDirection) { auto k0 = std::make_shared(identity::generate()); auto k1 = std::make_shared(identity::generate()); dpf::net::session_options o[2] = {with_limits({}), with_limits({})}; o[0].security.self = k0; o[1].security.self = k1; o[0].security.trusted[1] = k1->key(); for (auto kind : {dpf::net::transport::mux, dpf::net::transport::parallel}) { o[0].kind = o[1].kind = kind; o[0].n_lanes = o[1].n_lanes = 2; auto r = join_two(o); ASSERT_TRUE(r.error[0].empty() && r.error[1].empty()) << r.error[0] << r.error[1]; EXPECT_EQ(r.sec[0].peer_auth, "key"); EXPECT_EQ(r.sec[1].peer_auth, "none"); EXPECT_FALSE(r.sec[0].peer_verified_us); EXPECT_TRUE(r.sec[1].peer_verified_us); ASSERT_TRUE(r.sec[1].peer_key.has_value()); EXPECT_EQ(*r.sec[1].peer_key, k0->key()); } o[1].security.trusted[0] = k0->key(); auto r = join_two(o); ASSERT_TRUE(r.error[0].empty() && r.error[1].empty()) << r.error[0] << r.error[1]; EXPECT_EQ(r.sec[0].peer_auth, "key"); EXPECT_EQ(r.sec[1].peer_auth, "key"); EXPECT_TRUE(r.sec[0].peer_verified_us); EXPECT_TRUE(r.sec[1].peer_verified_us); } TEST(Session, WrongKeyFailsBothEndsQuickly) { auto k1 = std::make_shared(identity::generate()); dpf::net::session_options o[2] = {with_limits({}), with_limits({})}; o[1].security.self = k1; o[0].security.trusted[1] = identity::generate().key(); const auto t0 = std::chrono::steady_clock::now(); const auto r = join_two(o); EXPECT_LT(std::chrono::steady_clock::now() - t0, std::chrono::seconds(2)); EXPECT_NE(r.error[0].find(k1->key().base64()), std::string::npos) << r.error[0]; EXPECT_FALSE(r.error[1].empty()); } TEST(Session, MixedEncryptionSettingsAreNamed) { dpf::net::session_options o[2] = {with_limits({}), with_limits({})}; o[1].security.encrypt = false; const auto r = join_two(o); const std::string both = r.error[0] + " | " + r.error[1]; EXPECT_NE(both.find("encryption"), std::string::npos) << both; } TEST(Session, SctpIsRefusedWhileEncrypted) { asio::io_context io; dpf::net::session_options o; o.kind = dpf::net::transport::sctp; try { dpf::net::party_session s(io, 0, 2, o); FAIL() << "an encrypted session must refuse SCTP"; } catch (const std::invalid_argument & e) { EXPECT_NE(std::string(e.what()).find("cannot be encrypted"), std::string::npos); } if (dpf::net::sctp_available()) { o.security.encrypt = false; EXPECT_NO_THROW(dpf::net::party_session(io, 0, 2, o)); } } TEST(Session, LackOfKeysIsLogged) { const auto path = temp_path("lack.log"); ::unlink(path.c_str()); dpf::log::settings ls; ls.sinks = "file:" + path; dpf::log::configure(ls); dpf::net::session_options o[2] = {with_limits({}), with_limits({})}; const auto r = join_two(o, "lack-test-p"); dpf::log::configure(dpf::log::settings{}); ASSERT_TRUE(r.error[0].empty() && r.error[1].empty()) << r.error[0] << r.error[1]; std::ifstream in(path); const std::string text((std::istreambuf_iterator(in)), std::istreambuf_iterator()); EXPECT_NE(text.find("ev=security.no_identity"), std::string::npos) << text; EXPECT_NE(text.find("ev=security.unauthenticated"), std::string::npos) << text; EXPECT_NE(text.find("ephemeral=1"), std::string::npos) << text; EXPECT_NE(text.find("auth=none"), std::string::npos) << text; EXPECT_NE(text.find("encryption=TLSv1.3/"), std::string::npos) << text; ::unlink(path.c_str()); } TEST(Session, DealerKeyAuthenticatesTheDealer) { auto dk = std::make_shared(identity::generate()); dpf::net::session_options dopt = with_limits({}); dopt.security.self = dk; dpf::net::session_options popt = with_limits({}); popt.security.trusted[dpf::net::dealer_id] = dk->key(); std::atomic port{0}; std::string derr; dpf::net::link_security dealer_view; std::thread dealer([&] { try { asio::io_context io; dpf::net::dealer_session d(io, 1, dopt); port.store(d.listen()); d.accept_parties(); dealer_view = d.party_security(0); } catch (const std::exception & e) { derr = e.what(); } }); while (port.load() == 0) std::this_thread::yield(); asio::io_context io; dpf::net::party_session p(io, 0, 2, popt); p.connect_dealer("127.0.0.1", port.load()); dealer.join(); ASSERT_TRUE(derr.empty()) << derr; EXPECT_EQ(p.dealer_security().peer_auth, "key"); EXPECT_EQ(dealer_view.peer_auth, "none"); EXPECT_TRUE(dealer_view.peer_verified_us); } // --------------------------------------------------------------------------- // Client links // --------------------------------------------------------------------------- namespace { /// A client connects to a `client_listener` and sends one word to it. std::string client_roundtrip(const dpf::net::server_security & ss, const dpf::net::client_security & cs, dpf::net::link_security * client_view = nullptr) { std::atomic port{0}; std::string serr, cerr; std::uint64_t got = 0; std::thread server([&] { try { asio::io_context io; dpf::net::deadlines lim; lim.accept = std::chrono::milliseconds(3000); dpf::net::client_listener l(io, ss, 1, {}, lim); port.store(l.listen()); auto c = l.accept(); bool done = false; c.link->async_read(0, &got, 8, [&](const std::error_code &) { done = true; }); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); run_until(io, done, deadline); } catch (const std::exception & e) { serr = e.what(); } }); while (port.load() == 0) std::this_thread::yield(); try { asio::io_context io; auto c = dpf::net::connect_server(io, "127.0.0.1", port.load(), cs); if (client_view != nullptr) *client_view = c.security; std::uint64_t v = 77; bool done = false; c.link->async_write(0, &v, 8, [&](const std::error_code &) { done = true; }); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); run_until(io, done, deadline); c.link.reset(); for (int i = 0; i < 20; ++i) io.poll(); } catch (const std::exception & e) { cerr = e.what(); } server.join(); if (!cerr.empty()) return "client: " + cerr; if (!serr.empty()) return "server: " + serr; return got == 77 ? "" : "server got the wrong word"; } } // namespace TEST(ClientLink, DevelopmentDefaultWorksAndSaysSo) { dpf::net::link_security view; EXPECT_EQ(client_roundtrip({}, {}, &view), ""); EXPECT_EQ(view.peer_auth, "development"); } TEST(ClientLink, ServerIdentityNeedsAPinOrVerifyOff) { dpf::net::server_security ss; ss.self = std::make_shared(identity::generate()); EXPECT_NE(client_roundtrip(ss, {}).find("does not trust"), std::string::npos); dpf::net::client_security pinned; pinned.pins.push_back(ss.self->key()); dpf::net::link_security view; EXPECT_EQ(client_roundtrip(ss, pinned, &view), ""); EXPECT_EQ(view.peer_auth, "key"); dpf::net::client_security off; off.verify = false; EXPECT_EQ(client_roundtrip(ss, off, &view), ""); EXPECT_EQ(view.peer_auth, "none"); } // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- TEST(Config, SecurityFromAFileWithRelativePaths) { const std::string dir = temp_path("cfgdir"); (void)::system(("rm -rf '" + dir + "' && mkdir -p '" + dir + "'").c_str()); const auto me = identity::generate(); const auto other = identity::generate(); me.save(dir + "/p0.key"); { std::ofstream pub(dir + "/p1.pub"); pub << other.key().base64() << "\n"; std::ofstream cfg(dir + "/p0.conf"); cfg << "# party 0\n" << "identity = p0.key\n" << "peer.1 = file:p1.pub # the other party\n" << "transport = mux\n" << "client_pin = " << other.key().base64() << "\n"; } dpf::app::run_config c; const std::string arg = "--config=" + dir + "/p0.conf"; const char * argv[] = {"x", arg.c_str()}; EXPECT_TRUE(c.apply_args(2, const_cast(argv)).empty()); ASSERT_TRUE(c.security.self); EXPECT_EQ(c.security.self->key(), me.key()); ASSERT_NE(c.security.trusted_key(1), nullptr); EXPECT_EQ(*c.security.trusted_key(1), other.key()); EXPECT_EQ(c.kind, dpf::net::transport::mux); ASSERT_EQ(c.client.pins.size(), 1u); const auto summary = c.summary(); EXPECT_NE(summary.find("encryption=on"), std::string::npos) << summary; EXPECT_NE(summary.find("identity=" + me.key().base64()), std::string::npos); EXPECT_NE(summary.find("trusted=1"), std::string::npos); { std::ofstream bad(dir + "/bad.conf"); bad << "transport = mux\npeer.1 = not-a-key\n"; } try { c.set("config", dir + "/bad.conf"); FAIL() << "a bad key must be rejected"; } catch (const std::invalid_argument & e) { EXPECT_NE(std::string(e.what()).find("bad.conf:2"), std::string::npos) << e.what(); } EXPECT_THROW(c.set("encryption", "maybe"), std::invalid_argument); (void)::system(("rm -rf '" + dir + "'").c_str()); } // --------------------------------------------------------------------------- // Separate processes from key files and config files // --------------------------------------------------------------------------- namespace { dpf::protocol::plan open_plan(std::size_t party, dpf::protocol::node & x, dpf::protocol::node & out) { dpf::protocol::composer c(party); x = c.input(dpf::protocol::domain::a, 8); out = c.exchange(x); return c.schedule(); } } // namespace TEST(Deployment, TwoProcessesAuthenticateEachOther) { const std::string dir = temp_path("deploy"); (void)::system(("rm -rf '" + dir + "' && mkdir -p '" + dir + "'").c_str()); const identity ids[2] = {identity::generate(), identity::generate()}; unsigned short ports[2]; for (int i = 0; i < 2; ++i) { ids[i].save(dir + "/p" + std::to_string(i) + ".key"); asio::io_context io; asio::ip::tcp::acceptor a(io); dpf::net::open_listener(a, 0); ports[i] = a.local_endpoint().port(); } for (int i = 0; i < 2; ++i) { std::ofstream cfg(dir + "/p" + std::to_string(i) + ".conf"); cfg << "identity = p" << i << ".key\n" << "peer." << (1 - i) << " = " << ids[1 - i].key().base64() << "\n" << "transport = mux\n"; } const std::string peers = "--peers=127.0.0.1:" + std::to_string(ports[0]) + ",127.0.0.1:" + std::to_string(ports[1]); pid_t kids[2]; for (int me = 0; me < 2; ++me) { const pid_t pid = ::fork(); ASSERT_GE(pid, 0); if (pid == 0) { int code = 1; try { const std::string party = "--party=" + std::to_string(me); const std::string cfg = "--config=" + dir + "/p" + std::to_string(me) + ".conf"; const std::string log = "--log=file:" + dir + "/p" + std::to_string(me) + ".log"; const char * argv[] = {"node", party.c_str(), peers.c_str(), cfg.c_str(), log.c_str()}; auto args = dpf::app::parse_node_args(5, const_cast(argv), dpf::app::run_config{}); dpf::log::settings ls; ls.sinks = args.cfg.log_sinks; dpf::log::configure(ls); dpf::protocol::node x, out; auto plan = open_plan(static_cast(me), x, out); dpf::app::party_values v(plan.nodes().size()); v[x.id].assign(8, 0); const std::uint64_t in = me == 0 ? 40 : 2; std::memcpy(v[x.id].data(), &in, 8); (void)dpf::app::run_node(args, plan, v, {}); std::uint64_t o = 0; std::memcpy(&o, v[out.id].data(), 8); code = o == 42 ? 0 : 2; } catch (const std::exception & e) { std::fprintf(stderr, "party %d: %s\n", me, e.what()); code = 3; } ::_exit(code); } kids[me] = pid; } for (int me = 0; me < 2; ++me) { int status = 0; ASSERT_EQ(::waitpid(kids[me], &status, 0), kids[me]); ASSERT_TRUE(WIFEXITED(status)); EXPECT_EQ(WEXITSTATUS(status), 0) << "party " << me; } for (int me = 0; me < 2; ++me) { std::ifstream in(dir + "/p" + std::to_string(me) + ".log"); const std::string text((std::istreambuf_iterator(in)), std::istreambuf_iterator()); EXPECT_NE(text.find("ev=link.up"), std::string::npos) << text; EXPECT_NE(text.find("auth=key"), std::string::npos) << text; EXPECT_NE(text.find("peer_verified_us=1"), std::string::npos) << text; EXPECT_EQ(text.find("ev=security.unauthenticated"), std::string::npos) << text; } (void)::system(("rm -rf '" + dir + "'").c_str()); }