//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Network:TLS; import std; import :Stream; #ifndef CRAFTER_NETWORK_BROWSER namespace Crafter { // TLS over a TCP socket, via libssl (OpenSSL 3). This is the transport // that turns `ClientHTTP1`/`ListenerHTTP1` into `https://` endpoints; it // is deliberately protocol-agnostic, so anything else that owns a // connected descriptor can wrap it the same way. // // No OpenSSL type appears below: the SSL_CTX and SSL live behind the Impl // pointers, so importing this partition does not drag into // the consumer. TLS 1.2 is the floor, the platform's cipher defaults are // used unchanged, and renegotiation is left to OpenSSL's own policy. export class TLSException : public std::runtime_error { public: using std::runtime_error::runtime_error; }; // A certificate and its private key, PEM-encoded. export struct TLSCertificatePem { std::string certificate; std::string privateKey; }; // The certificate the server presents. Exactly one source is used, in // this order: certPath/keyPath, then certPem/keyPem, then selfSigned. // // selfSigned generates an ephemeral in-memory certificate (see // GetSelfSignedCertificatePem) — for development, tests and LAN use. A // client talking to it needs either insecureNoServerValidation or the // certificate itself as a trust anchor. export struct TLSServerCredentials { // PEM files on disk. certPath may hold a chain (leaf first). std::string certPath; std::string keyPath; // The same material inline, for callers that hold it in memory // already (a secret store, a test) and would rather not touch disk. std::string certPem; std::string keyPem; bool selfSigned = false; // Mutual TLS. With requireClientCertificate set, a peer that presents // no certificate — or one that does not chain to clientCaPath — is // rejected during the handshake. clientCaPath is a PEM file or a // directory of them; when it is empty the system trust store is used. std::string clientCaPath; bool requireClientCertificate = false; // Protocols we are willing to speak, in server preference order. A // client that offers ALPN and none of these is rejected with // no_application_protocol (RFC 7301 §3.2) rather than being let // through to speak something we cannot parse. A client that offers no // ALPN at all is accepted — plenty of tooling still does not send it. std::vector alpnProtocols = { "http/1.1" }; }; // How the client checks the server, and what it presents itself. // // The default verifies the chain against the system trust store *and* the // hostname, which is the only combination that is actually safe; a chain // check without a name check accepts any valid certificate for any name. export struct TLSClientCredentials { // Skip both checks. Development only — it accepts any certificate, // including an attacker's. bool insecureNoServerValidation = false; // Extra trust anchor: a PEM file or a directory of them. Added to the // system store rather than replacing it. This is how you talk to a // self-signed listener without giving up verification — hand the // client the server's certificate. std::string caPath; // The same, inline. std::string caPem; // Overrides the name used for SNI and hostname verification. Empty // means the host being connected to, which is what you want unless // you are dialling an address that differs from the certificate name // (a tunnel, a pinned IP). std::string serverName; // Client certificate for mutual TLS. Ignored when the server does not // ask for one. std::string certPath; std::string keyPath; // Protocols to offer, in client preference order. Empty sends no ALPN // extension at all. std::vector alpnProtocols = { "http/1.1" }; }; // A configured SSL_CTX. Shared by every connection it produces — one per // listener, one per client — because the expensive parts (parsing the // certificate, loading the trust store) are per-context, and OpenSSL 3 // lets an SSL_CTX be used concurrently from many threads. // // Held by shared_ptr: a TLSStream keeps its context alive, so a listener // that goes away mid-connection does not pull the configuration out from // under a session still using it. export class TLSContext { public: static std::shared_ptr Server(const TLSServerCredentials& credentials); static std::shared_ptr Client(const TLSClientCredentials& credentials); ~TLSContext(); TLSContext(const TLSContext&) = delete; TLSContext& operator=(const TLSContext&) = delete; private: TLSContext(); struct Impl; std::unique_ptr impl; friend class TLSStream; }; // A TLS session over an already-connected descriptor. Non-owning, like // PlainStream: the descriptor stays owned by its ClientTCP, and this only // adds the record layer on top. // // Both factories complete the handshake before returning, so a stream you // hold is a stream you can write to. They throw TLSException on // certificate rejection, on a protocol mismatch, and on a peer that stops // answering mid-handshake. export class TLSStream final : public ByteStream { public: // Client side. `hostName` drives SNI and hostname verification unless // the credentials overrode it with serverName; an IP literal sets no // SNI (RFC 6066 forbids it) and is checked against the certificate's // iPAddress SANs instead. static std::unique_ptr Connect(int descriptor, std::shared_ptr context, const std::string& hostName, std::chrono::milliseconds timeout); // Server side, on a descriptor accept(2) just handed us. static std::unique_ptr Accept(int descriptor, std::shared_ptr context, std::chrono::milliseconds timeout); ~TLSStream() override; StreamStatus ReadSome(char* buffer, std::size_t size, std::chrono::milliseconds timeout, std::size_t& read) override; void Write(const void* buffer, std::size_t size, std::chrono::milliseconds timeout) override; void Shutdown() noexcept override; int Descriptor() const noexcept override; std::string_view Protocol() const noexcept override; bool Secure() const noexcept override { return true; } // The negotiated protocol version, e.g. "TLSv1.3". For logging. std::string Version() const; // One-line subject of the peer's certificate, empty when it presented // none. With requireClientCertificate a non-empty value is the // authenticated client identity. std::string PeerCertificateSubject() const; private: TLSStream(); struct Impl; std::unique_ptr impl; }; // The process-wide ephemeral self-signed certificate used by // TLSServerCredentials{selfSigned=true}, in PEM form. Generated on first // call and then cached, so every listener in a process presents the same // certificate and a client can be handed it as a trust anchor. // // ECDSA P-256, CN=localhost, SAN {DNS:localhost, IP:127.0.0.1, IP:::1}, // valid for 10 days. Development and tests only — it is regenerated on // every process start and no peer has any reason to trust it. export const TLSCertificatePem& GetSelfSignedCertificatePem(); } #endif