libdpf/include/dpf/net/client_link.hpp

219 lines
7.9 KiB
C++
Raw Permalink Normal View History

/// @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 <cstddef>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
#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<tls_context> context;
#endif
std::unique_ptr<async_stream_array> 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<std::uint32_t>(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<async_tls_mux_stream_array>(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<asio::ip::tcp::acceptor>(*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<async_tls_mux_stream_array>(*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<tls_context> ctx_;
#endif
std::unique_ptr<asio::ip::tcp::acceptor> acceptor_;
};
} // namespace net
} // namespace dpf
#endif // LIBDPF_INCLUDE_DPF_NET_CLIENT_LINK_HPP__