/// @file dpf/net/secure_channel.hpp /// @brief Framed `channel` edges using the same peer TLS policy as `party_session`. /// @details Mux paths go through `party_session`. Framed APIs (`tcp_pair`, /// `trio`) keep length/tag framing but run the same `peer_security` /// defaults: TLS 1.3 when `encrypt` is on, optional per-direction /// authentication, socket tuning, and link logging. #ifndef LIBDPF_INCLUDE_DPF_NET_SECURE_CHANNEL_HPP__ #define LIBDPF_INCLUDE_DPF_NET_SECURE_CHANNEL_HPP__ #include #include #include #include #include #include #include "dpf/log.hpp" #include "dpf/net/channel.hpp" #include "dpf/net/identity.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 This process's key for a party link, or a fresh one (logged). inline std::shared_ptr resolve_peer_identity( const peer_security & sec, const std::string & who) { if (sec.self) { DPF_LOG(info, "security.identity").kv("who", who) .kv("key", sec.self->key().base64()).kv("ephemeral", false) .kv("trusted", sec.trusted.size()); return sec.self; } auto id = std::make_shared(identity::generate()); DPF_LOG(info, "security.identity").kv("who", who).kv("key", id->key().base64()) .kv("ephemeral", true).kv("trusted", sec.trusted.size()); if (log::first_time("security.no_identity." + log::role() + "." + who)) DPF_LOG(warning, "security.no_identity").kv("who", who) .kv("detail", "no identity key configured: links are encrypted to a fresh " "key for this run, so peers cannot authenticate " + who); return id; } inline void note_channel_security(const std::string & peer_role, const link_security & sec) { if (!sec.encrypted) return; const std::string me = log::role().empty() ? std::string("this party") : log::role(); if (sec.peer_auth == "none" && log::first_time("security.unauthenticated." + me + "." + peer_role)) DPF_LOG(warning, "security.unauthenticated").kv("peer", peer_role) .kv("peer_key", sec.peer_key ? sec.peer_key->base64() : std::string("none")) .kv("detail", "no key configured for " + peer_role + ": the link is " "encrypted but " + peer_role + " is not authenticated"); } /// @brief Adopt a TCP socket as a framed channel under `peer_security`. inline channel secure_tcp_channel(asio::io_context & io, asio::ip::tcp::socket sock, bool server, std::uint32_t peer_party, const peer_security & sec = {}, const socket_options & so = {}, std::chrono::milliseconds handshake = std::chrono::milliseconds(30000), const std::string & peer_role = {}, const char * how = "connect") { const std::string who = peer_role.empty() ? ("party " + std::to_string(peer_party)) : peer_role; tune_tcp(sock, so); if (!sec.encrypt) { const int fd = sock.native_handle(); log_link_up(how, who, transport::mux, 1, 0, 0, fd, so, nullptr); return channel(std::move(sock)); } #if DPF_HAS_OPENSSL auto self = resolve_peer_identity(sec, log::role().empty() ? "channel" : log::role()); auto ctx = make_peer_tls_context(*self); auto tls = std::make_unique(std::move(sock), *ctx); try { tls_handshake(io, *tls, server, handshake, who + " TLS"); } catch (const std::system_error & e) { throw std::runtime_error(std::string(e.what()) + " (if the peer has encryption off, set it the same at both ends)"); } link_security desc = tls_describe(*tls); try { check_peer(desc, sec, peer_party, who); } catch (...) { std::error_code e; tls->lowest_layer().close(e); throw; } note_channel_security(who, desc); const int fd = tls->lowest_layer().native_handle(); log_link_up(how, who, transport::mux, 1, 0, 0, fd, so, &desc); return channel::from_tls(io, std::move(*tls), std::move(ctx)); #else (void)io; (void)server; (void)handshake; (void)how; throw std::logic_error("secure_tcp_channel: built without OpenSSL; set " "encryption=off for plaintext links"); #endif } #if DPF_HAS_OPENSSL /// @brief Adopt a unix-domain socket under the same peer TLS policy. inline channel secure_local_channel(asio::io_context & io, asio::local::stream_protocol::socket sock, bool server, std::uint32_t peer_party, const peer_security & sec = {}, std::chrono::milliseconds handshake = std::chrono::milliseconds(30000), const std::string & peer_role = {}, const char * how = "connect") { const std::string who = peer_role.empty() ? ("party " + std::to_string(peer_party)) : peer_role; if (!sec.encrypt) return channel(std::move(sock)); auto self = resolve_peer_identity(sec, log::role().empty() ? "channel" : log::role()); auto ctx = make_peer_tls_context(*self); auto tls = std::make_unique(std::move(sock), *ctx); try { tls_handshake(io, *tls, server, handshake, who + " TLS"); } catch (const std::system_error & e) { throw std::runtime_error(std::string(e.what()) + " (if the peer has encryption off, set it the same at both ends)"); } link_security desc = tls_describe(*tls); try { check_peer(desc, sec, peer_party, who); } catch (...) { std::error_code e; tls->lowest_layer().close(e); throw; } note_channel_security(who, desc); (void)how; return channel::from_tls_local(io, std::move(*tls), std::move(ctx)); } #else inline channel secure_local_channel(asio::io_context &, asio::local::stream_protocol::socket sock, bool, std::uint32_t, const peer_security & sec = {}, std::chrono::milliseconds = {}, const std::string & = {}, const char * = nullptr) { if (sec.encrypt) throw std::logic_error("secure_local_channel: built without OpenSSL; set " "encryption=off for plaintext links"); return channel(std::move(sock)); } #endif } // namespace net } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_NET_SECURE_CHANNEL_HPP__