179 lines
8.1 KiB
Text
179 lines
8.1 KiB
Text
|
|
//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 <openssl/*.h> 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<std::string> 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<std::string> 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<TLSContext> Server(const TLSServerCredentials& credentials);
|
||
|
|
static std::shared_ptr<TLSContext> Client(const TLSClientCredentials& credentials);
|
||
|
|
|
||
|
|
~TLSContext();
|
||
|
|
TLSContext(const TLSContext&) = delete;
|
||
|
|
TLSContext& operator=(const TLSContext&) = delete;
|
||
|
|
|
||
|
|
private:
|
||
|
|
TLSContext();
|
||
|
|
struct Impl;
|
||
|
|
std::unique_ptr<Impl> 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<TLSStream> Connect(int descriptor,
|
||
|
|
std::shared_ptr<TLSContext> context,
|
||
|
|
const std::string& hostName,
|
||
|
|
std::chrono::milliseconds timeout);
|
||
|
|
|
||
|
|
// Server side, on a descriptor accept(2) just handed us.
|
||
|
|
static std::unique_ptr<TLSStream> Accept(int descriptor,
|
||
|
|
std::shared_ptr<TLSContext> 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> 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
|