/// @file dpf/net/client_link.hpp /// @brief Client-to-server links: TLS 1.3, the server always verified. /// @details A client that supplies inputs (for example, one share to each /// party) connects with `connect_server`. It verifies the server /// unless `client_security::verify` is off: a pinned server key, a CA /// chain for the host name, or, when neither is configured, the /// built-in development certificate, which a `client_listener` with no /// certificate or identity presents. The development certificate's /// private key is public, so that pairing works out of the box and is /// logged as providing no security. A server may also check client /// keys (`server_security::client_pins`). Both ends get an ordinary /// `async_stream_array` of `lanes` lanes and the link's /// `link_security`. #ifndef LIBDPF_INCLUDE_DPF_NET_CLIENT_LINK_HPP__ #define LIBDPF_INCLUDE_DPF_NET_CLIENT_LINK_HPP__ #include #include #include #include #include #include "dpf/net/asio_ns.hpp" #include "dpf/log.hpp" #include "dpf/net/async_stream_array.hpp" #include "dpf/net/connect.hpp" #include "dpf/net/link_log.hpp" #include "dpf/net/policy.hpp" #include "dpf/net/security.hpp" #include "dpf/net/socket_tune.hpp" #include "dpf/net/tls.hpp" namespace dpf { namespace net { /// @brief One client link and how it was secured. struct client_connection { #if DPF_HAS_OPENSSL std::shared_ptr context; #endif std::unique_ptr link; link_security security; }; namespace detail { inline constexpr std::uint32_t client_magic = 0x4c435044u; // 'DPCL' #if DPF_HAS_OPENSSL /// @brief Both ends send `{magic, lanes}` inside TLS and must agree. inline void client_hello(asio::io_context & io, tls_stream & s, std::size_t lanes, bool server, std::chrono::milliseconds budget, const std::string & who) { std::uint8_t mine[8]; std::uint8_t theirs[8]; put_u32(mine, client_magic); put_u32(mine + 4, static_cast(lanes)); if (server) { tls_read(io, s, theirs, sizeof(theirs), budget, who); tls_write(io, s, mine, sizeof(mine), budget, who); } else { tls_write(io, s, mine, sizeof(mine), budget, who); tls_read(io, s, theirs, sizeof(theirs), budget, who); } if (get_u32(theirs) != client_magic) throw std::runtime_error(who + ": the peer is not a libdpf client link"); if (get_u32(theirs + 4) != lanes) throw std::runtime_error(who + ": lanes " + std::to_string(get_u32(theirs + 4)) + " vs " + std::to_string(lanes) + " (peer vs this side)"); } #endif } // namespace detail /// @brief Connect to the server at `host:port` and verify it. inline client_connection connect_server(asio::io_context & io, const std::string & host, unsigned short port, const client_security & sec, std::size_t lanes = 1, const wire_policy & pol = {}, const deadlines & lim = {}) { #if DPF_HAS_OPENSSL const std::string where = host + ":" + std::to_string(port); client_connection out; out.context = make_client_tls_context(sec); asio::ip::tcp::socket sock(io); connect_until(sock, host, port, lim.connect); tune_tcp(sock, pol.socket); const int fd = sock.native_handle(); tls_stream s(std::move(sock), *out.context); if (sec.verify && !sec.ca_file.empty()) tls_expect_host(s, sec.server_name.empty() ? host : sec.server_name); tls_handshake(io, s, false, lim.handshake, "client: TLS handshake with " + where); out.security = tls_describe(s); try { check_server(out.security, s, sec, where); } catch (...) { std::error_code e; s.lowest_layer().close(e); throw; } if (!sec.verify) DPF_LOG(error, "client.verify_off").kv("server", where) .kv("detail", "client_verify=off: any server certificate is accepted, so " "this connection is encrypted but the server is not authenticated"); else if (out.security.peer_auth == "development" && log::first_time("client.development." + where)) DPF_LOG(warning, "client.development_certificate").kv("server", where) .kv("detail", "the server presented the built-in development certificate, " "whose private key is public: this connection is encrypted but the " "server is not authenticated (pin its key or configure client_ca)"); detail::client_hello(io, s, lanes, false, lim.handshake, "client link to " + where); out.link = std::make_unique(io, std::move(s), 1, 0, lanes, pol); log_link_up("connect", "server", transport::mux, lanes, 0, 0, fd, pol.socket, &out.security); return out; #else (void)io; (void)host; (void)port; (void)sec; (void)lanes; (void)pol; (void)lim; throw std::logic_error("connect_server: built without OpenSSL"); #endif } /// @brief Server side: accept clients, each on its own TLS link. class client_listener { public: client_listener(asio::io_context & io, server_security sec, std::size_t lanes = 1, wire_policy pol = {}, deadlines lim = {}) : io_(&io), sec_(std::move(sec)), lanes_(lanes), pol_(pol), lim_(lim) { #if DPF_HAS_OPENSSL ctx_ = make_server_tls_context(sec_, development_); if (development_ && log::first_time("server.development")) DPF_LOG(warning, "server.development_certificate") .kv("detail", "presenting the built-in development certificate, whose " "private key is public: clients cannot tell this server from any " "other (set server_cert/server_key or server_identity)"); #else throw std::logic_error("client_listener: built without OpenSSL"); #endif } /// @brief Bind `port` (0 = ephemeral) and return it. unsigned short listen(unsigned short port = 0) { if (!acceptor_) { acceptor_ = std::make_unique(*io_); open_listener(*acceptor_, port); log_listen(acceptor_->local_endpoint().port(), false, true); } return acceptor_->local_endpoint().port(); } bool development() const noexcept { return development_; } /// @brief Wait (up to the accept deadline) for one client. client_connection accept() { listen(0); client_connection out; #if DPF_HAS_OPENSSL out.context = ctx_; asio::ip::tcp::socket sock(*io_); accept_until(*acceptor_, sock, lim_.accept); tune_tcp(sock, pol_.socket); const int fd = sock.native_handle(); tls_stream s(std::move(sock), *ctx_); tls_handshake(*io_, s, true, lim_.handshake, "server: TLS handshake with a client"); out.security = tls_describe(s); check_client(out.security, sec_); detail::client_hello(*io_, s, lanes_, true, lim_.handshake, "client link"); out.link = std::make_unique(*io_, std::move(s), 0, 1, lanes_, pol_); log_link_up("accept", "client", transport::mux, lanes_, 0, 0, fd, pol_.socket, &out.security); if (!sec_.client_pins.empty() && out.security.peer_auth == "none") DPF_LOG(warning, "server.client_unauthenticated") .kv("client_key", out.security.peer_key ? out.security.peer_key->base64() : std::string("none")) .kv("detail", "the client presented no pinned key"); #endif return out; } private: asio::io_context * io_ = nullptr; server_security sec_; std::size_t lanes_ = 1; wire_policy pol_{}; deadlines lim_{}; bool development_ = false; #if DPF_HAS_OPENSSL std::shared_ptr ctx_; #endif std::unique_ptr acceptor_; }; } // namespace net } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_NET_CLIENT_LINK_HPP__