//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Mutual TLS on the HTTP/1.1 listener, plus the raw :TLS stream API on its // own. A server that asks for a client certificate is only useful if it also // refuses the peers that do not have one, so both directions are asserted. // // The certificate authority and client certificate are minted with the openssl // CLI: a client certificate needs the clientAuth extended key usage, which the // library's built-in development certificate (serverAuth, for listeners) does // not carry. Without openssl the mTLS half is skipped. import Crafter.Network; import std; using namespace Crafter; namespace { int failures = 0; void Check(bool condition, std::string_view what) { if (!condition) { std::println("FAIL: {}", what); ++failures; } } bool HaveCommand(std::string_view name) { const std::string probe = "command -v " + std::string(name) + " >/dev/null 2>&1"; return std::system(probe.c_str()) == 0; } std::unordered_map> Routes() { std::unordered_map> routes; routes["/"] = [](const HTTPRequest&) { return CreateResponseHTTP("200", "authenticated"); }; return routes; } // A throwaway CA and a client certificate signed by it. struct ClientIdentity { std::filesystem::path directory; std::filesystem::path authority; std::filesystem::path certificate; std::filesystem::path privateKey; bool ok = false; ClientIdentity() { directory = std::filesystem::temp_directory_path() / "crafter-network-mtls"; std::error_code error; std::filesystem::remove_all(directory, error); std::filesystem::create_directories(directory); authority = directory / "ca.pem"; certificate = directory / "client.pem"; privateKey = directory / "client-key.pem"; const std::string extensions = (directory / "client.ext").string(); std::ofstream(extensions, std::ios::binary) << "basicConstraints=critical,CA:FALSE\n" << "keyUsage=critical,digitalSignature\n" << "extendedKeyUsage=clientAuth\n"; const std::string command = std::format( "set -e\n" "openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes" " -keyout '{0}/ca-key.pem' -out '{1}' -days 5 -subj '/CN=Crafter Test CA'" " -addext 'basicConstraints=critical,CA:TRUE'" " -addext 'keyUsage=critical,keyCertSign'\n" "openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes" " -keyout '{2}' -out '{0}/client.csr' -subj '/CN=crafter-test-client'\n" "openssl x509 -req -in '{0}/client.csr' -CA '{1}' -CAkey '{0}/ca-key.pem'" " -set_serial 2 -days 5 -extfile '{0}/client.ext' -out '{3}'\n", directory.string(), authority.string(), privateKey.string(), certificate.string()); ok = std::system((command + " >/dev/null 2>&1").c_str()) == 0; } ~ClientIdentity() { std::error_code error; std::filesystem::remove_all(directory, error); } ClientIdentity(const ClientIdentity&) = delete; }; // The :TLS layer without any HTTP on top — the case for anything else that // owns a connected socket and wants a record layer over it. void RawStreamAgainstListener() { ListenerAsyncHTTP1 listener(8099, Routes(), TLSServerCredentials{ .selfSigned = true }); TLSClientCredentials credentials; credentials.caPem = GetSelfSignedCertificatePem().certificate; auto context = TLSContext::Client(credentials); ClientTCP socket("localhost", 8099); std::unique_ptr stream = TLSStream::Connect(socket.socketid, context, "localhost", std::chrono::seconds(5)); Check(stream->Secure(), "a TLSStream reports itself as secure"); Check(stream->Protocol() == "http/1.1", "the raw stream negotiated ALPN"); Check(stream->Version().starts_with("TLS"), "a TLS version was negotiated"); // The listener presents the development certificate, whose subject is // CN=localhost. Check(stream->PeerCertificateSubject().find("localhost") != std::string::npos, "the server certificate subject is readable"); // Hand-written HTTP/1.1 straight down the stream, to prove the record // layer is usable on its own. const std::string request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; stream->Write(request.data(), request.size(), std::chrono::seconds(5)); HTTP1::MessageParser parser(HTTP1::MessageKind::Response); std::vector chunk(4096); while (!parser.Complete()) { std::size_t read = 0; const StreamStatus status = stream->ReadSome(chunk.data(), chunk.size(), std::chrono::seconds(5), read); if (status != StreamStatus::Data) break; parser.Feed(chunk.data(), read); } Check(parser.Complete(), "a response arrived over the raw TLS stream"); if (parser.Complete()) { HTTPResponse response = parser.TakeResponse(); Check(response.status == "200", "raw TLS stream response status"); Check(response.body == "authenticated", "raw TLS stream response body"); } stream.reset(); listener.Stop(); } void MutualTLS() { if (!HaveCommand("openssl")) { std::println("skipping the mTLS half: openssl is not installed"); return; } ClientIdentity identity; if (!identity.ok) { std::println("skipping the mTLS half: could not mint a client certificate"); return; } TLSServerCredentials server; server.selfSigned = true; server.requireClientCertificate = true; server.clientCaPath = identity.authority.string(); ListenerAsyncHTTP1 listener(8100, Routes(), server); // A client with a certificate the listener's CA vouches for. { TLSClientCredentials credentials; credentials.caPem = GetSelfSignedCertificatePem().certificate; credentials.certPath = identity.certificate.string(); credentials.keyPath = identity.privateKey.string(); ClientHTTP1 client("localhost", 8100, credentials); HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/", "localhost")); Check(response.status == "200", "a client with a trusted certificate is served"); Check(response.body == "authenticated", "mTLS response body"); } Check(listener.listener.HandshakeFailureCount() == 0, "a valid client certificate is not a handshake failure"); // The same client, with no certificate at all. Under TLS 1.3 the // server's rejection arrives after the client believes the handshake // finished, so the failure can surface at connect *or* on the first // exchange — either is a refusal, and neither may be a success. { TLSClientCredentials credentials; credentials.caPem = GetSelfSignedCertificatePem().certificate; ClientHTTP1 anonymous("localhost", 8100, credentials); bool refused = false; try { anonymous.Send(CreateRequestHTTP("GET", "/", "localhost")); } catch (const std::exception&) { refused = true; } Check(refused, "a client with no certificate is refused"); } for (int wait = 0; wait < 100; ++wait) { if (listener.listener.HandshakeFailureCount() > 0) break; std::this_thread::sleep_for(std::chrono::milliseconds(20)); } Check(listener.listener.HandshakeFailureCount() == 1, "the refused client is counted as a handshake failure"); listener.Stop(); } } int main() { try { RawStreamAgainstListener(); MutualTLS(); } catch (const std::exception& error) { std::println("threw: {}", error.what()); return 1; } if (failures != 0) { std::println("{} check(s) failed", failures); return 1; } return 0; }