diff --git a/depends/packages/openssl.mk b/depends/packages/openssl.mk index fe19c6734..438092a36 100644 --- a/depends/packages/openssl.mk +++ b/depends/packages/openssl.mk @@ -13,40 +13,40 @@ $(package)_config_opts+=no-async $(package)_config_opts+=no-bf $(package)_config_opts+=no-blake2 $(package)_config_opts+=no-camellia -$(package)_config_opts+=no-capieng +# $(package)_config_opts+=no-capieng $(package)_config_opts+=no-cast $(package)_config_opts+=no-chacha $(package)_config_opts+=no-cmac $(package)_config_opts+=no-cms -$(package)_config_opts+=no-comp +# $(package)_config_opts+=no-comp $(package)_config_opts+=no-crypto-mdebug $(package)_config_opts+=no-crypto-mdebug-backtrace -$(package)_config_opts+=no-ct +# $(package)_config_opts+=no-ct $(package)_config_opts+=no-des $(package)_config_opts+=no-dgram -$(package)_config_opts+=no-dsa +# $(package)_config_opts+=no-dsa $(package)_config_opts+=no-dso $(package)_config_opts+=no-dtls $(package)_config_opts+=no-dtls1 $(package)_config_opts+=no-dtls1-method $(package)_config_opts+=no-dynamic-engine -$(package)_config_opts+=no-ec2m -$(package)_config_opts+=no-ec_nistp_64_gcc_128 +# $(package)_config_opts+=no-ec2m +# $(package)_config_opts+=no-ec_nistp_64_gcc_128 $(package)_config_opts+=no-egd $(package)_config_opts+=no-engine -$(package)_config_opts+=no-err +# $(package)_config_opts+=no-err $(package)_config_opts+=no-gost $(package)_config_opts+=no-heartbeats -$(package)_config_opts+=no-idea +# $(package)_config_opts+=no-idea $(package)_config_opts+=no-md2 $(package)_config_opts+=no-md4 $(package)_config_opts+=no-mdc2 $(package)_config_opts+=no-multiblock $(package)_config_opts+=no-nextprotoneg $(package)_config_opts+=no-ocb -$(package)_config_opts+=no-ocsp +# $(package)_config_opts+=no-ocsp $(package)_config_opts+=no-poly1305 -$(package)_config_opts+=no-posix-io +# $(package)_config_opts+=no-posix-io $(package)_config_opts+=no-psk $(package)_config_opts+=no-rc2 $(package)_config_opts+=no-rc4 @@ -58,24 +58,24 @@ $(package)_config_opts+=no-scrypt $(package)_config_opts+=no-sctp $(package)_config_opts+=no-seed $(package)_config_opts+=no-shared -$(package)_config_opts+=no-sock +# $(package)_config_opts+=no-sock $(package)_config_opts+=no-srp $(package)_config_opts+=no-srtp $(package)_config_opts+=no-ssl $(package)_config_opts+=no-ssl3 $(package)_config_opts+=no-ssl3-method $(package)_config_opts+=no-ssl-trace -$(package)_config_opts+=no-stdio -$(package)_config_opts+=no-tls -$(package)_config_opts+=no-tls1 -$(package)_config_opts+=no-tls1-method +# $(package)_config_opts+=no-stdio +# $(package)_config_opts+=no-tls +# $(package)_config_opts+=no-tls1 +# $(package)_config_opts+=no-tls1-method $(package)_config_opts+=no-ts $(package)_config_opts+=no-ui $(package)_config_opts+=no-unit-test $(package)_config_opts+=no-weak-ssl-ciphers $(package)_config_opts+=no-whirlpool -$(package)_config_opts+=no-zlib -$(package)_config_opts+=no-zlib-dynamic +# $(package)_config_opts+=no-zlib +# $(package)_config_opts+=no-zlib-dynamic $(package)_config_opts+=$($(package)_cflags) $($(package)_cppflags) $(package)_config_opts+=-DPURIFY $(package)_config_opts_linux=-fPIC -Wa,--noexecstack diff --git a/qa/rpc-tests/nodehandling.py b/qa/rpc-tests/nodehandling.py index 391a935d0..1ce93b476 100755 --- a/qa/rpc-tests/nodehandling.py +++ b/qa/rpc-tests/nodehandling.py @@ -62,5 +62,22 @@ class NodeHandlingTest (BitcoinTestFramework): found = True assert(found) + ########################### + # Connection to self (takes approx 5 min) + # Stress test for network layer. Trying to connect to self every 0.5 sec. + # Helps to discover different multi-threading problems. + ########################### + url = urlparse.urlparse(self.nodes[0].url) + + print "Connection to self stress test. " \ + "Constantly trying to connect to self every 0.5 sec. " \ + "The whole test takes approx 5 mins" + for x in xrange(600): + connect_nodes(self.nodes[0], 0) + time.sleep(0.5) + # self-connection should be disconnected during the version checking + for node in self.nodes[0].getpeerinfo(): + assert(node['addr'] != url.hostname+":"+str(p2p_port(0))) + if __name__ == '__main__': NodeHandlingTest ().main () diff --git a/src/Makefile.am b/src/Makefile.am index 5a72e5bd3..09e487c31 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -182,6 +182,7 @@ BITCOIN_CORE_H = \ utilmoneystr.h \ utilstrencodings.h \ utiltime.h \ + utiltls.h \ validationinterface.h \ version.h \ wallet/asyncrpcoperation_sendmany.h \ @@ -240,6 +241,7 @@ libbitcoin_server_a_SOURCES = \ torcontrol.cpp \ txdb.cpp \ txmempool.cpp \ + utiltls.cpp \ validationinterface.cpp \ $(BITCOIN_CORE_H) \ $(LIBZCASH_H) diff --git a/src/init.cpp b/src/init.cpp index 0c29e3f03..91c98ef15 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -394,6 +394,11 @@ std::string HelpMessage(HelpMessageMode mode) strUsage += HelpMessageOpt("-timeout=", strprintf(_("Specify connection timeout in milliseconds (minimum: 1, default: %d)"), DEFAULT_CONNECT_TIMEOUT)); strUsage += HelpMessageOpt("-torcontrol=:", strprintf(_("Tor control port to use if onion listening enabled (default: %s)"), DEFAULT_TOR_CONTROL)); strUsage += HelpMessageOpt("-torpassword=", _("Tor control port password (default: empty)")); + strUsage += HelpMessageOpt("-tlsvalidate=<0 or 1>", _("Connect to peers only with valid certificates (default: 0)")); + strUsage += HelpMessageOpt("-tlskeypath=", _("Full path to a private key")); + strUsage += HelpMessageOpt("-tlskeypwd=", _("Password for a private key encryption (default: not set, i.e. private key will be stored unencrypted)")); + strUsage += HelpMessageOpt("-tlscertpath=", _("Full path to a certificate")); + strUsage += HelpMessageOpt("-tlstrustdir=", _("Full path to a trusted certificates directory")); strUsage += HelpMessageOpt("-whitebind=", _("Bind to given address and whitelist peers connecting to it. Use [host]:port notation for IPv6")); strUsage += HelpMessageOpt("-whitelist=", _("Whitelist peers connecting from the given netmask or IP address. Can be specified multiple times.") + " " + _("Whitelisted peers cannot be DoS banned and their transactions are always relayed, even if they are already in the mempool, useful e.g. for a gateway")); @@ -1263,6 +1268,24 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) BOOST_FOREACH(const std::string& strDest, mapMultiArgs["-seednode"]) AddOneShot(strDest); + if (mapArgs.count("-tlskeypath")) { + boost::filesystem::path pathTLSKey(GetArg("-tlskeypath", "")); + if (!boost::filesystem::exists(pathTLSKey)) + return InitError(strprintf(_("Cannot find TLS key file: '%s'"), pathTLSKey.string())); + } + + if (mapArgs.count("-tlscertpath")) { + boost::filesystem::path pathTLSCert(GetArg("-tlscertpath", "")); + if (!boost::filesystem::exists(pathTLSCert)) + return InitError(strprintf(_("Cannot find TLS cert file: '%s'"), pathTLSCert.string())); + } + + if (mapArgs.count("-tlstrustdir")) { + boost::filesystem::path pathTLSTrustredDir(GetArg("-tlstrustdir", "")); + if (!boost::filesystem::exists(pathTLSTrustredDir)) + return InitError(strprintf(_("Cannot find trusted certificates directory: '%s'"), pathTLSTrustredDir.string())); + } + #if ENABLE_ZMQ pzmqNotificationInterface = CZMQNotificationInterface::CreateWithArguments(mapArgs); diff --git a/src/metrics.cpp b/src/metrics.cpp index 1c87a3426..b2f60ae9f 100644 --- a/src/metrics.cpp +++ b/src/metrics.cpp @@ -202,12 +202,14 @@ int printStats(bool mining) int height; int64_t tipmediantime; size_t connections; + size_t tlsConnections; int64_t netsolps; { LOCK2(cs_main, cs_vNodes); height = chainActive.Height(); tipmediantime = chainActive.Tip()->GetMedianTimePast(); connections = vNodes.size(); + tlsConnections = std::count_if(vNodes.begin(), vNodes.end(), [](CNode* n) {return n->ssl != NULL;}); netsolps = GetNetworkHashPS(120, -1); } auto localsolps = GetLocalSolPS(); @@ -219,7 +221,7 @@ int printStats(bool mining) } else { std::cout << " " << _("Block height") << " | " << height << std::endl; } - std::cout << " " << _("Connections") << " | " << connections << std::endl; + std::cout << " " << _("Connections") << " | " << connections << " (TLS: " << tlsConnections << ")" << std::endl; std::cout << " " << _("Network solution rate") << " | " << netsolps << " Sol/s" << std::endl; if (mining && miningTimer.running()) { std::cout << " " << _("Local solution rate") << " | " << strprintf("%.4f Sol/s", localsolps) << std::endl; diff --git a/src/net.cpp b/src/net.cpp index 05f427707..9b3fd0462 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -16,6 +16,7 @@ #include "scheduler.h" #include "ui_interface.h" #include "crypto/common.h" +#include "utiltls.h" #ifdef WIN32 #include @@ -26,6 +27,10 @@ #include #include +#include +#include +#include + // Dump addresses to peers.dat every 15 minutes (900s) #define DUMP_ADDRESSES_INTERVAL 900 @@ -44,6 +49,12 @@ #endif #endif +#define USE_TLS +#define COMPAT_NON_TLS // enables compatibility with nodes, that still doesn't support TLS connections + +typedef enum {sslAccept, sslConnect, sslShutdown} SSLConnectionRoutine; +typedef enum {clientContext, serverContext} TLSContextType; + using namespace std; namespace { @@ -99,6 +110,28 @@ boost::condition_variable messageHandlerCondition; static CNodeSignals g_signals; CNodeSignals& GetNodeSignals() { return g_signals; } +// OpenSSL server and client contexts +SSL_CTX *tls_ctx_server, *tls_ctx_client; + +typedef struct _NODE_ADDR +{ + std::string ipAddr; + int64_t time; // time in msec, of an attempt to connect via TLS + + _NODE_ADDR(std::string _ipAddr, int64_t _time = 0) : ipAddr(_ipAddr), time(_time){} +} NODE_ADDR, *PNODE_ADDR; + +static bool operator==(_NODE_ADDR a, _NODE_ADDR b) +{ + return (a.ipAddr == b.ipAddr); +} + +static std::vector vNonTLSNodesInbound; +static CCriticalSection cs_vNonTLSNodesInbound; + +static std::vector vNonTLSNodesOutbound; +static CCriticalSection cs_vNonTLSNodesOutbound; + void AddOneShot(const std::string& strDest) { LOCK(cs_vOneShots); @@ -351,6 +384,130 @@ CNode* FindNode(const CService& addr) return NULL; } +#ifdef USE_TLS + +static int WaitFor(SSLConnectionRoutine eRoutine, SOCKET hSocket, SSL *ssl, int timeoutSec) +{ + int nErr = 0; + + while (true) + { + switch (eRoutine) + { + case sslConnect: + nErr = SSL_connect(ssl); + break; + + case sslAccept: + nErr = SSL_accept(ssl); + break; + + case sslShutdown: + nErr = SSL_shutdown(ssl); + break; + + default: + return -1; + } + + if (eRoutine == sslShutdown) + { + if (nErr >= 0) + break; + } + else + { + if (nErr == 1) + break; + } + + int sslErr = SSL_get_error(ssl, nErr); + + if (sslErr != SSL_ERROR_WANT_READ && sslErr != SSL_ERROR_WANT_WRITE) + { + LogPrint("net", "TLS: WARNING: %s: %s: ssl_err_code: %s; errno: %s\n", __FILE__, __func__, ERR_error_string(sslErr, NULL), strerror(errno)); + nErr = -1; + break; + } + + fd_set socketSet; + FD_ZERO(&socketSet); + FD_SET(hSocket, &socketSet); + + struct timeval timeout = { timeoutSec, 0 }; + + if (sslErr == SSL_ERROR_WANT_READ) + { + int result = select(hSocket + 1, &socketSet, NULL, NULL, &timeout); + if (result == 0) + { + LogPrint("net", "TLS: ERROR: %s: %s: WANT_READ timeout\n", __FILE__, __func__); + nErr = -1; + break; + } + else if (result == -1) + { + LogPrint("net", "TLS: ERROR: %s: %s: WANT_READ ssl_err_code: %s; errno: %s\n", __FILE__, __func__, ERR_error_string(sslErr, NULL), strerror(errno)); + nErr = -1; + break; + } + } + else + { + int result = select(hSocket + 1, NULL, &socketSet, NULL, &timeout); + if (result == 0) + { + LogPrint("net", "TLS: ERROR: %s: %s: WANT_WRITE timeout\n", __FILE__, __func__); + nErr = -1; + break; + } + else if (result == -1) + { + LogPrint("net", "TLS: ERROR: %s: %s: WANT_WRITE ssl_err_code: %s; errno: %s\n", __FILE__, __func__, ERR_error_string(sslErr, NULL), strerror(errno)); + nErr = -1; + break; + } + } + } + + return nErr; +} + +static SSL* TLSConnect(SOCKET hSocket, const CAddress &addrConnect) +{ + LogPrint("net", "TLS: establishing connection (tid = %X), (peerid = %s)\n", pthread_self(), addrConnect.ToString()); + + SSL *ssl = NULL; + bool bConnectedTLS = false; + + if ((ssl = SSL_new(tls_ctx_client))) + { + if (SSL_set_fd(ssl, hSocket)) + { + if (WaitFor(sslConnect, hSocket, ssl, (DEFAULT_CONNECT_TIMEOUT / 1000)) == 1) + bConnectedTLS = true; + } + } + + if (bConnectedTLS) + { + LogPrintf ("TLS: connection to %s has been established. Using cipher: %s\n", addrConnect.ToString(), SSL_get_cipher(ssl)); + } + else + { + LogPrintf ("TLS: %s: %s: TLS connection to %s failed\n", __FILE__, __func__, addrConnect.ToString()); + + if (ssl) + { + SSL_free(ssl); + ssl = NULL; + } + } + return ssl; +} + +#endif // USE_TLS + CNode* ConnectNode(CAddress addrConnect, const char *pszDest) { if (pszDest == NULL) { @@ -385,8 +542,67 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest) addrman.Attempt(addrConnect); + SSL *ssl = NULL; + +#ifdef USE_TLS + /* TCP connection is ready. Do client side SSL. */ +#ifdef COMPAT_NON_TLS + { + LOCK(cs_vNonTLSNodesOutbound); + + NODE_ADDR nodeAddr(addrConnect.ToStringIP()); + + bool bUseTLS = (find(vNonTLSNodesOutbound.begin(), + vNonTLSNodesOutbound.end(), + nodeAddr) == vNonTLSNodesOutbound.end()); + if (bUseTLS) + { + ssl = TLSConnect(hSocket, addrConnect); + if (!ssl) + { + // Further reconnection will be made in non-TLS (unencrypted) mode + vNonTLSNodesOutbound.push_back(NODE_ADDR(addrConnect.ToStringIP(), GetTimeMillis())); + CloseSocket(hSocket); + return NULL; + } + } + else + { + LogPrintf ("Connection to %s will be unencrypted\n", addrConnect.ToString()); + + vNonTLSNodesOutbound.erase( + remove( + vNonTLSNodesOutbound.begin(), + vNonTLSNodesOutbound.end(), + nodeAddr), + vNonTLSNodesOutbound.end()); + } + } +#else + ssl = TLSConnect(hSocket, addrConnect); + if(!ssl) + { + CloseSocket(hSocket); + return NULL; + } +#endif // COMPAT_NON_TLS + + if (GetBoolArg("-tlsvalidate", false)) + { + if (ssl && !ValidatePeerCertificate(ssl)) + { + LogPrintf ("TLS: ERROR: Wrong server certificate from %s. Connection will be closed.\n", addrConnect.ToString()); + + SSL_shutdown(ssl); + CloseSocket(hSocket); + SSL_free(ssl); + return NULL; + } + } +#endif // USE_TLS + // Add node - CNode* pnode = new CNode(hSocket, addrConnect, pszDest ? pszDest : "", false); + CNode* pnode = new CNode(hSocket, addrConnect, pszDest ? pszDest : "", false, ssl); pnode->AddRef(); { @@ -409,10 +625,24 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest) void CNode::CloseSocketDisconnect() { fDisconnect = true; - if (hSocket != INVALID_SOCKET) + { - LogPrint("net", "disconnecting peer=%d\n", id); - CloseSocket(hSocket); + LOCK(cs_hSocket); + + if (hSocket != INVALID_SOCKET) + { + LogPrint("net", "disconnecting peer=%d\n", id); + + if (ssl) + { + WaitFor(sslShutdown, hSocket, ssl, (DEFAULT_CONNECT_TIMEOUT / 1000)); + + SSL_free(ssl); + ssl = NULL; + } + + CloseSocket(hSocket); + } } // in case this fails, we'll empty the recv buffer when the CNode is deleted @@ -570,6 +800,13 @@ void CNode::copyStats(CNodeStats &stats) // Leave string empty if addrLocal invalid (not filled in yet) stats.addrLocal = addrLocal.IsValid() ? addrLocal.ToString() : ""; + + // If ssl != NULL it means TLS connection was established successfully + { + LOCK(cs_hSocket); + stats.fTLSEstablished = (ssl != NULL) && (SSL_get_state(ssl) == TLS_ST_OK); + stats.fTLSVerified = (ssl != NULL) && ValidatePeerCertificate(ssl); + } } #undef X @@ -659,52 +896,97 @@ int CNetMessage::readData(const char *pch, unsigned int nBytes) return nCopy; } - - - - - - - - // requires LOCK(cs_vSend) void SocketSendData(CNode *pnode) { std::deque::iterator it = pnode->vSendMsg.begin(); - while (it != pnode->vSendMsg.end()) { + while (it != pnode->vSendMsg.end()) + { const CSerializeData &data = *it; assert(data.size() > pnode->nSendOffset); - int nBytes = send(pnode->hSocket, &data[pnode->nSendOffset], data.size() - pnode->nSendOffset, MSG_NOSIGNAL | MSG_DONTWAIT); - if (nBytes > 0) { + + bool bIsSSL = false; + int nBytes = 0, nRet = 0; + + { + LOCK(pnode->cs_hSocket); + + if (pnode->hSocket == INVALID_SOCKET) + { + LogPrint("net", "Send: connection with %s is already closed\n", pnode->addr.ToString()); + break; + } + + bIsSSL = (pnode->ssl != NULL); + + if (bIsSSL) + { + nBytes = SSL_write(pnode->ssl, &data[pnode->nSendOffset], data.size() - pnode->nSendOffset); + nRet = SSL_get_error(pnode->ssl, nBytes); + } + else + { + nBytes = send(pnode->hSocket, &data[pnode->nSendOffset], data.size() - pnode->nSendOffset, MSG_NOSIGNAL | MSG_DONTWAIT); + nRet = WSAGetLastError(); + } + } + if (nBytes > 0) + { pnode->nLastSend = GetTime(); pnode->nSendBytes += nBytes; pnode->nSendOffset += nBytes; pnode->RecordBytesSent(nBytes); - if (pnode->nSendOffset == data.size()) { + + if (pnode->nSendOffset == data.size()) + { pnode->nSendOffset = 0; pnode->nSendSize -= data.size(); it++; - } else { + } + else + { // could not send full message; stop sending more break; } - } else { - if (nBytes < 0) { + } + else + { + if (nBytes <= 0) + { // error - int nErr = WSAGetLastError(); - if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) + // + if (bIsSSL) { - LogPrintf("socket send error %s\n", NetworkErrorString(nErr)); - pnode->CloseSocketDisconnect(); + if (nRet != SSL_ERROR_WANT_READ && nRet != SSL_ERROR_WANT_WRITE) + { + LogPrintf("ERROR: SSL_write %s; closing connection\n", ERR_error_string(nRet, NULL)); + pnode->CloseSocketDisconnect(); + } + else + { + // preventive measure from exhausting CPU usage + // + MilliSleep(1); // 1 msec + } + } + else + { + if (nRet != WSAEWOULDBLOCK && nRet != WSAEMSGSIZE && nRet != WSAEINTR && nRet != WSAEINPROGRESS) + { + LogPrintf("ERROR: send %s; closing connection\n", NetworkErrorString(nRet)); + pnode->CloseSocketDisconnect(); + } } } + // couldn't send anything at all break; } } - if (it == pnode->vSendMsg.end()) { + if (it == pnode->vSendMsg.end()) + { assert(pnode->nSendOffset == 0); assert(pnode->nSendSize == 0); } @@ -867,6 +1149,44 @@ static bool AttemptToEvictConnection(bool fPreferNewConnection) { return true; } +#ifdef USE_TLS + +static SSL* TLSAccept(SOCKET hSocket, const CAddress &addr) +{ + LogPrint("net", "TLS: accepting connection from %s (tid = %X)\n", addr.ToString(), pthread_self()); + + SSL *ssl = NULL; + bool bAcceptedTLS = false; + + if ((ssl = SSL_new(tls_ctx_server))) + { + if (SSL_set_fd(ssl, hSocket)) + { + if (WaitFor(sslAccept, hSocket, ssl, (DEFAULT_CONNECT_TIMEOUT / 1000)) == 1) + bAcceptedTLS = true; + } + } + + if (bAcceptedTLS) + { + LogPrintf ("TLS: connection from %s has been accepted. Using cipher: %s\n", addr.ToString(), SSL_get_cipher(ssl)); + } + else + { + LogPrintf ("TLS: ERROR: %s: %s: TLS connection from %s failed\n", __FILE__, __func__, addr.ToString()); + + if (ssl) + { + SSL_free(ssl); + ssl = NULL; + } + } + + return ssl; +} + +#endif + static void AcceptConnection(const ListenSocket& hListenSocket) { struct sockaddr_storage sockaddr; socklen_t len = sizeof(sockaddr); @@ -928,18 +1248,124 @@ static void AcceptConnection(const ListenSocket& hListenSocket) { setsockopt(hSocket, IPPROTO_TCP, TCP_NODELAY, (void*)&set, sizeof(int)); #endif - CNode* pnode = new CNode(hSocket, addr, "", true); + SSL *ssl = NULL; + + SetSocketNonBlocking(hSocket, true); + +#ifdef USE_TLS + /* TCP connection is ready. Do server side SSL. */ +#ifdef COMPAT_NON_TLS + { + LOCK(cs_vNonTLSNodesInbound); + + NODE_ADDR nodeAddr(addr.ToStringIP()); + + bool bUseTLS = (find(vNonTLSNodesInbound.begin(), + vNonTLSNodesInbound.end(), + nodeAddr) == vNonTLSNodesInbound.end()); + if (bUseTLS) + { + ssl = TLSAccept(hSocket, addr); + if(!ssl) + { + // Further reconnection will be made in non-TLS (unencrypted) mode + vNonTLSNodesInbound.push_back(NODE_ADDR(addr.ToStringIP(), GetTimeMillis())); + CloseSocket(hSocket); + return; + } + } + else + { + LogPrintf ("TLS: Connection from %s will be unencrypted\n", addr.ToString()); + + vNonTLSNodesInbound.erase( + remove( + vNonTLSNodesInbound.begin(), + vNonTLSNodesInbound.end(), + nodeAddr + ), + vNonTLSNodesInbound.end()); + } + } +#else + ssl = TLSAccept(hSocket, addr); + if(!ssl) + { + CloseSocket(hSocket); + return; + } +#endif // COMPAT_NON_TLS + + if (GetBoolArg("-tlsvalidate", false)) + { + if (ssl && !ValidatePeerCertificate(ssl)) + { + LogPrintf ("TLS: ERROR: Wrong client certificate from %s. Connection will be closed.\n", addr.ToString()); + + SSL_shutdown(ssl); + CloseSocket(hSocket); + SSL_free(ssl); + return; + } + } +#endif // USE_TLS + + CNode* pnode = new CNode(hSocket, addr, "", true, ssl); pnode->AddRef(); pnode->fWhitelisted = whitelisted; - LogPrint("net", "connection from %s accepted\n", addr.ToString()); - { LOCK(cs_vNodes); vNodes.push_back(pnode); } } +#if defined(USE_TLS) && defined(COMPAT_NON_TLS) + +static bool IsNonTLSAddr(const string &strAddr, const vector &vPool, CCriticalSection &cs) +{ + LOCK(cs); + return (find(vPool.begin(), vPool.end(), NODE_ADDR(strAddr)) != vPool.end()); +} + +static void CleanNonTLSPool(std::vector &vPool, CCriticalSection &cs) +{ + LOCK(cs); + + vector vDeleted; + + BOOST_FOREACH(NODE_ADDR nodeAddr, vPool) + { + if ((GetTimeMillis() - nodeAddr.time) >= 900000) + { + vDeleted.push_back(nodeAddr); + LogPrint("net", "TLS: Node %s is deleted from the non-TLS pool\n", nodeAddr.ipAddr); + } + } + + BOOST_FOREACH(NODE_ADDR nodeAddrDeleted, vDeleted) + { + vPool.erase( + remove( + vPool.begin(), + vPool.end(), + nodeAddrDeleted), + vPool.end()); + } +} + +void ThreadNonTLSPoolsCleaner() +{ + while (true) + { + CleanNonTLSPool(vNonTLSNodesInbound, cs_vNonTLSNodesInbound); + CleanNonTLSPool(vNonTLSNodesOutbound, cs_vNonTLSNodesOutbound); + MilliSleep(DEFAULT_CONNECT_TIMEOUT); // sleep and sleep_for are interruption points, which will throw boost::thread_interrupted + } +} + +#endif // USE_TLS && COMPAT_NON_TLS + void ThreadSocketHandler() { unsigned int nPrevNodeCount = 0; @@ -1034,8 +1460,11 @@ void ThreadSocketHandler() LOCK(cs_vNodes); BOOST_FOREACH(CNode* pnode, vNodes) { + LOCK(pnode->cs_hSocket); + if (pnode->hSocket == INVALID_SOCKET) continue; + FD_SET(pnode->hSocket, &fdsetError); hSocketMax = max(hSocketMax, pnode->hSocket); have_fds = true; @@ -1118,17 +1547,54 @@ void ThreadSocketHandler() // // Receive // - if (pnode->hSocket == INVALID_SOCKET) - continue; - if (FD_ISSET(pnode->hSocket, &fdsetRecv) || FD_ISSET(pnode->hSocket, &fdsetError)) + bool recvSet = false, sendSet = false, errorSet = false; + + { + LOCK(pnode->cs_hSocket); + + if (pnode->hSocket == INVALID_SOCKET) + continue; + + recvSet = FD_ISSET(pnode->hSocket, &fdsetRecv); + sendSet = FD_ISSET(pnode->hSocket, &fdsetSend); + errorSet = FD_ISSET(pnode->hSocket, &fdsetError); + } + + if (recvSet || errorSet) { TRY_LOCK(pnode->cs_vRecvMsg, lockRecv); if (lockRecv) { { // typical socket buffer is 8K-64K + // maximum record size is 16kB for SSLv3/TLSv1 char pchBuf[0x10000]; - int nBytes = recv(pnode->hSocket, pchBuf, sizeof(pchBuf), MSG_DONTWAIT); + bool bIsSSL = false; + int nBytes = 0, nRet = 0; + + { + LOCK(pnode->cs_hSocket); + + if (pnode->hSocket == INVALID_SOCKET) + { + LogPrint("net", "Receive: connection with %s is already closed\n", pnode->addr.ToString()); + continue; + } + + bIsSSL = (pnode->ssl != NULL); + + if (bIsSSL) + { + nBytes = SSL_read(pnode->ssl, pchBuf, sizeof(pchBuf)); + nRet = SSL_get_error(pnode->ssl, nBytes); + } + else + { + nBytes = recv(pnode->hSocket, pchBuf, sizeof(pchBuf), MSG_DONTWAIT); + nRet = WSAGetLastError(); + } + } + if (nBytes > 0) { if (!pnode->ReceiveMsgBytes(pchBuf, nBytes)) @@ -1139,20 +1605,39 @@ void ThreadSocketHandler() } else if (nBytes == 0) { - // socket closed gracefully + // socket closed gracefully (peer disconnected) + // if (!pnode->fDisconnect) - LogPrint("net", "socket closed\n"); + LogPrint("net", "socket closed (%s)\n", pnode->addr.ToString()); pnode->CloseSocketDisconnect(); } else if (nBytes < 0) { // error - int nErr = WSAGetLastError(); - if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) + // + if (bIsSSL) { - if (!pnode->fDisconnect) - LogPrintf("socket recv error %s\n", NetworkErrorString(nErr)); - pnode->CloseSocketDisconnect(); + if (nRet != SSL_ERROR_WANT_READ && nRet != SSL_ERROR_WANT_WRITE) // SSL_read() operation has to be repeated because of SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE (https://wiki.openssl.org/index.php/Manual:SSL_read(3)#NOTES) + { + if (!pnode->fDisconnect) + LogPrintf("ERROR: SSL_read %s\n", ERR_error_string(nRet, NULL)); + pnode->CloseSocketDisconnect(); + } + else + { + // preventive measure from exhausting CPU usage + // + MilliSleep(1); // 1 msec + } + } + else + { + if (nRet != WSAEWOULDBLOCK && nRet != WSAEMSGSIZE && nRet != WSAEINTR && nRet != WSAEINPROGRESS) + { + if (!pnode->fDisconnect) + LogPrintf("ERROR: socket recv %s\n", NetworkErrorString(nRet)); + pnode->CloseSocketDisconnect(); + } } } } @@ -1162,9 +1647,7 @@ void ThreadSocketHandler() // // Send // - if (pnode->hSocket == INVALID_SOCKET) - continue; - if (FD_ISSET(pnode->hSocket, &fdsetSend)) + if (sendSet) { TRY_LOCK(pnode->cs_vSend, lockSend); if (lockSend) @@ -1479,6 +1962,28 @@ bool OpenNetworkConnection(const CAddress& addrConnect, CSemaphoreGrant *grantOu CNode* pnode = ConnectNode(addrConnect, pszDest); boost::this_thread::interruption_point(); +#if defined(USE_TLS) && defined(COMPAT_NON_TLS) + + if (!pnode) + { + string strDest; + int port; + + if (!pszDest) + strDest = addrConnect.ToStringIP(); + else + SplitHostPort(string(pszDest), port, strDest); + + if (IsNonTLSAddr(strDest, vNonTLSNodesOutbound, cs_vNonTLSNodesOutbound)) + { + // Attempt to reconnect in non-TLS mode + pnode = ConnectNode(addrConnect, pszDest); + boost::this_thread::interruption_point(); + } + } + +#endif + if (!pnode) return false; if (grantOutbound) @@ -1610,6 +2115,11 @@ bool BindListenPort(const CService &addrBind, string& strError, bool fWhiteliste #endif // Set to non-blocking, incoming connections will also inherit this + // + // WARNING! + // On Linux, the new socket returned by accept() does not inherit file + // status flags such as O_NONBLOCK and O_ASYNC from the listening + // socket. http://man7.org/linux/man-pages/man2/accept.2.html if (!SetSocketNonBlocking(hListenSocket, true)) { strError = strprintf("BindListenPort: Setting listening socket to non-blocking failed, error %s\n", NetworkErrorString(WSAGetLastError())); LogPrintf("%s\n", strError); @@ -1713,6 +2223,167 @@ void static Discover(boost::thread_group& threadGroup) #endif } +#ifdef USE_TLS + +static int TLSCertVerificationCallback(int preverify_ok, X509_STORE_CTX *chainContext) +{ + //If verify_callback always returns 1, the TLS/SSL handshake will not be terminated with respect to verification failures and the connection will be established. + return 1; +} + +static SSL_CTX* TLSInitCtx( + TLSContextType ctxType, + const boost::filesystem::path &privateKeyFile, + const boost::filesystem::path &certificateFile, + const std::vector &trustedDirs) +{ + if (!boost::filesystem::exists(privateKeyFile) || + !boost::filesystem::exists(certificateFile)) + return NULL; + + bool bInitialized = false; + SSL_CTX *tlsCtx = NULL; + + if ((tlsCtx = SSL_CTX_new(ctxType == serverContext ? TLS_server_method() : TLS_client_method()))) + { + SSL_CTX_set_mode(tlsCtx, SSL_MODE_AUTO_RETRY); + + int rootCertsNum = LoadDefaultRootCertificates(tlsCtx); + int trustedPathsNum = 0; + + for (boost::filesystem::path trustedDir : trustedDirs) + { + if (SSL_CTX_load_verify_locations(tlsCtx, NULL, trustedDir.string().c_str()) == 1) + trustedPathsNum++; + } + + if (rootCertsNum == 0 && trustedPathsNum == 0) + LogPrintf("TLS: WARNING: %s: %s: failed to set up verified certificates. It will be impossible to verify peer certificates. \n", __FILE__, __func__); + + SSL_CTX_set_verify(tlsCtx, SSL_VERIFY_PEER, TLSCertVerificationCallback); + + if (SSL_CTX_use_certificate_file(tlsCtx, certificateFile.string().c_str(), SSL_FILETYPE_PEM) > 0) + { + if (SSL_CTX_use_PrivateKey_file(tlsCtx, privateKeyFile.string().c_str(), SSL_FILETYPE_PEM) > 0) + { + if (SSL_CTX_check_private_key(tlsCtx)) + bInitialized = true; + else + LogPrintf("TLS: ERROR: %s: %s: private key does not match the certificate public key\n", __FILE__, __func__); + } + else + LogPrintf("TLS: ERROR: %s: %s: failed to use privateKey file\n", __FILE__, __func__); + } + else + { + LogPrintf("TLS: ERROR: %s: %s: failed to use certificate file\n", __FILE__, __func__); + ERR_print_errors_fp(stderr); + } + } + else + LogPrintf("TLS: ERROR: %s: %s: failed to create TLS context\n", __FILE__, __func__); + + if (!bInitialized) + { + if (tlsCtx) + { + SSL_CTX_free(tlsCtx); + tlsCtx = NULL; + } + } + + return tlsCtx; +} + +static bool TLSInitialize() +{ + bool bInitializationStatus = false; + + // Initialization routines for the OpenSSL library + // + SSL_load_error_strings(); + OpenSSL_add_ssl_algorithms(); // OpenSSL_add_ssl_algorithms() always returns "1", so it is safe to discard the return value. + + namespace fs = boost::filesystem; + fs::path certFile = GetArg("-tlscertpath", ""); + if (!fs::exists(certFile)) + certFile = (GetDataDir() / TLS_CERT_FILE_NAME); + + fs::path privKeyFile = GetArg("-tlskeypath", ""); + if (!fs::exists(privKeyFile)) + privKeyFile = (GetDataDir() / TLS_KEY_FILE_NAME); + + std::vector trustedDirs; + fs::path trustedDir = GetArg("-tlstrustdir", ""); + if (fs::exists(trustedDir)) + // Use only the specified trusted directory + trustedDirs.push_back(trustedDir); + else + // If specified directory can't be used, then setting the default trusted directories + trustedDirs = GetDefaultTrustedDirectories(); + + for (fs::path dir : trustedDirs) + LogPrintf("TLS: trusted directory '%s' will be used\n", dir.c_str()); + + // Initialization of the server and client contexts + // + if ((tls_ctx_server = TLSInitCtx(serverContext, privKeyFile, certFile, trustedDirs))) + { + if ((tls_ctx_client = TLSInitCtx(clientContext, privKeyFile, certFile, trustedDirs))) + { + LogPrint("net", "TLS: contexts are initialized\n"); + bInitializationStatus = true; + } + else + { + LogPrintf("TLS: ERROR: %s: %s: failed to initialize TLS client context\n", __FILE__, __func__); + SSL_CTX_free (tls_ctx_server); + } + } + else + LogPrintf("TLS: ERROR: %s: %s: failed to initialize TLS server context\n", __FILE__, __func__); + + return bInitializationStatus; +} + +static bool TLSPrepareCredentials() +{ + boost::filesystem::path + defaultKeyPath (GetDataDir() / TLS_KEY_FILE_NAME), + defaultCertPath(GetDataDir() / TLS_CERT_FILE_NAME); + + CredentialsStatus credStatus = + VerifyCredentials( + boost::filesystem::path(GetArg("-tlskeypath", defaultKeyPath.string())), + boost::filesystem::path(GetArg("-tlscertpath", defaultCertPath.string())), + GetArg("-tlskeypwd","")); + + bool bPrepared = (credStatus == credOk); + + if (!bPrepared) + { + if (!mapArgs.count("-tlskeypath") && !mapArgs.count("-tlscertpath")) + { + // Default paths were used + // + if (credStatus == credAbsent) + { + // Generate new credentials (key and self-signed certificate on it) only if credentials were absent previously + // + bPrepared = GenerateCredentials( + defaultKeyPath, + defaultCertPath, + GetArg("-tlskeypwd","")); + } + } + } + + return bPrepared; +} + +#endif // USE_TLS + + void StartNode(boost::thread_group& threadGroup, CScheduler& scheduler) { uiInterface.InitMessage(_("Loading addresses...")); @@ -1738,6 +2409,23 @@ void StartNode(boost::thread_group& threadGroup, CScheduler& scheduler) Discover(threadGroup); +#ifdef USE_TLS + + if (!TLSPrepareCredentials()) + { + LogPrintf("TLS: ERROR: %s: %s: Credentials weren't loaded. Node can't be started.\n", __FILE__, __func__); + return; + } + + if (!TLSInitialize()) + { + LogPrintf("TLS: ERROR: %s: %s: TLS initialization failed. Node can't be started.\n", __FILE__, __func__); + return; + } +#else + LogPrintf("TLS is not used!\n"); +#endif + // // Start threads // @@ -1759,6 +2447,11 @@ void StartNode(boost::thread_group& threadGroup, CScheduler& scheduler) // Process messages threadGroup.create_thread(boost::bind(&TraceThread, "msghand", &ThreadMessageHandler)); +#if defined(USE_TLS) && defined(COMPAT_NON_TLS) + // Clean pools of addresses for non-TLS connections + threadGroup.create_thread(boost::bind(&TraceThread, "poolscleaner", &ThreadNonTLSPoolsCleaner)); +#endif + // Dump network addresses scheduler.scheduleEvery(&DumpAddresses, DUMP_ADDRESSES_INTERVAL); } @@ -1788,8 +2481,7 @@ public: { // Close sockets BOOST_FOREACH(CNode* pnode, vNodes) - if (pnode->hSocket != INVALID_SOCKET) - CloseSocket(pnode->hSocket); + pnode->CloseSocketDisconnect(); BOOST_FOREACH(ListenSocket& hListenSocket, vhListenSocket) if (hListenSocket.socket != INVALID_SOCKET) if (!CloseSocket(hListenSocket.socket)) @@ -2024,11 +2716,12 @@ bool CAddrDB::Read(CAddrMan& addr) unsigned int ReceiveFloodSize() { return 1000*GetArg("-maxreceivebuffer", 5*1000); } unsigned int SendBufferSize() { return 1000*GetArg("-maxsendbuffer", 1*1000); } -CNode::CNode(SOCKET hSocketIn, const CAddress& addrIn, const std::string& addrNameIn, bool fInboundIn) : +CNode::CNode(SOCKET hSocketIn, const CAddress& addrIn, const std::string& addrNameIn, bool fInboundIn, SSL *sslIn) : ssSend(SER_NETWORK, INIT_PROTO_VERSION), addrKnown(5000, 0.001), setInventoryKnown(SendBufferSize() / 1000) { + ssl = sslIn; nServices = 0; hSocket = hSocketIn; nRecvVersion = INIT_PROTO_VERSION; @@ -2083,7 +2776,18 @@ CNode::CNode(SOCKET hSocketIn, const CAddress& addrIn, const std::string& addrNa CNode::~CNode() { - CloseSocket(hSocket); + // No need to make a lock on cs_hSocket, because before deletion CNode object is removed from the vNodes vector, so any other thread hasn't access to it. + // Removal is synchronized with read and write routines, so all of them will be completed to this moment. + if (hSocket != INVALID_SOCKET) + { + if (ssl) + { + WaitFor(sslShutdown, hSocket, ssl, (DEFAULT_CONNECT_TIMEOUT / 1000)); + SSL_free(ssl); + ssl = NULL; + } + CloseSocket(hSocket); + } if (pfilter) delete pfilter; diff --git a/src/net.h b/src/net.h index 6fd49536d..5c3875b1d 100644 --- a/src/net.h +++ b/src/net.h @@ -30,6 +30,10 @@ #include #include +// Enable OpenSSL Support for Zen +#include +#include + class CAddrMan; class CBlockIndex; class CScheduler; @@ -74,6 +78,20 @@ bool BindListenPort(const CService &bindAddr, std::string& strError, bool fWhite void StartNode(boost::thread_group& threadGroup, CScheduler& scheduler); bool StopNode(); void SocketSendData(CNode *pnode); +void SocketSendData(CNode *pnode); +SSL_CTX* create_context(bool server_side); +EVP_PKEY *generate_key(); +X509 *generate_x509(EVP_PKEY *pkey); +bool write_to_disk(EVP_PKEY *pkey, X509 *x509); +void configure_context(SSL_CTX *ctx, bool server_side); +static boost::filesystem::path tlsKeyPath; +static boost::filesystem::path tlsCertPath; + +// OpenSSL related variables for metrics.cpp +static std::string routingsecrecy; +static std::string cipherdescription; +static std::string securitylevel; +static std::string validationdescription; typedef int NodeId; @@ -154,6 +172,9 @@ extern CCriticalSection cs_vAddedNodes; extern NodeId nLastNodeId; extern CCriticalSection cs_nLastNodeId; +extern SSL_CTX *tls_ctx_server; +extern SSL_CTX *tls_ctx_client; + struct LocalServiceInfo { int nScore; int nPort; @@ -167,6 +188,8 @@ class CNodeStats public: NodeId nodeid; uint64_t nServices; + bool fTLSEstablished; + bool fTLSVerified; int64_t nLastSend; int64_t nLastRecv; int64_t nTimeConnected; @@ -233,9 +256,13 @@ public: class CNode { public: + // OpenSSL + SSL *ssl; + // socket uint64_t nServices; SOCKET hSocket; + CCriticalSection cs_hSocket; CDataStream ssSend; size_t nSendSize; // total size of all vSendMsg entries size_t nSendOffset; // offset inside the first vSendMsg already sent @@ -324,7 +351,7 @@ public: // Whether a ping is requested. bool fPingQueued; - CNode(SOCKET hSocketIn, const CAddress &addrIn, const std::string &addrNameIn = "", bool fInboundIn = false); + CNode(SOCKET hSocketIn, const CAddress &addrIn, const std::string &addrNameIn = "", bool fInboundIn = false, SSL *sslIn = NULL); ~CNode(); private: diff --git a/src/rpcnet.cpp b/src/rpcnet.cpp index 62c31e9ee..421635856 100644 --- a/src/rpcnet.cpp +++ b/src/rpcnet.cpp @@ -13,6 +13,7 @@ #include "timedata.h" #include "util.h" #include "version.h" +#include "utiltls.h" #include @@ -87,6 +88,8 @@ UniValue getpeerinfo(const UniValue& params, bool fHelp) " \"addr\":\"host:port\", (string) The ip address and port of the peer\n" " \"addrlocal\":\"ip:port\", (string) local address\n" " \"services\":\"xxxxxxxxxxxxxxxx\", (string) The services offered\n" + " \"tls_established\": true:false, (boolean) Status of TLS connection\n" + " \"tls_verified\": true:false, (boolean) Status of peer certificate. Will be true if a peer certificate can be verified with some trusted root certs \n" " \"lastsend\": ttt, (numeric) The time in seconds since epoch (Jan 1 1970 GMT) of the last send\n" " \"lastrecv\": ttt, (numeric) The time in seconds since epoch (Jan 1 1970 GMT) of the last receive\n" " \"bytessent\": n, (numeric) The total bytes sent\n" @@ -130,6 +133,8 @@ UniValue getpeerinfo(const UniValue& params, bool fHelp) if (!(stats.addrLocal.empty())) obj.push_back(Pair("addrlocal", stats.addrLocal)); obj.push_back(Pair("services", strprintf("%016x", stats.nServices))); + obj.push_back(Pair("tls_established", stats.fTLSEstablished)); + obj.push_back(Pair("tls_verified", stats.fTLSVerified)); obj.push_back(Pair("lastsend", stats.nLastSend)); obj.push_back(Pair("lastrecv", stats.nLastRecv)); obj.push_back(Pair("bytessent", stats.nSendBytes)); @@ -412,6 +417,7 @@ UniValue getnetworkinfo(const UniValue& params, bool fHelp) " \"localservices\": \"xxxxxxxxxxxxxxxx\", (string) the services we offer to the network\n" " \"timeoffset\": xxxxx, (numeric) the time offset\n" " \"connections\": xxxxx, (numeric) the number of connections\n" + " \"tls_cert_verified\": true|flase, (boolean) true if the certificate of the current node is verified\n" " \"networks\": [ (array) information per network\n" " {\n" " \"name\": \"xxx\", (string) network (ipv4, ipv6 or onion)\n" @@ -447,6 +453,7 @@ UniValue getnetworkinfo(const UniValue& params, bool fHelp) obj.push_back(Pair("localservices", strprintf("%016x", nLocalServices))); obj.push_back(Pair("timeoffset", GetTimeOffset())); obj.push_back(Pair("connections", (int)vNodes.size())); + obj.push_back(Pair("tls_cert_verified", ValidateCertificate(tls_ctx_server))); obj.push_back(Pair("networks", GetNetworksInfo())); obj.push_back(Pair("relayfee", ValueFromAmount(::minRelayTxFee.GetFeePerK()))); UniValue localAddresses(UniValue::VARR); diff --git a/src/utiltls.cpp b/src/utiltls.cpp new file mode 100644 index 000000000..c924aaef0 --- /dev/null +++ b/src/utiltls.cpp @@ -0,0 +1,491 @@ +// Copyright (c) 2017 The Zen Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" +#include "utiltls.h" + +// Set of most common default trusted certificates directories used by OpenSSL +static const char* defaultTrustedDirs[] = +{ +#ifdef WIN32 + "" +#elif MAC_OSX + "/System/Library/OpenSSL/certs" +#else // Linux build + "/etc/ssl/certs", + "/usr/local/ssl/certs", + "/usr/lib/ssl/certs", + "/usr/share/ssl/certs", + "/etc/pki/tls/certs", + "/var/lib/ca-certificates" +#endif +}; + +// Default root certificates (PEM encoded) +static const char defaultRootCerts[] = +{ +// // Example of specifying a certificate +// // +// "-----BEGIN CERTIFICATE-----\n" +// "MIIDYDCCAkigAwIBAgIJAJMakdoBYY67MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n" +// "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" +// "aWRnaXRzIFB0eSBMdGQwHhcNMTcwODE0MTc0MTMyWhcNNDQxMjMwMTc0MTMyWjBF\n" +// "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n" +// "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" +// "CgKCAQEAzNV+SPRCKSEGlntfpCRMVSfz99NoEo3K1SRyw6GTSb1LNSTQCn1EsCSH\n" +// "cVZTmyfjcTHpwz4aF14yw8lQC42f218AOsG1DV5suCaUXhSmZlajMkvEJVwfBOft\n" +// "xpcqE1fA9wovXlnJLXVgyJGMc896S8tcbrCU/l/BsqKh5QX8N60MQ3w376nSGvVP\n" +// "ussN8bVH3aKRwjhateqx1GRt0GPnM8/u7EkgF8Bc+m8WZYcUfkPC5Am2D0MO1HOA\n" +// "u3IKxXZMs/fYd6nF5DZBwg+D23EP/V8oqenn8ilvrSORq5PguOl1QoDyY66PhmjN\n" +// "L9c4Spxw8HXUDlrfuSQn2NJnw1XhdQIDAQABo1MwUTAdBgNVHQ4EFgQU/KD+n5Bz\n" +// "QLbp09qKzwwyNwOQU4swHwYDVR0jBBgwFoAU/KD+n5BzQLbp09qKzwwyNwOQU4sw\n" +// "DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAVtprBxZD6O+WNYUM\n" +// "ksdKiVVoszEJXlt7wajuaPBPK/K3buxE9FLVxS+LiH1PUhPCc6V28guyKWwn109/\n" +// "4WnO51LQjygvd7SaePlbiO7iIatkOk4oETJQZ+tEJ7fv/NITY/GQUfgPNkANmPPz\n" +// "Mz9I6He8XhIpO6NGuDG+74aR1RhvR3PWJJYT0QpL0STVR4qTc/HfnymF5XnnjOYZ\n" +// "mwzT8jXX5dhLYwJmyPBS+uv+oa1quM/FitA63N9anYtRBiPaBtund9Ikjat1hM0h\n" +// "neo2tz7Mfsgjb0aiORtiyaH2OetvwR0QuCSVPnknkfGWPDINdUdkgKyA1PX58Smw\n" +// "vaXEcw==\n" +// "-----END CERTIFICATE-----" + + "" +}; + +// Generates RSA keypair (a private key of 'bits' length for a specified 'uPublicKey') +// +static EVP_PKEY* GenerateRsaKey(int bits, BN_ULONG uPublicKey) +{ + EVP_PKEY *evpPrivKey = NULL; + + BIGNUM *pubKey = BN_new(); + if (pubKey) + { + if (BN_set_word(pubKey, uPublicKey)) + { + RSA *privKey = RSA_new(); + if (privKey) + { + if (RAND_poll() && // The pseudo-random number generator must be seeded prior to calling RSA_generate_key_ex(). (https://www.openssl.org/docs/man1.1.0/crypto/RSA_generate_key.html) + RSA_generate_key_ex(privKey, bits, pubKey, NULL)) + { + if ((evpPrivKey = EVP_PKEY_new())) + { + if (!EVP_PKEY_assign_RSA(evpPrivKey, privKey)) + { + EVP_PKEY_free(evpPrivKey); + evpPrivKey = NULL; + } + } + } + + if(!evpPrivKey) // EVP_PKEY_assign_RSA uses the supplied key internally + RSA_free(privKey); + } + } + BN_free(pubKey); + } + + return evpPrivKey; +} + +// Generates certificate for a specified public key using a corresponding private key (both of them should be specified in the 'keypair'). +// +static X509* GenerateCertificate(EVP_PKEY *keypair) +{ + if (!keypair) + return NULL; + + X509 *cert = X509_new(); + if (cert) + { + bool bCertSigned = false; + long sn = 0; + + if (RAND_bytes((unsigned char*)&sn, sizeof sn) && + ASN1_INTEGER_set(X509_get_serialNumber(cert), sn)) + { + X509_gmtime_adj(X509_get_notBefore(cert), 0); + X509_gmtime_adj(X509_get_notAfter(cert), (60 * 60 * 24 * CERT_VALIDITY_DAYS)); + + // setting a public key from the keypair + if (X509_set_pubkey(cert, keypair)) + { + X509_NAME *subjectName = X509_get_subject_name(cert); + if (subjectName) + { + // an issuer name is the same as a subject name, due to certificate is self-signed + if (X509_set_issuer_name(cert, subjectName)) + { + // private key from keypair is used; signature will be set inside of the cert + bCertSigned = X509_sign(cert, keypair, EVP_sha512()); + } + } + } + } + + if (!bCertSigned) + { + X509_free(cert); + cert = NULL; + } + } + + return cert; +} + +// Stores key to file, specified by the 'filePath' +// +static bool StoreKey(EVP_PKEY *key, const boost::filesystem::path &filePath, const std::string &passphrase) +{ + if (!key) + return false; + + bool bStored = false; + + FILE *keyfd = fopen(filePath.string().c_str(), "wb"); + if (keyfd) + { + const EVP_CIPHER* pCipher = NULL; + + if (passphrase.length() && (pCipher = EVP_aes_256_cbc())) + bStored = PEM_write_PrivateKey(keyfd, key, pCipher, NULL, 0, NULL, (void*)passphrase.c_str()); + else + bStored = PEM_write_PrivateKey(keyfd, key, NULL, NULL, 0, NULL, NULL); + + fclose(keyfd); + } + + return bStored; +} + +// Stores certificate to file, specified by the 'filePath' +// +static bool StoreCertificate(X509 *cert, const boost::filesystem::path &filePath) +{ + if (!cert) + return false; + + bool bStored = false; + + FILE *certfd = fopen(filePath.string().c_str(), "wb"); + if (certfd) + { + bStored = PEM_write_X509(certfd, cert); + fclose(certfd); + } + + return bStored; +} + +// Loads key from file, specified by the 'filePath' +// +static EVP_PKEY* LoadKey(const boost::filesystem::path &filePath, const std::string &passphrase) +{ + if (!boost::filesystem::exists(filePath)) + return NULL; + + EVP_PKEY *key = NULL; + FILE *keyfd = fopen(filePath.string().c_str(), "rb"); + if (keyfd) + { + key = PEM_read_PrivateKey(keyfd, NULL, NULL, passphrase.length() ? (void*)passphrase.c_str() : NULL); + fclose(keyfd); + } + + return key; +} + +// Loads certificate from file, specified by the 'filePath' +// +static X509* LoadCertificate(const boost::filesystem::path &filePath) +{ + if (!boost::filesystem::exists(filePath)) + return NULL; + + X509 *cert = NULL; + FILE *certfd = fopen(filePath.string().c_str(), "rb"); + if (certfd) + { + cert = PEM_read_X509(certfd, NULL, NULL, NULL); + fclose(certfd); + } + + return cert; +} + +// Verifies if the private key in 'key' matches the public key in 'cert' +// (Signs random bytes on 'key' and verifies signature correctness on public key from 'cert') +// +static bool IsMatching(EVP_PKEY *key, X509 *cert) +{ + if (!key || !cert) + return false; + + bool bIsMatching = false; + + EVP_PKEY_CTX *ctxSign = EVP_PKEY_CTX_new(key, NULL); + if (ctxSign) + { + if (EVP_PKEY_sign_init(ctxSign) == 1 && + EVP_PKEY_CTX_set_signature_md(ctxSign, EVP_sha512()) > 0) + { + unsigned char digest[SHA512_DIGEST_LENGTH] = { 0 }; + size_t digestSize = sizeof digest, signatureSize = 0; + + if (RAND_bytes((unsigned char*)&digest, digestSize) && // set random bytes as a digest + EVP_PKEY_sign(ctxSign, NULL, &signatureSize, digest, digestSize) == 1) // determine buffer length + { + unsigned char *signature = (unsigned char*)OPENSSL_malloc(signatureSize); + if (signature) + { + if (EVP_PKEY_sign(ctxSign, signature, &signatureSize, digest, digestSize) == 1) + { + EVP_PKEY *pubkey = X509_get_pubkey(cert); + if (pubkey) + { + EVP_PKEY_CTX *ctxVerif = EVP_PKEY_CTX_new(pubkey, NULL); + if (ctxVerif) + { + if (EVP_PKEY_verify_init(ctxVerif) == 1 && + EVP_PKEY_CTX_set_signature_md(ctxVerif, EVP_sha512()) > 0) + { + bIsMatching = (EVP_PKEY_verify(ctxVerif, signature, signatureSize, digest, digestSize) == 1); + } + EVP_PKEY_CTX_free(ctxVerif); + } + EVP_PKEY_free(pubkey); + } + } + OPENSSL_free(signature); + } + } + } + EVP_PKEY_CTX_free(ctxSign); + } + + return bIsMatching; +} + +// Checks the correctness of a private-public key pair and the validity of a certificate using public key from key pair +// +static bool CheckCredentials(EVP_PKEY *key, X509 *cert) +{ + if (!key || !cert) + return false; + + bool bIsOk = false; + + // Validating the correctness of a private-public key pair, depending on a key type + // + switch (EVP_PKEY_base_id(key)) + { + case EVP_PKEY_RSA: + case EVP_PKEY_RSA2: + { + RSA *rsaKey = EVP_PKEY_get1_RSA(key); + if (rsaKey) + { + bIsOk = (RSA_check_key(rsaKey) == 1); + RSA_free(rsaKey); + } + break; + } + + // Currently only RSA keys are supported. + // Other key types can be added here in further. + + default: + bIsOk = false; + } + + // Verifying if the private key matches the public key in certificate + if (bIsOk) + bIsOk = IsMatching(key, cert); + + return bIsOk; +} + +// Verifies credentials (a private key, a certificate for public key and a correspondence between the private and the public key) +// +CredentialsStatus VerifyCredentials( + const boost::filesystem::path &keyPath, + const boost::filesystem::path &certPath, + const std::string &passphrase) +{ + CredentialsStatus status = credAbsent; + + EVP_PKEY *key = NULL; + X509 *cert = NULL; + + key = LoadKey(keyPath, passphrase); + cert = LoadCertificate(certPath); + + if (key && cert) + status = CheckCredentials(key, cert) ? credOk : credNonConsistent; + else if (!key && !cert) + status = credAbsent; + else + status = credPartiallyAbsent; + + if (key) + EVP_PKEY_free(key); + if (cert) + X509_free(cert); + + return status; +} + +// Generates public key pair and the self-signed certificate for it, and then stores them by the specified paths 'keyPath' and 'certPath' respectively. +// +bool GenerateCredentials( + const boost::filesystem::path &keyPath, + const boost::filesystem::path &certPath, + const std::string &passphrase) +{ + bool bGenerated = false; + + EVP_PKEY *key = NULL; + X509 *cert = NULL; + + // Generating RSA key and the self-signed certificate for it + // + key = GenerateRsaKey(TLS_RSA_KEY_SIZE, RSA_F4); + if (key) + { + cert = GenerateCertificate(key); + if (cert) + { + if (StoreKey(key, keyPath, passphrase) && + StoreCertificate(cert, certPath)) + { + bGenerated = true; + LogPrintStr("TLS: New private key and self-signed certificate were generated successfully\n"); + } + + X509_free(cert); + } + EVP_PKEY_free(key); + } + + return bGenerated; +} + +// Checks if certificate of a peer is valid (by internal means of the TLS protocol) +// +// Validates peer certificate using a chain of CA certificates. +// If some of intermediate CA certificates are absent in the trusted certificates store, then validation status will be 'false') +// +bool ValidatePeerCertificate(SSL *ssl) +{ + if (!ssl) + return false; + + bool bIsOk = false; + + X509 *cert = SSL_get_peer_certificate (ssl); + if (cert) + { + // NOTE: SSL_get_verify_result() is only useful in connection with SSL_get_peer_certificate (https://www.openssl.org/docs/man1.0.2/ssl/SSL_get_verify_result.html) + // + bIsOk = (SSL_get_verify_result(ssl) == X509_V_OK); + X509_free(cert); + } + else + { + LogPrint("net", "TLS: Peer does not have certificate\n"); + bIsOk = false; + } + return bIsOk; +} + +// Check if a given context is set up with a cert that can be validated by this context +// +bool ValidateCertificate(SSL_CTX *ssl_ctx) +{ + if (!ssl_ctx) + return false; + + bool bIsOk = false; + + X509_STORE *store = SSL_CTX_get_cert_store(ssl_ctx); + + if (store) + { + X509_STORE_CTX *ctx = X509_STORE_CTX_new(); + if (ctx) + { + if (X509_STORE_CTX_init(ctx, store, SSL_CTX_get0_certificate(ssl_ctx), NULL) == 1) + bIsOk = X509_verify_cert(ctx) == 1; + + X509_STORE_CTX_free(ctx); + } + } + + return bIsOk; +} + +// Creates the list of available OpenSSL default directories for trusted certificates storage +// +std::vector GetDefaultTrustedDirectories() +{ + namespace fs = boost::filesystem; + std::vector defaultDirectoriesList; + + // Default certificates directory specified in OpenSSL build + fs::path libDefaultDir = X509_get_default_cert_dir(); + + if (fs::exists(libDefaultDir)) + defaultDirectoriesList.push_back(libDefaultDir); + + // Check and set all possible standard default directories + for (const char *dir : defaultTrustedDirs) + { + fs::path defaultDir(dir); + + if (defaultDir != libDefaultDir && + fs::exists(defaultDir)) + defaultDirectoriesList.push_back(defaultDir); + } + + return defaultDirectoriesList; +} + +// Loads default root certificates (placed in the 'defaultRootCerts') into the specified context. +// Returns the number of loaded certificates. +// +int LoadDefaultRootCertificates(SSL_CTX *ctx) +{ + if (!ctx) + return 0; + + int certsLoaded = 0; + + // Certificate text buffer 'defaultRootCerts' is a C string with certificates in PEM format + BIO *memBuf = BIO_new_mem_buf(defaultRootCerts, -1); + if (memBuf) + { + X509 *cert = NULL; + while ((cert = PEM_read_bio_X509(memBuf, NULL, 0, NULL))) + { + if (X509_STORE_add_cert(SSL_CTX_get_cert_store(ctx), cert) > 0) + certsLoaded++; + + X509_free(cert); + } + BIO_free(memBuf); + } + + return certsLoaded; +} \ No newline at end of file diff --git a/src/utiltls.h b/src/utiltls.h new file mode 100644 index 000000000..17d89c68f --- /dev/null +++ b/src/utiltls.h @@ -0,0 +1,53 @@ +// Copyright (c) 2017 The Zen Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef UTILTLS_H +#define UTILTLS_H + +#include + +#define TLS_KEY_FILE_NAME "key.pem" // default name of a private key +#define TLS_CERT_FILE_NAME "cert.pem" // default name of a certificate + +#define CERT_VALIDITY_DAYS (365 * 10) // period of validity, in days, for a self-signed certificate + +#define TLS_RSA_KEY_SIZE 2048 // size of a private RSA key, in bits, that will be generated, if no other key is specified + +typedef enum {credOk, credNonConsistent, credAbsent, credPartiallyAbsent} CredentialsStatus; + +// Verifies credentials (a private key, a certificate for public key and a correspondence between the private and the public key) +// +CredentialsStatus VerifyCredentials( + const boost::filesystem::path &keyPath, + const boost::filesystem::path &certPath, + const std::string &passphrase); + +// Generates public key pair and the self-signed certificate for it, and then stores them by the specified paths 'keyPath' and 'certPath' respectively. +// +bool GenerateCredentials( + const boost::filesystem::path &keyPath, + const boost::filesystem::path &certPath, + const std::string &passphrase); + +// Checks if certificate of a peer is valid (by internal means of the TLS protocol) +// +// Validates peer certificate using a chain of CA certificates. +// If some of intermediate CA certificates are absent in the trusted certificates store, then validation status will be 'false') +// +bool ValidatePeerCertificate(SSL *ssl); + +// Check if a given context is set up with a cert that can be validated by this context +// +bool ValidateCertificate(SSL_CTX *ssl_ctx); + +// Creates the list of available OpenSSL default directories for trusted certificates storage +// +std::vector GetDefaultTrustedDirectories(); + +// Loads default root certificates (placed in the 'defaultRootCerts') into the specified context. +// Returns the number of loaded certificates. +// +int LoadDefaultRootCertificates(SSL_CTX *ctx); + +#endif // UTILTLS_H \ No newline at end of file