diff --git a/include/columnlynx/client/net/tcp/tcp_client.hpp b/include/columnlynx/client/net/tcp/tcp_client.hpp index 4a2193e..ae6fbd6 100644 --- a/include/columnlynx/client/net/tcp/tcp_client.hpp +++ b/include/columnlynx/client/net/tcp/tcp_client.hpp @@ -69,5 +69,6 @@ namespace ColumnLynx::Net::TCP { std::chrono::steady_clock::time_point mLastHeartbeatReceived; std::chrono::steady_clock::time_point mLastHeartbeatSent; int mMissedHeartbeats = 0; + bool mIsHostDomain; }; } \ No newline at end of file diff --git a/include/columnlynx/common/libsodium_wrapper.hpp b/include/columnlynx/common/libsodium_wrapper.hpp index 331855e..2867bd8 100644 --- a/include/columnlynx/common/libsodium_wrapper.hpp +++ b/include/columnlynx/common/libsodium_wrapper.hpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace ColumnLynx { using PublicKey = std::array; // Ed25519 @@ -222,6 +223,57 @@ namespace ColumnLynx::Utils { return result == 1; } + static inline std::vector getCertificateHostname(const std::vector& cert_der) { + std::vector names; + + if (cert_der.empty()) + return names; + + // Parse DER certificate + const unsigned char* p = cert_der.data(); + X509* cert = d2i_X509(nullptr, &p, cert_der.size()); + if (!cert) + return names; + + // --- Subject Alternative Names (SAN) --- + GENERAL_NAMES* san_names = + (GENERAL_NAMES*)X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr); + + if (san_names) { + int san_count = sk_GENERAL_NAME_num(san_names); + for (int i = 0; i < san_count; i++) { + const GENERAL_NAME* current = sk_GENERAL_NAME_value(san_names, i); + if (current->type == GEN_DNS) { + const char* dns_name = (const char*)ASN1_STRING_get0_data(current->d.dNSName); + // Safety: ensure no embedded nulls + if (ASN1_STRING_length(current->d.dNSName) == (int)std::strlen(dns_name)) { + names.emplace_back(dns_name); + } + } + } + GENERAL_NAMES_free(san_names); + } + + // --- Fallback: Common Name (CN) --- + if (names.empty()) { + X509_NAME* subject = X509_get_subject_name(cert); + if (subject) { + int idx = X509_NAME_get_index_by_NID(subject, NID_commonName, -1); + if (idx >= 0) { + X509_NAME_ENTRY* entry = X509_NAME_get_entry(subject, idx); + ASN1_STRING* cn_asn1 = X509_NAME_ENTRY_get_data(entry); + const char* cn_str = (const char*)ASN1_STRING_get0_data(cn_asn1); + if (ASN1_STRING_length(cn_asn1) == (int)std::strlen(cn_str)) { + names.emplace_back(cn_str); + } + } + } + } + + X509_free(cert); + return names; + } + private: std::array mPublicKey; std::array mPrivateKey; diff --git a/src/client/net/tcp/tcp_client.cpp b/src/client/net/tcp/tcp_client.cpp index 4ca2016..d5388a3 100644 --- a/src/client/net/tcp/tcp_client.cpp +++ b/src/client/net/tcp/tcp_client.cpp @@ -3,6 +3,7 @@ // Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details. #include +#include namespace ColumnLynx::Net::TCP { void TCPClient::start() { @@ -24,6 +25,11 @@ namespace ColumnLynx::Net::TCP { // Init connection handshake Utils::log("Sending handshake init to server."); + // Check if hostname or IPv4/IPv6 + sockaddr_in addr4{}; + sockaddr_in6 addr6{}; + self->mIsHostDomain = inet_pton(AF_INET, mHost.c_str(), (void*)(&addr4)) != 1 && inet_pton(AF_INET6, mHost.c_str(), (void*)(&addr6)) != 1; + std::vector payload; payload.reserve(1 + crypto_box_PUBLICKEYBYTES); payload.push_back(Utils::protocolVersion()); @@ -136,7 +142,6 @@ namespace ColumnLynx::Net::TCP { std::vector serverPublicKeyVec(std::begin(mServerPublicKey), std::end(mServerPublicKey)); // Verify server public key - // TODO: Verify / Match hostname of public key to hostname of server if (!Utils::LibSodiumWrapper::verifyCertificateWithSystemCAs(serverPublicKeyVec)) { if (!(*mInsecureMode)) { Utils::error("Server public key verification failed. Terminating connection."); @@ -144,7 +149,29 @@ namespace ColumnLynx::Net::TCP { return; } - Utils::log("Warning: Server public key verification failed, but continuing due to insecure mode."); + Utils::warn("Warning: Server public key verification failed, but continuing due to insecure mode."); + } + + // Extract and verify hostname from certificate if not IP + if (mIsHostDomain) { + std::vector certHostnames = Utils::LibSodiumWrapper::getCertificateHostname(serverPublicKeyVec); + + // Temp: print extracted hostnames if any + for (const auto& hostname : certHostnames) { + Utils::log("Extracted hostname from certificate: " + hostname); + } + + if (certHostnames.empty() || std::find(certHostnames.begin(), certHostnames.end(), mHost) == certHostnames.end()) { + if (!(*mInsecureMode)) { + Utils::error("Server hostname verification failed. Terminating connection."); + disconnect(); + return; + } + + Utils::warn("Warning: Server hostname verification failed, but continuing due to insecure mode."); + } + } else { + Utils::warn("Connecting via IP address, I can't verify the server's identity! You might be getting MITM'd!"); } // Generate and send challenge