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>
This commit is contained in:
parent
695f8e84f7
commit
0d22946a0e
1835 changed files with 170291 additions and 2849 deletions
345
include/dpf/net/tls.hpp
Normal file
345
include/dpf/net/tls.hpp
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
/// @file dpf/net/tls.hpp
|
||||
/// @brief TLS 1.3 for party links and client links (OpenSSL through asio).
|
||||
/// @details Only TLS 1.3 is offered, without session tickets or resumption.
|
||||
/// The TLS layer accepts any certificate; the decision is made right
|
||||
/// after the handshake, before any application byte is read:
|
||||
/// `check_peer` compares a party's key with the one this side holds
|
||||
/// for that party (none held means unauthenticated in that
|
||||
/// direction), and `check_server` applies a client's pins, CA chain
|
||||
/// and host name, or the development-certificate default. Every
|
||||
/// blocking step here pumps the socket's `io_context` until it
|
||||
/// completes or its budget runs out, then closes the socket.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_TLS_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_TLS_HPP__
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
#include "dpf/net/connect.hpp"
|
||||
#include "dpf/net/identity.hpp"
|
||||
#include "dpf/net/security.hpp"
|
||||
|
||||
#if DPF_HAS_OPENSSL
|
||||
#include <asio/ssl.hpp>
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/x509v3.h>
|
||||
#endif
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
#if DPF_HAS_OPENSSL
|
||||
|
||||
using tls_stream = asio::ssl::stream<asio::ip::tcp::socket>;
|
||||
using tls_local_stream = asio::ssl::stream<asio::local::stream_protocol::socket>;
|
||||
using tls_context = asio::ssl::context;
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
inline int tls_accept_any(int, X509_STORE_CTX *) { return 1; }
|
||||
|
||||
inline void tls_common(SSL_CTX * c)
|
||||
{
|
||||
if (SSL_CTX_set_min_proto_version(c, TLS1_3_VERSION) != 1
|
||||
|| SSL_CTX_set_max_proto_version(c, TLS1_3_VERSION) != 1
|
||||
|| SSL_CTX_set_ciphersuites(c,
|
||||
"TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:"
|
||||
"TLS_AES_256_GCM_SHA384")
|
||||
!= 1)
|
||||
throw std::runtime_error(openssl_error("tls: context"));
|
||||
SSL_CTX_set_num_tickets(c, 0);
|
||||
SSL_CTX_set_session_cache_mode(c, SSL_SESS_CACHE_OFF);
|
||||
SSL_CTX_set_options(c, SSL_OP_NO_TICKET);
|
||||
}
|
||||
|
||||
inline void use_identity(SSL_CTX * c, const identity & id)
|
||||
{
|
||||
if (SSL_CTX_use_certificate(c, id.cert()) != 1
|
||||
|| SSL_CTX_use_PrivateKey(c, id.pkey()) != 1
|
||||
|| SSL_CTX_check_private_key(c) != 1)
|
||||
throw std::runtime_error(openssl_error("tls: identity"));
|
||||
}
|
||||
|
||||
/// @brief Run one async step on `s`, pumping `io`, for at most `budget`.
|
||||
template <typename Stream, typename Start>
|
||||
inline std::error_code tls_step(asio::io_context & io, Stream & s,
|
||||
std::chrono::milliseconds budget, Start start)
|
||||
{
|
||||
bool done = false;
|
||||
std::error_code result;
|
||||
start([&done, &result](const std::error_code & ec) {
|
||||
result = ec;
|
||||
done = true;
|
||||
});
|
||||
const auto deadline = setup_clock::now() + budget;
|
||||
bool expired = false;
|
||||
while (!done)
|
||||
{
|
||||
if (!expired && setup_clock::now() >= deadline)
|
||||
{
|
||||
expired = true;
|
||||
std::error_code e;
|
||||
s.lowest_layer().cancel(e);
|
||||
s.lowest_layer().close(e);
|
||||
}
|
||||
if (io.stopped())
|
||||
io.restart();
|
||||
io.run_one_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
// Running out of work stops `io`; leave it ready for the caller's run().
|
||||
if (io.stopped())
|
||||
io.restart();
|
||||
if (expired && result)
|
||||
return std::make_error_code(std::errc::timed_out);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::string tls_failure(const std::string & what, const std::error_code & ec,
|
||||
std::chrono::milliseconds budget)
|
||||
{
|
||||
if (ec == std::errc::timed_out)
|
||||
return what + ": no answer within " + std::to_string(budget.count()) + " ms";
|
||||
return what + ": " + ec.message();
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// @brief Party links: both ends present `self` and request the peer's key.
|
||||
inline std::shared_ptr<tls_context> make_peer_tls_context(const identity & self)
|
||||
{
|
||||
auto ctx = std::make_shared<tls_context>(tls_context::tls);
|
||||
SSL_CTX * c = ctx->native_handle();
|
||||
detail::tls_common(c);
|
||||
detail::use_identity(c, self);
|
||||
SSL_CTX_set_verify(c, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
|
||||
detail::tls_accept_any);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/// @brief Server side of client links. `development` reports whether the
|
||||
/// development certificate is presented.
|
||||
inline std::shared_ptr<tls_context> make_server_tls_context(const server_security & sec,
|
||||
bool & development)
|
||||
{
|
||||
auto ctx = std::make_shared<tls_context>(tls_context::tls);
|
||||
SSL_CTX * c = ctx->native_handle();
|
||||
detail::tls_common(c);
|
||||
development = false;
|
||||
if (!sec.cert_file.empty())
|
||||
{
|
||||
const std::string key = sec.key_file.empty() ? sec.cert_file : sec.key_file;
|
||||
if (SSL_CTX_use_certificate_chain_file(c, sec.cert_file.c_str()) != 1
|
||||
|| SSL_CTX_use_PrivateKey_file(c, key.c_str(), SSL_FILETYPE_PEM) != 1
|
||||
|| SSL_CTX_check_private_key(c) != 1)
|
||||
throw std::runtime_error(detail::openssl_error(
|
||||
("tls: server certificate '" + sec.cert_file + "'").c_str()));
|
||||
}
|
||||
else if (sec.self)
|
||||
detail::use_identity(c, *sec.self);
|
||||
else
|
||||
{
|
||||
detail::use_identity(c, identity::development());
|
||||
development = true;
|
||||
}
|
||||
// Client keys are optional; `check_client` reads whichever arrived.
|
||||
SSL_CTX_set_verify(c, SSL_VERIFY_PEER, detail::tls_accept_any);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/// @brief Client side of client links. Verification happens in `check_server`.
|
||||
inline std::shared_ptr<tls_context> make_client_tls_context(const client_security & sec)
|
||||
{
|
||||
auto ctx = std::make_shared<tls_context>(tls_context::tls);
|
||||
SSL_CTX * c = ctx->native_handle();
|
||||
detail::tls_common(c);
|
||||
if (sec.self)
|
||||
detail::use_identity(c, *sec.self);
|
||||
if (sec.ca_file == "system")
|
||||
{
|
||||
if (SSL_CTX_set_default_verify_paths(c) != 1)
|
||||
throw std::runtime_error(detail::openssl_error("tls: system trust store"));
|
||||
}
|
||||
else if (!sec.ca_file.empty()
|
||||
&& SSL_CTX_load_verify_locations(c, sec.ca_file.c_str(), nullptr) != 1)
|
||||
throw std::runtime_error(detail::openssl_error(
|
||||
("tls: CA file '" + sec.ca_file + "'").c_str()));
|
||||
SSL_CTX_set_verify(c, SSL_VERIFY_PEER, detail::tls_accept_any);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/// @brief Set the name a CA-issued server certificate must carry (and SNI).
|
||||
template <typename Stream>
|
||||
inline void tls_expect_host(Stream & s, const std::string & name)
|
||||
{
|
||||
if (name.empty())
|
||||
return;
|
||||
SSL * ssl = s.native_handle();
|
||||
SSL_set_tlsext_host_name(ssl, name.c_str());
|
||||
if (SSL_set1_host(ssl, name.c_str()) != 1)
|
||||
throw std::runtime_error(detail::openssl_error("tls: server name"));
|
||||
}
|
||||
|
||||
template <typename Stream>
|
||||
inline void tls_handshake(asio::io_context & io, Stream & s, bool server,
|
||||
std::chrono::milliseconds budget, const std::string & what)
|
||||
{
|
||||
const auto ec = detail::tls_step(io, s, budget, [&](auto h) {
|
||||
s.async_handshake(server ? asio::ssl::stream_base::server
|
||||
: asio::ssl::stream_base::client,
|
||||
h);
|
||||
});
|
||||
if (ec)
|
||||
throw std::system_error(ec, detail::tls_failure(what + ": TLS handshake", ec,
|
||||
budget));
|
||||
}
|
||||
|
||||
template <typename Stream>
|
||||
inline void tls_write(asio::io_context & io, Stream & s, const void * p,
|
||||
std::size_t n, std::chrono::milliseconds budget, const std::string & what)
|
||||
{
|
||||
const auto ec = detail::tls_step(io, s, budget, [&](auto h) {
|
||||
asio::async_write(s, asio::buffer(p, n),
|
||||
[h](const std::error_code & e, std::size_t) { h(e); });
|
||||
});
|
||||
if (ec)
|
||||
throw std::system_error(ec, detail::tls_failure(what, ec, budget));
|
||||
}
|
||||
|
||||
template <typename Stream>
|
||||
inline void tls_read(asio::io_context & io, Stream & s, void * p, std::size_t n,
|
||||
std::chrono::milliseconds budget, const std::string & what)
|
||||
{
|
||||
const auto ec = detail::tls_step(io, s, budget, [&](auto h) {
|
||||
asio::async_read(s, asio::buffer(p, n),
|
||||
[h](const std::error_code & e, std::size_t) { h(e); });
|
||||
});
|
||||
if (ec)
|
||||
throw std::system_error(ec, detail::tls_failure(what, ec, budget));
|
||||
}
|
||||
|
||||
/// @brief The Ed25519 key in the peer's certificate, if it sent one.
|
||||
template <typename Stream>
|
||||
inline std::optional<public_key> tls_peer_key(Stream & s)
|
||||
{
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
X509 * x = SSL_get1_peer_certificate(s.native_handle());
|
||||
#else
|
||||
X509 * x = SSL_get_peer_certificate(s.native_handle());
|
||||
#endif
|
||||
if (x == nullptr)
|
||||
return std::nullopt;
|
||||
std::optional<public_key> out;
|
||||
EVP_PKEY * k = X509_get0_pubkey(x);
|
||||
public_key pk;
|
||||
std::size_t n = public_key::size;
|
||||
if (k != nullptr && EVP_PKEY_id(k) == EVP_PKEY_ED25519
|
||||
&& EVP_PKEY_get_raw_public_key(k, pk.bytes.data(), &n) == 1
|
||||
&& n == public_key::size)
|
||||
out = pk;
|
||||
X509_free(x);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief Protocol, cipher, and peer key of an established stream.
|
||||
template <typename Stream>
|
||||
inline link_security tls_describe(Stream & s)
|
||||
{
|
||||
link_security out;
|
||||
out.encrypted = true;
|
||||
SSL * ssl = s.native_handle();
|
||||
out.protocol = SSL_get_version(ssl);
|
||||
const char * cipher = SSL_get_cipher_name(ssl);
|
||||
out.cipher = cipher != nullptr ? cipher : "unknown";
|
||||
out.peer_key = tls_peer_key(s);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief Party link: authenticate `who` if this side holds its key.
|
||||
/// @details Throws when a held key does not match. Otherwise fills
|
||||
/// `sec.peer_auth` with `key` or `none`.
|
||||
inline void check_peer(link_security & sec, const peer_security & policy,
|
||||
std::uint32_t party, const std::string & who)
|
||||
{
|
||||
const public_key * want = policy.trusted_key(party);
|
||||
if (want == nullptr)
|
||||
{
|
||||
sec.peer_auth = "none";
|
||||
return;
|
||||
}
|
||||
if (!sec.peer_key || *sec.peer_key != *want)
|
||||
throw std::runtime_error("security: " + who + " presented key "
|
||||
+ (sec.peer_key ? sec.peer_key->base64() : std::string("(not Ed25519)"))
|
||||
+ " but this side holds " + want->base64() + " for it");
|
||||
sec.peer_auth = "key";
|
||||
}
|
||||
|
||||
/// @brief Client link: apply `policy` to the server behind `s`.
|
||||
/// @details Accepts a pinned key, then a CA chain that matched the expected
|
||||
/// name, then (only when neither is configured) the development
|
||||
/// certificate. `verify` off accepts anything.
|
||||
inline void check_server(link_security & sec, tls_stream & s,
|
||||
const client_security & policy, const std::string & where)
|
||||
{
|
||||
if (!policy.verify)
|
||||
{
|
||||
sec.peer_auth = "none";
|
||||
return;
|
||||
}
|
||||
if (sec.peer_key
|
||||
&& std::find(policy.pins.begin(), policy.pins.end(), *sec.peer_key)
|
||||
!= policy.pins.end())
|
||||
{
|
||||
sec.peer_auth = "key";
|
||||
return;
|
||||
}
|
||||
if (!policy.ca_file.empty() && SSL_get_verify_result(s.native_handle()) == X509_V_OK)
|
||||
{
|
||||
sec.peer_auth = "ca";
|
||||
return;
|
||||
}
|
||||
if (!policy.configured() && sec.peer_key
|
||||
&& *sec.peer_key == identity::development().key())
|
||||
{
|
||||
sec.peer_auth = "development";
|
||||
return;
|
||||
}
|
||||
std::string why;
|
||||
if (!policy.ca_file.empty())
|
||||
why = std::string(" (CA check: ")
|
||||
+ X509_verify_cert_error_string(SSL_get_verify_result(s.native_handle()))
|
||||
+ ")";
|
||||
throw std::runtime_error("security: server " + where + " presented "
|
||||
+ (sec.peer_key ? "key " + sec.peer_key->base64()
|
||||
: std::string("a certificate without an Ed25519 key"))
|
||||
+ ", which this client does not trust" + why
|
||||
+ "; pin it (client_pin=...), configure client_ca, or set client_verify=off");
|
||||
}
|
||||
|
||||
/// @brief Server side: authenticate the client if it presented a pinned key.
|
||||
inline void check_client(link_security & sec, const server_security & policy)
|
||||
{
|
||||
sec.peer_auth = sec.peer_key
|
||||
&& std::find(policy.client_pins.begin(), policy.client_pins.end(),
|
||||
*sec.peer_key)
|
||||
!= policy.client_pins.end()
|
||||
? "key"
|
||||
: "none";
|
||||
}
|
||||
|
||||
#endif // DPF_HAS_OPENSSL
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_TLS_HPP__
|
||||
Loading…
Add table
Add a link
Reference in a new issue