libdpf/include/dpf/net/secure_channel.hpp
Ryan Henry 0d22946a0e Checkpoint the party/runtime stack before share-program and malicious-mode work.
Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 05:59:19 -06:00

177 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__