178 lines
6.3 KiB
C++
178 lines
6.3 KiB
C++
|
|
/// @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 <chrono>
|
||
|
|
#include <cstdint>
|
||
|
|
#include <memory>
|
||
|
|
#include <stdexcept>
|
||
|
|
#include <string>
|
||
|
|
#include <utility>
|
||
|
|
|
||
|
|
#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<const identity> 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>(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<tls_stream>(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<tls_local_stream>(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__
|