Crafter.Network/implementations/Crafter.Network-TLS.cpp
catbot b7fe39871b fix(tls): stop a peer that closed first from killing the process
OpenSSL's socket BIO writes with write(2) rather than send(2), so unlike
PlainStream it cannot pass MSG_NOSIGNAL. Writing to a peer that is gone
therefore raised SIGPIPE, and with the default disposition that takes the
whole process down.

This is not an edge case. It fires on any teardown where the far side
closed first, because SSL_shutdown still tries to put a close_notify on
the wire — which is exactly what ShouldSendRecieveHTTPS1 does when it
drops a client whose certificate check failed. The test died on SIGPIPE
with every assertion passing.

Installing a process-wide SIG_IGN would fix it by changing how the
caller's own writes report failure, which a library has no business
doing. SIGPIPE from write(2) is delivered to the writing thread, so block
it for that thread across each OpenSSL call instead and drain any pending
instance before unblocking. A caller who already blocks SIGPIPE is left
untouched — a pending signal there may be theirs to consume.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:18:51 +00:00

721 lines
32 KiB
C++

//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include <poll.h>
#include <arpa/inet.h>
#include <signal.h>
#include <pthread.h>
#include <cerrno>
#include <climits>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/sslerr.h>
#include <openssl/x509v3.h>
module Crafter.Network:TLS_impl;
import :TLS;
import :Stream;
import std;
using namespace Crafter;
namespace {
// ── SIGPIPE ──────────────────────────────────────────────────────────
// OpenSSL's socket BIO writes with write(2), not send(2), so it cannot
// pass MSG_NOSIGNAL the way PlainStream does. Writing to a peer that has
// gone therefore raises SIGPIPE and — with the default disposition — kills
// the process. That is not an edge case: it happens on every teardown
// where the far side closed first, because SSL_shutdown still tries to put
// a close_notify on the wire.
//
// A library must not install a process-wide SIG_IGN on its caller's
// behalf; that would silently change how the caller's own writes behave.
// SIGPIPE from write(2) is delivered to the thread that wrote, so block it
// for this thread across the call instead, and drain any instance that
// went pending while it was blocked so it cannot fire on unblock.
class SigPipeGuard {
public:
SigPipeGuard() {
sigset_t pipeOnly;
sigemptyset(&pipeOnly);
sigaddset(&pipeOnly, SIGPIPE);
sigset_t previous;
// If the caller already blocks SIGPIPE, leave everything alone —
// any pending instance may be theirs to consume, not ours.
blocked = pthread_sigmask(SIG_BLOCK, &pipeOnly, &previous) == 0
&& sigismember(&previous, SIGPIPE) == 0;
restore = previous;
}
~SigPipeGuard() {
if (!blocked) return;
sigset_t pipeOnly;
sigemptyset(&pipeOnly);
sigaddset(&pipeOnly, SIGPIPE);
const timespec immediately{ .tv_sec = 0, .tv_nsec = 0 };
while (sigtimedwait(&pipeOnly, nullptr, &immediately) >= 0) {}
pthread_sigmask(SIG_SETMASK, &restore, nullptr);
}
SigPipeGuard(const SigPipeGuard&) = delete;
private:
sigset_t restore{};
bool blocked = false;
};
// ── OpenSSL plumbing ─────────────────────────────────────────────────
template <typename T, void (*Release)(T*)>
struct Releaser {
void operator()(T* pointer) const noexcept { if (pointer) Release(pointer); }
};
template <typename T, void (*Release)(T*)>
using Owned = std::unique_ptr<T, Releaser<T, Release>>;
using OwnedBio = Owned<BIO, BIO_free_all>;
using OwnedKey = Owned<EVP_PKEY, EVP_PKEY_free>;
using OwnedCertificate = Owned<X509, X509_free>;
// Drain OpenSSL's per-thread error queue into a readable message. Without
// this every TLS failure reads as "handshake failed" with no hint as to
// whether it was the certificate, the version, or the peer hanging up.
std::string Describe(std::string_view what) {
std::string message(what);
bool first = true;
while (const unsigned long code = ERR_get_error()) {
char buffer[256];
ERR_error_string_n(code, buffer, sizeof(buffer));
message += first ? ": " : "; ";
message += buffer;
first = false;
}
return message;
}
// True once OpenSSL has told us the peer vanished without a close_notify.
// For HTTP/1.1 that is an end of connection like any other — the message
// parser is the thing that decides whether it arrived too early.
bool UnexpectedEof() {
const unsigned long code = ERR_peek_error();
return ERR_GET_LIB(code) == ERR_LIB_SSL
&& ERR_GET_REASON(code) == SSL_R_UNEXPECTED_EOF_WHILE_READING;
}
std::string BioToString(BIO* bio) {
char* data = nullptr;
const long length = BIO_get_mem_data(bio, &data);
if (length <= 0 || data == nullptr) return {};
return std::string(data, static_cast<std::size_t>(length));
}
bool IsIpLiteral(const std::string& name) {
in_addr v4{};
in6_addr v6{};
return inet_pton(AF_INET, name.c_str(), &v4) == 1
|| inet_pton(AF_INET6, name.c_str(), &v6) == 1;
}
// ── ALPN ─────────────────────────────────────────────────────────────
// Wire form is a sequence of length-prefixed protocol names.
std::vector<unsigned char> EncodeAlpn(const std::vector<std::string>& protocols) {
std::vector<unsigned char> wire;
for (const std::string& protocol : protocols) {
if (protocol.empty() || protocol.size() > 255) {
throw TLSException("ALPN protocol names must be 1..255 bytes: '" + protocol + "'");
}
wire.push_back(static_cast<unsigned char>(protocol.size()));
wire.insert(wire.end(), protocol.begin(), protocol.end());
}
return wire;
}
// Server-side selection, in *our* preference order rather than the
// client's: the server is the side that knows what it can actually parse.
// No overlap is a fatal no_application_protocol alert (RFC 7301 §3.2) —
// letting the connection through would mean answering HTTP/2 with an
// HTTP/1.1 response and confusing both ends.
int SelectAlpn(SSL*, const unsigned char** out, unsigned char* outLength,
const unsigned char* in, unsigned int inLength, void* argument) {
const auto& preferred = *static_cast<const std::vector<std::string>*>(argument);
for (const std::string& candidate : preferred) {
for (unsigned int offset = 0; offset < inLength;) {
const unsigned int length = in[offset];
if (offset + 1 + length > inLength) break; // malformed list
if (length == candidate.size()
&& std::memcmp(in + offset + 1, candidate.data(), length) == 0) {
*out = in + offset + 1;
*outLength = static_cast<unsigned char>(length);
return SSL_TLSEXT_ERR_OK;
}
offset += 1 + length;
}
}
return SSL_TLSEXT_ERR_ALERT_FATAL;
}
// ── Self-signed certificate ──────────────────────────────────────────
void AddExtension(X509* certificate, X509V3_CTX* context, int nid, const char* value) {
X509_EXTENSION* extension = X509V3_EXT_conf_nid(nullptr, context, nid, value);
if (extension == nullptr) {
throw TLSException(Describe("could not build certificate extension"));
}
const int added = X509_add_ext(certificate, extension, -1);
X509_EXTENSION_free(extension);
if (added != 1) throw TLSException(Describe("could not add certificate extension"));
}
TLSCertificatePem MakeSelfSignedCertificate() {
OwnedKey key(EVP_EC_gen("P-256"));
if (!key) throw TLSException(Describe("could not generate a P-256 key"));
OwnedCertificate certificate(X509_new());
if (!certificate) throw TLSException(Describe("could not allocate a certificate"));
// X509_set_version takes the zero-based version, so 2 is v3 — which is
// what the extensions below require.
X509_set_version(certificate.get(), 2);
ASN1_INTEGER_set(X509_get_serialNumber(certificate.get()), 1);
// Backdated an hour so a peer whose clock runs slightly behind ours
// does not reject a certificate we just minted.
X509_gmtime_adj(X509_getm_notBefore(certificate.get()), -3600);
X509_gmtime_adj(X509_getm_notAfter(certificate.get()), 10 * 24 * 60 * 60);
if (X509_set_pubkey(certificate.get(), key.get()) != 1) {
throw TLSException(Describe("could not set the certificate public key"));
}
X509_NAME* subject = X509_get_subject_name(certificate.get());
X509_NAME_add_entry_by_txt(subject, "CN", MBSTRING_ASC,
reinterpret_cast<const unsigned char*>("localhost"), -1, -1, 0);
// Self-signed: issuer is the subject.
X509_set_issuer_name(certificate.get(), subject);
X509V3_CTX extensionContext;
X509V3_set_ctx_nodb(&extensionContext);
X509V3_set_ctx(&extensionContext, certificate.get(), certificate.get(),
nullptr, nullptr, 0);
AddExtension(certificate.get(), &extensionContext, NID_basic_constraints,
"critical,CA:FALSE");
AddExtension(certificate.get(), &extensionContext, NID_key_usage,
"critical,digitalSignature,keyEncipherment");
AddExtension(certificate.get(), &extensionContext, NID_ext_key_usage, "serverAuth");
// The SANs are what a verifying client actually matches on; a bare CN
// has not been accepted by anything for years.
AddExtension(certificate.get(), &extensionContext, NID_subject_alt_name,
"DNS:localhost,IP:127.0.0.1,IP:::1");
if (X509_sign(certificate.get(), key.get(), EVP_sha256()) == 0) {
throw TLSException(Describe("could not sign the certificate"));
}
TLSCertificatePem pem;
{
OwnedBio bio(BIO_new(BIO_s_mem()));
if (!bio || PEM_write_bio_X509(bio.get(), certificate.get()) != 1) {
throw TLSException(Describe("could not encode the certificate as PEM"));
}
pem.certificate = BioToString(bio.get());
}
{
OwnedBio bio(BIO_new(BIO_s_mem()));
if (!bio || PEM_write_bio_PrivateKey(bio.get(), key.get(), nullptr, nullptr, 0,
nullptr, nullptr) != 1) {
throw TLSException(Describe("could not encode the private key as PEM"));
}
pem.privateKey = BioToString(bio.get());
}
return pem;
}
// ── Credential loading ───────────────────────────────────────────────
OwnedBio MemoryBio(const std::string& contents) {
if (contents.size() > static_cast<std::size_t>(INT_MAX)) {
throw TLSException("PEM blob is implausibly large");
}
OwnedBio bio(BIO_new_mem_buf(contents.data(), static_cast<int>(contents.size())));
if (!bio) throw TLSException(Describe("could not wrap the PEM blob"));
return bio;
}
// Leaf first, then any intermediates, exactly as OpenSSL's own
// *_chain_file loader treats a PEM bundle.
void UseCertificateChainPem(SSL_CTX* context, const std::string& pem) {
OwnedBio bio = MemoryBio(pem);
OwnedCertificate leaf(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr));
if (!leaf) throw TLSException(Describe("could not read the certificate PEM"));
if (SSL_CTX_use_certificate(context, leaf.get()) != 1) {
throw TLSException(Describe("could not install the certificate"));
}
SSL_CTX_clear_chain_certs(context);
for (;;) {
OwnedCertificate extra(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr));
if (!extra) break;
// add0 takes ownership on success, so the pointer is released.
if (SSL_CTX_add0_chain_cert(context, extra.get()) != 1) {
throw TLSException(Describe("could not install a chain certificate"));
}
(void)extra.release();
}
// PEM_read_bio_X509 leaves a "no start line" error behind when it runs
// out of certificates; that is the loop's exit condition, not a fault.
ERR_clear_error();
}
void UsePrivateKeyPem(SSL_CTX* context, const std::string& pem) {
OwnedBio bio = MemoryBio(pem);
OwnedKey key(PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, nullptr));
if (!key) throw TLSException(Describe("could not read the private key PEM"));
if (SSL_CTX_use_PrivateKey(context, key.get()) != 1) {
throw TLSException(Describe("could not install the private key"));
}
}
// A trust anchor path is either a PEM bundle or a hashed directory of
// them; OpenSSL wants to be told which, so look.
void LoadTrustAnchorPath(SSL_CTX* context, const std::string& path) {
std::error_code error;
const bool directory = std::filesystem::is_directory(path, error);
const int loaded = directory
? SSL_CTX_load_verify_locations(context, nullptr, path.c_str())
: SSL_CTX_load_verify_locations(context, path.c_str(), nullptr);
if (loaded != 1) {
throw TLSException(Describe("could not load trust anchors from '" + path + "'"));
}
}
void LoadTrustAnchorPem(SSL_CTX* context, const std::string& pem) {
X509_STORE* store = SSL_CTX_get_cert_store(context);
OwnedBio bio = MemoryBio(pem);
std::size_t added = 0;
for (;;) {
OwnedCertificate anchor(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr));
if (!anchor) break;
if (X509_STORE_add_cert(store, anchor.get()) != 1) {
throw TLSException(Describe("could not add a trust anchor"));
}
++added;
}
ERR_clear_error();
if (added == 0) throw TLSException("caPem contained no certificate");
}
void ApplyCommonOptions(SSL_CTX* context) {
// TLS 1.2 floor: 1.0/1.1 are deprecated (RFC 8996) and nothing we want
// to talk to needs them.
if (SSL_CTX_set_min_proto_version(context, TLS1_2_VERSION) != 1) {
throw TLSException(Describe("could not require TLS 1.2 or newer"));
}
// Partial writes plus a moving write buffer: our Write() loops over
// its own offset, so it must be allowed to make progress a record at a
// time instead of being forced to re-present a byte-identical buffer.
SSL_CTX_set_mode(context, SSL_MODE_ENABLE_PARTIAL_WRITE
| SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
// Renegotiation buys nothing here and is a cheap way for a peer to
// make us do asymmetric crypto on demand.
SSL_CTX_set_options(context, SSL_OP_NO_RENEGOTIATION);
}
}
// ── TLSContext ───────────────────────────────────────────────────────────
struct TLSContext::Impl {
SSL_CTX* context = nullptr;
bool verifyPeer = true;
std::string serverName;
// Server preference list; SelectAlpn holds a pointer to it, so it must
// outlive every SSL made from this context — which it does, being owned
// by the shared_ptr'd TLSContext.
std::vector<std::string> alpn;
~Impl() { if (context) SSL_CTX_free(context); }
};
TLSContext::TLSContext() : impl(std::make_unique<Impl>()) {}
TLSContext::~TLSContext() = default;
std::shared_ptr<TLSContext> TLSContext::Server(const TLSServerCredentials& credentials) {
std::shared_ptr<TLSContext> wrapper(new TLSContext());
Impl& state = *wrapper->impl;
state.context = SSL_CTX_new(TLS_server_method());
if (state.context == nullptr) throw TLSException(Describe("could not create a TLS context"));
ApplyCommonOptions(state.context);
if (!credentials.certPath.empty()) {
if (credentials.keyPath.empty()) {
throw TLSException("certPath was given without a matching keyPath");
}
if (SSL_CTX_use_certificate_chain_file(state.context, credentials.certPath.c_str()) != 1) {
throw TLSException(Describe("could not load the certificate '"
+ credentials.certPath + "'"));
}
if (SSL_CTX_use_PrivateKey_file(state.context, credentials.keyPath.c_str(),
SSL_FILETYPE_PEM) != 1) {
throw TLSException(Describe("could not load the private key '"
+ credentials.keyPath + "'"));
}
} else if (!credentials.certPem.empty()) {
if (credentials.keyPem.empty()) {
throw TLSException("certPem was given without a matching keyPem");
}
UseCertificateChainPem(state.context, credentials.certPem);
UsePrivateKeyPem(state.context, credentials.keyPem);
} else if (credentials.selfSigned) {
const TLSCertificatePem& pem = GetSelfSignedCertificatePem();
UseCertificateChainPem(state.context, pem.certificate);
UsePrivateKeyPem(state.context, pem.privateKey);
} else {
throw TLSException("no server certificate: set certPath/keyPath, certPem/keyPem, "
"or selfSigned for a development certificate");
}
if (SSL_CTX_check_private_key(state.context) != 1) {
throw TLSException(Describe("the private key does not match the certificate"));
}
if (credentials.requireClientCertificate) {
if (!credentials.clientCaPath.empty()) {
LoadTrustAnchorPath(state.context, credentials.clientCaPath);
// Advertise the acceptable issuers so the client can choose a
// certificate instead of guessing.
std::error_code error;
if (!std::filesystem::is_directory(credentials.clientCaPath, error)) {
if (STACK_OF(X509_NAME)* names =
SSL_load_client_CA_file(credentials.clientCaPath.c_str())) {
SSL_CTX_set_client_CA_list(state.context, names);
}
}
} else if (SSL_CTX_set_default_verify_paths(state.context) != 1) {
throw TLSException(Describe("could not load the system trust store"));
}
SSL_CTX_set_verify(state.context,
SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);
}
state.alpn = credentials.alpnProtocols;
if (!state.alpn.empty()) {
// Validate the names now rather than inside the handshake callback,
// where there is nowhere useful to report a bad configuration.
(void)EncodeAlpn(state.alpn);
SSL_CTX_set_alpn_select_cb(state.context, SelectAlpn, &state.alpn);
}
return wrapper;
}
std::shared_ptr<TLSContext> TLSContext::Client(const TLSClientCredentials& credentials) {
std::shared_ptr<TLSContext> wrapper(new TLSContext());
Impl& state = *wrapper->impl;
state.context = SSL_CTX_new(TLS_client_method());
if (state.context == nullptr) throw TLSException(Describe("could not create a TLS context"));
ApplyCommonOptions(state.context);
state.verifyPeer = !credentials.insecureNoServerValidation;
state.serverName = credentials.serverName;
if (state.verifyPeer) {
if (SSL_CTX_set_default_verify_paths(state.context) != 1) {
throw TLSException(Describe("could not load the system trust store"));
}
if (!credentials.caPath.empty()) LoadTrustAnchorPath(state.context, credentials.caPath);
if (!credentials.caPem.empty()) LoadTrustAnchorPem(state.context, credentials.caPem);
SSL_CTX_set_verify(state.context, SSL_VERIFY_PEER, nullptr);
} else {
// The handshake still completes and SSL_get_verify_result still
// reports what it found; nothing acts on it.
SSL_CTX_set_verify(state.context, SSL_VERIFY_NONE, nullptr);
}
if (!credentials.certPath.empty()) {
if (credentials.keyPath.empty()) {
throw TLSException("certPath was given without a matching keyPath");
}
if (SSL_CTX_use_certificate_chain_file(state.context, credentials.certPath.c_str()) != 1) {
throw TLSException(Describe("could not load the client certificate '"
+ credentials.certPath + "'"));
}
if (SSL_CTX_use_PrivateKey_file(state.context, credentials.keyPath.c_str(),
SSL_FILETYPE_PEM) != 1) {
throw TLSException(Describe("could not load the client private key '"
+ credentials.keyPath + "'"));
}
if (SSL_CTX_check_private_key(state.context) != 1) {
throw TLSException(Describe("the client key does not match the client certificate"));
}
}
if (!credentials.alpnProtocols.empty()) {
const std::vector<unsigned char> wire = EncodeAlpn(credentials.alpnProtocols);
if (SSL_CTX_set_alpn_protos(state.context, wire.data(),
static_cast<unsigned int>(wire.size())) != 0) {
throw TLSException(Describe("could not set the ALPN protocol list"));
}
}
return wrapper;
}
// ── TLSStream ────────────────────────────────────────────────────────────
struct TLSStream::Impl {
std::shared_ptr<TLSContext> context;
SSL* ssl = nullptr;
int descriptor = -1;
std::string protocol;
bool shutdownSent = false;
~Impl() { if (ssl) SSL_free(ssl); }
// Drive SSL_connect/SSL_accept to completion, polling for whichever
// direction OpenSSL is waiting on. The deadline covers the whole
// handshake, not each poll, so a peer that dribbles records cannot
// stretch it indefinitely.
void Handshake(bool client, std::chrono::milliseconds timeout) {
const SigPipeGuard noSigPipe;
const auto deadline = std::chrono::steady_clock::now() + timeout;
for (;;) {
ERR_clear_error();
const int result = client ? SSL_connect(ssl) : SSL_accept(ssl);
if (result == 1) break;
const int error = SSL_get_error(ssl, result);
if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) {
const short events = error == SSL_ERROR_WANT_READ ? POLLIN : POLLOUT;
if (!PollDescriptor(descriptor, events, deadline)) {
throw TLSException("TLS handshake timed out");
}
continue;
}
// Certificate problems are the failure people actually hit, and
// OpenSSL's generic queue message for them ("certificate verify
// failed") does not say which check tripped.
const long verified = SSL_get_verify_result(ssl);
if (verified != X509_V_OK) {
throw TLSException(std::string("TLS certificate rejected: ")
+ X509_verify_cert_error_string(verified));
}
if (error == SSL_ERROR_ZERO_RETURN
|| (error == SSL_ERROR_SYSCALL && result == 0)
|| (error == SSL_ERROR_SSL && UnexpectedEof())) {
throw TLSException("the peer closed the connection during the TLS handshake");
}
if (error == SSL_ERROR_SYSCALL) {
throw TLSException(std::string("TLS handshake failed: ") + std::strerror(errno));
}
throw TLSException(Describe("TLS handshake failed"));
}
const unsigned char* selected = nullptr;
unsigned int length = 0;
SSL_get0_alpn_selected(ssl, &selected, &length);
if (selected != nullptr && length != 0) {
protocol.assign(reinterpret_cast<const char*>(selected), length);
}
}
};
TLSStream::TLSStream() : impl(std::make_unique<Impl>()) {}
TLSStream::~TLSStream() {
Shutdown();
}
std::unique_ptr<TLSStream> TLSStream::Connect(int descriptor,
std::shared_ptr<TLSContext> context,
const std::string& hostName,
std::chrono::milliseconds timeout) {
if (!context) throw TLSException("no TLS context");
SetNonBlocking(descriptor);
std::unique_ptr<TLSStream> stream(new TLSStream());
TLSContext::Impl& configuration = *context->impl;
stream->impl->context = std::move(context);
stream->impl->descriptor = descriptor;
SSL* ssl = SSL_new(configuration.context);
if (ssl == nullptr) throw TLSException(Describe("could not create a TLS session"));
stream->impl->ssl = ssl;
if (SSL_set_fd(ssl, descriptor) != 1) {
throw TLSException(Describe("could not attach the socket to the TLS session"));
}
const std::string& name = configuration.serverName.empty()
? hostName : configuration.serverName;
const bool literal = !name.empty() && IsIpLiteral(name);
// SNI carries host names only — an IP literal there is a protocol
// violation and some servers reject the handshake outright (RFC 6066 §3).
if (!name.empty() && !literal && SSL_set_tlsext_host_name(ssl, name.c_str()) != 1) {
throw TLSException(Describe("could not set the SNI host name"));
}
if (configuration.verifyPeer) {
if (name.empty()) {
throw TLSException("certificate verification needs a name to check against; "
"set serverName or use insecureNoServerValidation");
}
SSL_set_hostflags(ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
const int named = literal
? X509_VERIFY_PARAM_set1_ip_asc(SSL_get0_param(ssl), name.c_str())
: SSL_set1_host(ssl, name.c_str());
if (named != 1) {
throw TLSException("could not use '" + name + "' as the name to verify");
}
}
SSL_set_connect_state(ssl);
stream->impl->Handshake(true, timeout);
return stream;
}
std::unique_ptr<TLSStream> TLSStream::Accept(int descriptor,
std::shared_ptr<TLSContext> context,
std::chrono::milliseconds timeout) {
if (!context) throw TLSException("no TLS context");
SetNonBlocking(descriptor);
std::unique_ptr<TLSStream> stream(new TLSStream());
TLSContext::Impl& configuration = *context->impl;
stream->impl->context = std::move(context);
stream->impl->descriptor = descriptor;
SSL* ssl = SSL_new(configuration.context);
if (ssl == nullptr) throw TLSException(Describe("could not create a TLS session"));
stream->impl->ssl = ssl;
if (SSL_set_fd(ssl, descriptor) != 1) {
throw TLSException(Describe("could not attach the socket to the TLS session"));
}
SSL_set_accept_state(ssl);
stream->impl->Handshake(false, timeout);
return stream;
}
StreamStatus TLSStream::ReadSome(char* buffer, std::size_t size,
std::chrono::milliseconds timeout,
std::size_t& read) {
read = 0;
if (size == 0) return StreamStatus::Data;
// A read can put bytes on the wire too — a TLS 1.3 key update, or an alert
// in response to something we refuse.
const SigPipeGuard noSigPipe;
const auto deadline = std::chrono::steady_clock::now() + timeout;
const int wanted = static_cast<int>(std::min<std::size_t>(size, INT_MAX));
for (;;) {
ERR_clear_error();
const int got = SSL_read(impl->ssl, buffer, wanted);
if (got > 0) {
read = static_cast<std::size_t>(got);
return StreamStatus::Data;
}
const int error = SSL_get_error(impl->ssl, got);
switch (error) {
case SSL_ERROR_WANT_READ:
if (!PollDescriptor(impl->descriptor, POLLIN, deadline)) {
return StreamStatus::TimedOut;
}
continue;
// A read can need the socket writable: TLS 1.3 key updates and
// (where allowed) renegotiation both send records mid-read.
case SSL_ERROR_WANT_WRITE:
if (!PollDescriptor(impl->descriptor, POLLOUT, deadline)) {
return StreamStatus::TimedOut;
}
continue;
case SSL_ERROR_ZERO_RETURN:
return StreamStatus::Closed; // close_notify: an orderly end
case SSL_ERROR_SYSCALL:
if (errno == EINTR) continue;
if (got == 0 || errno == 0 || errno == ECONNRESET) return StreamStatus::Closed;
throw TLSException(std::string("TLS read failed: ") + std::strerror(errno));
case SSL_ERROR_SSL:
if (UnexpectedEof()) return StreamStatus::Closed;
throw TLSException(Describe("TLS read failed"));
default:
throw TLSException(Describe("TLS read failed"));
}
}
}
void TLSStream::Write(const void* buffer, std::size_t size,
std::chrono::milliseconds timeout) {
const SigPipeGuard noSigPipe;
const auto deadline = std::chrono::steady_clock::now() + timeout;
const char* data = reinterpret_cast<const char*>(buffer);
std::size_t sent = 0;
while (sent < size) {
const int wanted = static_cast<int>(std::min<std::size_t>(size - sent, INT_MAX));
ERR_clear_error();
const int wrote = SSL_write(impl->ssl, data + sent, wanted);
if (wrote > 0) {
sent += static_cast<std::size_t>(wrote);
continue;
}
const int error = SSL_get_error(impl->ssl, wrote);
switch (error) {
case SSL_ERROR_WANT_READ:
if (!PollDescriptor(impl->descriptor, POLLIN, deadline)) {
throw TLSException("timed out writing to the TLS peer");
}
continue;
case SSL_ERROR_WANT_WRITE:
if (!PollDescriptor(impl->descriptor, POLLOUT, deadline)) {
throw TLSException("timed out writing to the TLS peer");
}
continue;
case SSL_ERROR_ZERO_RETURN:
throw TLSException("the TLS peer closed the connection");
case SSL_ERROR_SYSCALL:
if (errno == EINTR) continue;
throw TLSException(std::string("TLS write failed: ")
+ (errno == 0 ? "the peer closed the connection"
: std::strerror(errno)));
default:
throw TLSException(Describe("TLS write failed"));
}
}
}
void TLSStream::Shutdown() noexcept {
if (!impl || impl->ssl == nullptr || impl->shutdownSent) return;
impl->shutdownSent = true;
// The likeliest SIGPIPE of the lot: by the time a connection is being torn
// down the peer has often gone already.
const SigPipeGuard noSigPipe;
// One attempt only: close_notify goes out, and we deliberately do not
// wait for the peer's. Waiting means blocking a teardown path on a peer
// that may never answer, and every framing decision has already been made
// by the time we get here.
ERR_clear_error();
SSL_shutdown(impl->ssl);
ERR_clear_error();
}
int TLSStream::Descriptor() const noexcept {
return impl ? impl->descriptor : -1;
}
std::string_view TLSStream::Protocol() const noexcept {
return impl ? std::string_view(impl->protocol) : std::string_view();
}
std::string TLSStream::Version() const {
if (!impl || impl->ssl == nullptr) return {};
const char* version = SSL_get_version(impl->ssl);
return version == nullptr ? std::string() : std::string(version);
}
std::string TLSStream::PeerCertificateSubject() const {
if (!impl || impl->ssl == nullptr) return {};
OwnedCertificate peer(SSL_get1_peer_certificate(impl->ssl));
if (!peer) return {};
char buffer[512] = {};
X509_NAME_oneline(X509_get_subject_name(peer.get()), buffer, sizeof(buffer));
return std::string(buffer);
}
const TLSCertificatePem& Crafter::GetSelfSignedCertificatePem() {
// Generated once per process so every listener presents the same
// certificate: a client that was handed it as a trust anchor keeps
// working across reconnects.
static std::mutex mutex;
static std::optional<TLSCertificatePem> cached;
std::lock_guard lock(mutex);
if (!cached) cached = MakeSelfSignedCertificate();
return *cached;
}