/// @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 #include #include #include #include #include #include #include #include #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 #include #include #endif namespace dpf { namespace net { #if DPF_HAS_OPENSSL using tls_stream = asio::ssl::stream; using tls_local_stream = asio::ssl::stream; 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 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 make_peer_tls_context(const identity & self) { auto ctx = std::make_shared(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 make_server_tls_context(const server_security & sec, bool & development) { auto ctx = std::make_shared(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 make_client_tls_context(const client_security & sec) { auto ctx = std::make_shared(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 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 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 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 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 inline std::optional 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 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 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__