diff --git a/Makefile.in b/Makefile.in index 74d8896e..734d2421 100644 --- a/Makefile.in +++ b/Makefile.in @@ -42,7 +42,8 @@ endif LIB_SRCS := ZNCString.cpp Csocket.cpp znc.cpp IRCNetwork.cpp User.cpp IRCSock.cpp \ Client.cpp Chan.cpp Nick.cpp Server.cpp Modules.cpp MD5.cpp Buffer.cpp Utils.cpp \ FileUtils.cpp HTTPSock.cpp Template.cpp ClientCommand.cpp Socket.cpp SHA256.cpp \ - WebModules.cpp Listener.cpp Config.cpp ZNCDebug.cpp Threads.cpp version.cpp Query.cpp + WebModules.cpp Listener.cpp Config.cpp ZNCDebug.cpp Threads.cpp version.cpp Query.cpp \ + SSLVerifyHost.cpp LIB_SRCS := $(addprefix src/,$(LIB_SRCS)) BIN_SRCS := src/main.cpp LIB_OBJS := $(patsubst %cpp,%o,$(LIB_SRCS)) diff --git a/NOTICE b/NOTICE index a3480894..5aa4f700 100644 --- a/NOTICE +++ b/NOTICE @@ -10,6 +10,7 @@ ZNC includes modified code of autoconf macro AM_ICONV, licensed by FSF Unlimited ZNC includes modified code of autoconf macro gl_VISIBILITY, licensed by FSF Unlimited License. ZNC includes modified code of MD5 implementation by Christophe Devine, licensed by GPLv2+. ZNC includes resized External Wikipedia icon (https://commons.wikimedia.org/wiki/File:External.svg), licensed by public domain license. +ZNC includes modified code for SSL verification by Alban Diquet (https://github.com/iSECPartners/ssl-conservatory/) and Daniel Stenberg (https://github.com/bagder/curl/blob/master/lib/), licensed by MIT. ZNC is developed by these people: diff --git a/include/znc/IRCNetwork.h b/include/znc/IRCNetwork.h index ab6ef527..827a1aa4 100644 --- a/include/znc/IRCNetwork.h +++ b/include/znc/IRCNetwork.h @@ -121,6 +121,10 @@ public: bool SetNextServer(const CServer* pServer); bool IsLastServer() const; + const SCString& GetTrustedFingerprints() const { return m_ssTrustedFingerprints; } + void AddTrustedFingerprint(const CString& sFP) { m_ssTrustedFingerprints.insert(sFP); } + void DelTrustedFingerprint(const CString& sFP) { m_ssTrustedFingerprints.erase(sFP); } + void SetIRCConnectEnabled(bool b); bool GetIRCConnectEnabled() const { return m_bIRCConnectEnabled; } @@ -201,6 +205,7 @@ protected: CString m_sBindHost; CString m_sEncoding; CString m_sQuitMsg; + SCString m_ssTrustedFingerprints; CModules* m_pModules; diff --git a/include/znc/SSLVerifyHost.h b/include/znc/SSLVerifyHost.h new file mode 100644 index 00000000..405ceb63 --- /dev/null +++ b/include/znc/SSLVerifyHost.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2004-2014 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SSLVERIFYHOST_H +#define SSLVERIFYHOST_H + +#ifdef HAVE_LIBSSL + +#include +#include + +bool ZNC_SSLVerifyHost(const CString& sHost, const X509* pCert, CString& sError); + +#endif /* HAVE_LIBSSL */ + +#endif /* SSLVERIFYHOST_H */ diff --git a/include/znc/Socket.h b/include/znc/Socket.h index 048207d4..395f0378 100644 --- a/include/znc/Socket.h +++ b/include/znc/Socket.h @@ -29,12 +29,30 @@ public: CZNCSock(const CString& sHost, u_short port, int timeout = 60); ~CZNCSock() {} - virtual int ConvertAddress(const struct sockaddr_storage * pAddr, socklen_t iAddrLen, CS_STRING & sIP, u_short * piPort) const; + int ConvertAddress(const struct sockaddr_storage * pAddr, socklen_t iAddrLen, CS_STRING & sIP, u_short * piPort) const override; +#ifdef HAVE_LIBSSL + int VerifyPeerCertificate(int iPreVerify, X509_STORE_CTX * pStoreCTX) override; + void SSLHandShakeFinished() override; +#endif + void SetHostToVerifySSL(const CString& sHost) { m_HostToVerifySSL = sHost; } + CString GetSSLPeerFingerprint() const; + void SetSSLTrustedPeerFingerprints(const SCString& ssFPs) { m_ssTrustedFingerprints = ssFPs; } #ifndef HAVE_ICU // Don't fail to compile when ICU is not enabled void SetEncoding(const CString&) {} #endif + +protected: + // All existing errno codes seem to be in range 1-300 + enum { + errnoBadSSLCert = 12569, + }; + +private: + CString m_HostToVerifySSL; + SCString m_ssTrustedFingerprints; + SCString m_ssCertVerificationErrors; }; enum EAddrType { diff --git a/src/ClientCommand.cpp b/src/ClientCommand.cpp index 96ecc86e..1e1fbed0 100644 --- a/src/ClientCommand.cpp +++ b/src/ClientCommand.cpp @@ -762,6 +762,44 @@ void CClient::UserCommand(CString& sLine) { } else { PutStatus("You don't have any servers added."); } + } else if (sCommand.Equals("AddTrustedServerFingerprint")) { + if (!m_pNetwork) { + PutStatus("You must be connected with a network to use this command"); + return; + } + CString sFP = sLine.Token(1); + if (sFP.empty()) { + PutStatus("Usage: AddTrustedServerFingerprint "); + return; + } + m_pNetwork->AddTrustedFingerprint(sFP); + PutStatus("Done."); + } else if (sCommand.Equals("DelTrustedServerFingerprint")) { + if (!m_pNetwork) { + PutStatus("You must be connected with a network to use this command"); + return; + } + CString sFP = sLine.Token(1); + if (sFP.empty()) { + PutStatus("Usage: DelTrustedServerFingerprint "); + return; + } + m_pNetwork->DelTrustedFingerprint(sFP); + PutStatus("Done."); + } else if (sCommand.Equals("ListTrustedServerFingerprints")) { + if (!m_pNetwork) { + PutStatus("You must be connected with a network to use this command"); + return; + } + const SCString& ssFPs = m_pNetwork->GetTrustedFingerprints(); + if (ssFPs.empty()) { + PutStatus("No fingerprints added."); + } else { + int k = 0; + for (const CString& sFP : ssFPs) { + PutStatus(CString(++k) + ". " + sFP); + } + } } else if (sCommand.Equals("TOPICS")) { if (!m_pNetwork) { PutStatus("You must be connected with a network to use this command"); @@ -1605,6 +1643,10 @@ void CClient::HelpUser(const CString& sFilter) { AddCommandHelp(Table, "AddServer", " [[+]port] [pass]", "Add a server to the list of alternate/backup servers of current IRC network.", sFilter); AddCommandHelp(Table, "DelServer", " [port] [pass]", "Remove a server from the list of alternate/backup servers of current IRC network", sFilter); + AddCommandHelp(Table, "AddTrustedServerFingerprint", "", "Add a trusted server SSL certificate fingerprint (SHA-256) to current IRC network.", sFilter); + AddCommandHelp(Table, "DelTrustedServerFingerprint", "", "Delete a trusted server SSL certificate from current IRC network.", sFilter); + AddCommandHelp(Table, "ListTrustedServerFingerprints", "", "List all trusted server SSL certificates of current IRC network.", sFilter); + AddCommandHelp(Table, "EnableChan", "<#chans>", "Enable channels", sFilter); AddCommandHelp(Table, "DisableChan", "<#chans>", "Disable channels", sFilter); AddCommandHelp(Table, "Detach", "<#chans>", "Detach from channels", sFilter); diff --git a/src/IRCNetwork.cpp b/src/IRCNetwork.cpp index b7c45a65..646331de 100644 --- a/src/IRCNetwork.cpp +++ b/src/IRCNetwork.cpp @@ -176,6 +176,7 @@ void CIRCNetwork::Clone(const CIRCNetwork& Network, bool bCloneName) { SetBindHost(Network.GetBindHost()); SetEncoding(Network.GetEncoding()); SetQuitMsg(Network.GetQuitMsg()); + m_ssTrustedFingerprints = Network.m_ssTrustedFingerprints; // Servers const vector& vServers = Network.GetServers(); @@ -441,6 +442,11 @@ bool CIRCNetwork::ParseConfig(CConfig *pConfig, CString& sError, bool bUpgrade) CUtils::PrintStatus(AddServer(*vit)); } + pConfig->FindStringVector("trustedserverfingerprint", vsList); + for (const CString& sFP : vsList) { + m_ssTrustedFingerprints.insert(sFP); + } + pConfig->FindStringVector("chan", vsList); for (vit = vsList.begin(); vit != vsList.end(); ++vit) { AddChan(*vit, true); @@ -529,6 +535,10 @@ CConfig CIRCNetwork::ToConfig() const { config.AddKeyValuePair("Server", m_vServers[b]->GetString()); } + for (const CString& sFP : m_ssTrustedFingerprints) { + config.AddKeyValuePair("TrustedServerFingerprint", sFP); + } + // Chans for (unsigned int c = 0; c < m_vChans.size(); c++) { CChan* pChan = m_vChans[c]; @@ -1197,6 +1207,7 @@ bool CIRCNetwork::Connect() { CIRCSock *pIRCSock = new CIRCSock(this); pIRCSock->SetPass(pServer->GetPass()); + pIRCSock->SetSSLTrustedPeerFingerprints(m_ssTrustedFingerprints); DEBUG("Connecting user/network [" << m_pUser->GetUserName() << "/" << m_sName << "]"); diff --git a/src/IRCSock.cpp b/src/IRCSock.cpp index 6d9609fd..994ff72c 100644 --- a/src/IRCSock.cpp +++ b/src/IRCSock.cpp @@ -1127,6 +1127,29 @@ void CIRCSock::SockError(int iErrno, const CString& sDescription) { } else { m_pNetwork->PutStatus("Disconnected from IRC (" + sError + "). Reconnecting..."); } +#ifdef HAVE_LIBSSL + if (iErrno == errnoBadSSLCert) { + // Stringify bad cert + X509* pCert = GetX509(); + if (pCert) { + BIO* mem = BIO_new(BIO_s_mem()); + X509_print(mem, pCert); + X509_free(pCert); + char* pCertStr = nullptr; + long iLen = BIO_get_mem_data(mem, &pCertStr); + CString sCert(pCertStr, iLen); + BIO_free(mem); + + VCString vsCert; + sCert.Split("\n", vsCert); + for (const CString& s : vsCert) { + // It shouldn't contain any bad characters, but let's be safe... + m_pNetwork->PutStatus("|" + s.Escape_n(CString::EDEBUG)); + } + m_pNetwork->PutStatus("If you trust this certificate, do /znc AddTrustedServerFingerprint " + GetSSLPeerFingerprint()); + } + } +#endif } m_pNetwork->ClearRawBuffer(); m_pNetwork->ClearMotdBuffer(); diff --git a/src/SSLVerifyHost.cpp b/src/SSLVerifyHost.cpp new file mode 100644 index 00000000..72383aed --- /dev/null +++ b/src/SSLVerifyHost.cpp @@ -0,0 +1,450 @@ +/* + * Copyright (C) 2004-2014 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#ifdef HAVE_LIBSSL + +#include + +namespace ZNC_Curl { +/////////////////////////////////////////////////////////////////////////// +// +// This block is from https://github.com/bagder/curl/blob/master/lib/ +// Copyright: Daniel Stenberg, , license: MIT +// + +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) 1998 - 2014, Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at http://curl.haxx.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ***************************************************************************/ + +/* Portable, consistent toupper (remember EBCDIC). Do not use toupper() because + its behavior is altered by the current locale. */ +inline char Curl_raw_toupper(char in) +{ + switch (in) { + case 'a': + return 'A'; + case 'b': + return 'B'; + case 'c': + return 'C'; + case 'd': + return 'D'; + case 'e': + return 'E'; + case 'f': + return 'F'; + case 'g': + return 'G'; + case 'h': + return 'H'; + case 'i': + return 'I'; + case 'j': + return 'J'; + case 'k': + return 'K'; + case 'l': + return 'L'; + case 'm': + return 'M'; + case 'n': + return 'N'; + case 'o': + return 'O'; + case 'p': + return 'P'; + case 'q': + return 'Q'; + case 'r': + return 'R'; + case 's': + return 'S'; + case 't': + return 'T'; + case 'u': + return 'U'; + case 'v': + return 'V'; + case 'w': + return 'W'; + case 'x': + return 'X'; + case 'y': + return 'Y'; + case 'z': + return 'Z'; + } + return in; +} + +/* +* Curl_raw_equal() is for doing "raw" case insensitive strings. This is meant +* to be locale independent and only compare strings we know are safe for +* this. See http://daniel.haxx.se/blog/2008/10/15/strcasecmp-in-turkish/ for +* some further explanation to why this function is necessary. +* +* The function is capable of comparing a-z case insensitively even for +* non-ascii. +*/ +int Curl_raw_equal(const char *first, const char *second) +{ + while(*first && *second) { + if(Curl_raw_toupper(*first) != Curl_raw_toupper(*second)) + /* get out of the loop as soon as they don't match */ + break; + first++; + second++; + } + /* we do the comparison here (possibly again), just to make sure that if the + loop above is skipped because one of the strings reached zero, we must not + return this as a successful match */ + return (Curl_raw_toupper(*first) == Curl_raw_toupper(*second)); +} +int Curl_raw_nequal(const char *first, const char *second, size_t max) +{ + while(*first && *second && max) { + if(Curl_raw_toupper(*first) != Curl_raw_toupper(*second)) { + break; + } + max--; + first++; + second++; + } + if(0 == max) + return 1; /* they are equal this far */ + return Curl_raw_toupper(*first) == Curl_raw_toupper(*second); +} + +static const int CURL_HOST_NOMATCH = 0; +static const int CURL_HOST_MATCH = 1; + +/* + * Match a hostname against a wildcard pattern. + * E.g. + * "foo.host.com" matches "*.host.com". + * + * We use the matching rule described in RFC6125, section 6.4.3. + * http://tools.ietf.org/html/rfc6125#section-6.4.3 + * + * In addition: ignore trailing dots in the host names and wildcards, so that + * the names are used normalized. This is what the browsers do. + * + * Do not allow wildcard matching on IP numbers. There are apparently + * certificates being used with an IP address in the CN field, thus making no + * apparent distinction between a name and an IP. We need to detect the use of + * an IP address and not wildcard match on such names. + * + * NOTE: hostmatch() gets called with copied buffers so that it can modify the + * contents at will. + */ + +static int hostmatch(char *hostname, char *pattern) +{ + const char *pattern_label_end, *pattern_wildcard, *hostname_label_end; + int wildcard_enabled; + size_t prefixlen, suffixlen; + struct in_addr ignored; +#ifdef ENABLE_IPV6 + struct sockaddr_in6 si6; +#endif + + /* normalize pattern and hostname by stripping off trailing dots */ + size_t len = strlen(hostname); + if(hostname[len-1]=='.') + hostname[len-1]=0; + len = strlen(pattern); + if(pattern[len-1]=='.') + pattern[len-1]=0; + + pattern_wildcard = strchr(pattern, '*'); + if(pattern_wildcard == NULL) + return Curl_raw_equal(pattern, hostname) ? + CURL_HOST_MATCH : CURL_HOST_NOMATCH; + + /* detect IP address as hostname and fail the match if so */ + if(inet_pton(AF_INET, hostname, &ignored) > 0) + return CURL_HOST_NOMATCH; +#ifdef ENABLE_IPV6 + else if(Curl_inet_pton(AF_INET6, hostname, &si6.sin6_addr) > 0) + return CURL_HOST_NOMATCH; +#endif + + /* We require at least 2 dots in pattern to avoid too wide wildcard + match. */ + wildcard_enabled = 1; + pattern_label_end = strchr(pattern, '.'); + if(pattern_label_end == NULL || strchr(pattern_label_end+1, '.') == NULL || + pattern_wildcard > pattern_label_end || + Curl_raw_nequal(pattern, "xn--", 4)) { + wildcard_enabled = 0; + } + if(!wildcard_enabled) + return Curl_raw_equal(pattern, hostname) ? + CURL_HOST_MATCH : CURL_HOST_NOMATCH; + + hostname_label_end = strchr(hostname, '.'); + if(hostname_label_end == NULL || + !Curl_raw_equal(pattern_label_end, hostname_label_end)) + return CURL_HOST_NOMATCH; + + /* The wildcard must match at least one character, so the left-most + label of the hostname is at least as large as the left-most label + of the pattern. */ + if(hostname_label_end - hostname < pattern_label_end - pattern) + return CURL_HOST_NOMATCH; + + prefixlen = pattern_wildcard - pattern; + suffixlen = pattern_label_end - (pattern_wildcard+1); + return Curl_raw_nequal(pattern, hostname, prefixlen) && + Curl_raw_nequal(pattern_wildcard+1, hostname_label_end - suffixlen, + suffixlen) ? + CURL_HOST_MATCH : CURL_HOST_NOMATCH; +} + +int Curl_cert_hostcheck(const char *match_pattern, const char *hostname) +{ + char *matchp; + char *hostp; + int res = 0; + if(!match_pattern || !*match_pattern || + !hostname || !*hostname) /* sanity check */ + ; + else { + matchp = strdup(match_pattern); + if(matchp) { + hostp = strdup(hostname); + if(hostp) { + if(hostmatch(hostp, matchp) == CURL_HOST_MATCH) + res= 1; + free(hostp); + } + free(matchp); + } + } + + return res; +} + +// +// End of https://github.com/bagder/curl/blob/master/lib/ +// +/////////////////////////////////////////////////////////////////////////// +} // namespace ZNC_Curl + +namespace ZNC_iSECPartners { +/////////////////////////////////////////////////////////////////////////// +// +// This block is from https://github.com/iSECPartners/ssl-conservatory/ +// Copyright: Alban Diquet, license: MIT +// + +/* + * Helper functions to perform basic hostname validation using OpenSSL. + * + * Please read "everything-you-wanted-to-know-about-openssl.pdf" before + * attempting to use this code. This whitepaper describes how the code works, + * how it should be used, and what its limitations are. + * + * Author: Alban Diquet + * License: See LICENSE + * + */ + +typedef enum { + MatchFound, + MatchNotFound, + NoSANPresent, + MalformedCertificate, + Error +} HostnameValidationResult; + +#define HOSTNAME_MAX_SIZE 255 + +/** +* Tries to find a match for hostname in the certificate's Common Name field. +* +* Returns MatchFound if a match was found. +* Returns MatchNotFound if no matches were found. +* Returns MalformedCertificate if the Common Name had a NUL character embedded in it. +* Returns Error if the Common Name could not be extracted. +*/ +static HostnameValidationResult matches_common_name(const char *hostname, const X509 *server_cert) { + int common_name_loc = -1; + X509_NAME_ENTRY *common_name_entry = NULL; + ASN1_STRING *common_name_asn1 = NULL; + char *common_name_str = NULL; + + // Find the position of the CN field in the Subject field of the certificate + common_name_loc = X509_NAME_get_index_by_NID(X509_get_subject_name((X509 *) server_cert), NID_commonName, -1); + if (common_name_loc < 0) { + return Error; + } + + // Extract the CN field + common_name_entry = X509_NAME_get_entry(X509_get_subject_name((X509 *) server_cert), common_name_loc); + if (common_name_entry == NULL) { + return Error; + } + + // Convert the CN field to a C string + common_name_asn1 = X509_NAME_ENTRY_get_data(common_name_entry); + if (common_name_asn1 == NULL) { + return Error; + } + common_name_str = (char *) ASN1_STRING_data(common_name_asn1); + + // Make sure there isn't an embedded NUL character in the CN + if (ASN1_STRING_length(common_name_asn1) != static_cast(strlen(common_name_str))) { + return MalformedCertificate; + } + + DEBUG("SSLVerifyHost: Found CN " << common_name_str); + // Compare expected hostname with the CN + if (ZNC_Curl::Curl_cert_hostcheck(common_name_str, hostname)) { + return MatchFound; + } + else { + return MatchNotFound; + } +} + + +/** +* Tries to find a match for hostname in the certificate's Subject Alternative Name extension. +* +* Returns MatchFound if a match was found. +* Returns MatchNotFound if no matches were found. +* Returns MalformedCertificate if any of the hostnames had a NUL character embedded in it. +* Returns NoSANPresent if the SAN extension was not present in the certificate. +*/ +static HostnameValidationResult matches_subject_alternative_name(const char *hostname, const X509 *server_cert) { + HostnameValidationResult result = MatchNotFound; + int i; + int san_names_nb = -1; + STACK_OF(GENERAL_NAME) *san_names = NULL; + + // Try to extract the names within the SAN extension from the certificate + san_names = reinterpret_cast(X509_get_ext_d2i((X509 *) server_cert, NID_subject_alt_name, NULL, NULL)); + if (san_names == NULL) { + return NoSANPresent; + } + san_names_nb = sk_GENERAL_NAME_num(san_names); + + // Check each name within the extension + for (i=0; itype == GEN_DNS) { + // Current name is a DNS name, let's check it + char *dns_name = (char *) ASN1_STRING_data(current_name->d.dNSName); + + // Make sure there isn't an embedded NUL character in the DNS name + if (ASN1_STRING_length(current_name->d.dNSName) != static_cast(strlen(dns_name))) { + result = MalformedCertificate; + break; + } + else { // Compare expected hostname with the DNS name + DEBUG("SSLVerifyHost: Found SAN " << dns_name); + if (ZNC_Curl::Curl_cert_hostcheck(dns_name, hostname)) { + result = MatchFound; + break; + } + } + } + } + sk_GENERAL_NAME_pop_free(san_names, GENERAL_NAME_free); + + return result; +} + + +/** +* Validates the server's identity by looking for the expected hostname in the +* server's certificate. As described in RFC 6125, it first tries to find a match +* in the Subject Alternative Name extension. If the extension is not present in +* the certificate, it checks the Common Name instead. +* +* Returns MatchFound if a match was found. +* Returns MatchNotFound if no matches were found. +* Returns MalformedCertificate if any of the hostnames had a NUL character embedded in it. +* Returns Error if there was an error. +*/ +static HostnameValidationResult validate_hostname(const char *hostname, const X509 *server_cert) { + HostnameValidationResult result; + + if((hostname == NULL) || (server_cert == NULL)) + return Error; + + // First try the Subject Alternative Names extension + result = matches_subject_alternative_name(hostname, server_cert); + if (result == NoSANPresent) { + // Extension was not found: try the Common Name + result = matches_common_name(hostname, server_cert); + } + + return result; +} + +// +// End of https://github.com/iSECPartners/ssl-conservatory/ +// +/////////////////////////////////////////////////////////////////////////// +} // namespace ZNC_iSECPartners + +bool ZNC_SSLVerifyHost(const CString& sHost, const X509* pCert, CString& sError) { + DEBUG("SSLVerifyHost: checking " << sHost); + ZNC_iSECPartners::HostnameValidationResult eResult = ZNC_iSECPartners::validate_hostname(sHost.c_str(), pCert); + switch (eResult) { + case ZNC_iSECPartners::MatchFound: + DEBUG("SSLVerifyHost: verified"); + return true; + case ZNC_iSECPartners::MatchNotFound: + DEBUG("SSLVerifyHost: host doesn't match"); + sError = "hostname doesn't match"; + return false; + case ZNC_iSECPartners::MalformedCertificate: + DEBUG("SSLVerifyHost: malformed cert"); + sError = "malformed hostname in certificate"; + return false; + default: + DEBUG("SSLVerifyHost: error"); + sError = "hostname verification error"; + return false; + } +} + + +#endif /* HAVE_LIBSSL */ diff --git a/src/Socket.cpp b/src/Socket.cpp index 3190c007..ca7ef63c 100644 --- a/src/Socket.cpp +++ b/src/Socket.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -62,6 +63,77 @@ int CZNCSock::ConvertAddress(const struct sockaddr_storage * pAddr, socklen_t iA return ret; } +#ifdef HAVE_LIBSSL +int CZNCSock::VerifyPeerCertificate(int iPreVerify, X509_STORE_CTX * pStoreCTX) { + if (iPreVerify == 0) { + m_ssCertVerificationErrors.insert(X509_verify_cert_error_string(X509_STORE_CTX_get_error(pStoreCTX))); + } + return 1; +} + +void CZNCSock::SSLHandShakeFinished() { + X509* pCert = GetX509(); + if (!pCert) { + DEBUG(GetSockName() + ": No cert"); + CallSockError(errnoBadSSLCert, "Anonymous SSL cert is not allowed"); + Close(); + return; + } + CString sHostVerifyError; + if (!ZNC_SSLVerifyHost(m_HostToVerifySSL, pCert, sHostVerifyError)) { + m_ssCertVerificationErrors.insert(sHostVerifyError); + } + X509_free(pCert); + if (m_ssCertVerificationErrors.empty()) { + DEBUG(GetSockName() + ": Good cert"); + return; + } + CString sFP = GetSSLPeerFingerprint(); + if (m_ssTrustedFingerprints.count(sFP) != 0) { + DEBUG(GetSockName() + ": Cert explicitly trusted by user: " << sFP); + return; + } + DEBUG(GetSockName() + ": Bad cert"); + CString sErrorMsg = "Invalid SSL certificate: "; + sErrorMsg += CString(", ").Join(begin(m_ssCertVerificationErrors), end(m_ssCertVerificationErrors)); + CallSockError(errnoBadSSLCert, sErrorMsg); + Close(); +} +#endif + +CString CZNCSock::GetSSLPeerFingerprint() const { +#ifdef HAVE_LIBSSL + // Csocket's version returns insecure SHA-1 + // This one is SHA-256 + const EVP_MD* evp = EVP_sha256(); + X509* pCert = GetX509(); + if (!pCert) { + DEBUG(GetSockName() + ": GetSSLPeerFingerprint: Anonymous cert"); + return ""; + } + unsigned char buf[256/8]; + unsigned int _32 = 256/8; + int iSuccess = X509_digest(pCert, evp, buf, &_32); + X509_free(pCert); + if (!iSuccess) { + DEBUG(GetSockName() + ": GetSSLPeerFingerprint: Couldn't find digest"); + return ""; + } + CString sResult; + sResult.reserve(3*256/8); + for (char c : buf) { + char b[3]; + snprintf(b, 3, "%02x", c); + sResult += b; + sResult += ":"; + } + sResult.TrimRight(":"); + return sResult; +#else + return ""; +#endif +} + #ifdef HAVE_PTHREAD class CSockManager::CTDNSMonitorFD : public CSMonitorFD { public: @@ -217,6 +289,9 @@ CSockManager::~CSockManager() { } void CSockManager::Connect(const CString& sHostname, u_short iPort, const CString& sSockName, int iTimeout, bool bSSL, const CString& sBindHost, CZNCSock *pcSock) { + if (pcSock) { + pcSock->SetHostToVerifySSL(sHostname); + } #ifdef HAVE_THREADED_DNS DEBUG("TDNS: initiating resolving of [" << sHostname << "] and bindhost [" << sBindHost << "]"); TDNSTask* task = new TDNSTask;