From 641cb063bc71acc7f29d25b12c8713a8beb2018c Mon Sep 17 00:00:00 2001 From: Mattes D Date: Mon, 22 Aug 2016 19:53:34 +0200 Subject: cTCPLink supports TLS out of the box. --- src/OSSupport/Network.h | 33 +++++ src/OSSupport/TCPLinkImpl.cpp | 291 +++++++++++++++++++++++++++++++++++++++++- src/OSSupport/TCPLinkImpl.h | 75 +++++++++++ 3 files changed, 397 insertions(+), 2 deletions(-) (limited to 'src/OSSupport') diff --git a/src/OSSupport/Network.h b/src/OSSupport/Network.h index 1162d7fc6..78c5e92f0 100644 --- a/src/OSSupport/Network.h +++ b/src/OSSupport/Network.h @@ -20,6 +20,11 @@ typedef std::vector cTCPLinkPtrs; class cServerHandle; typedef SharedPtr cServerHandlePtr; typedef std::vector cServerHandlePtrs; +class cCryptoKey; +typedef SharedPtr cCryptoKeyPtr; +class cX509Cert; +typedef SharedPtr cX509CertPtr; + @@ -49,6 +54,10 @@ public: Sending data on the link is not an error, but the data won't be delivered. */ virtual void OnRemoteClosed(void) = 0; + /** Called when the TLS handshake has been completed and communication can continue regularly. + Has an empty default implementation, so that link callback descendants don't need to specify TLS handlers when they don't use TLS at all. */ + virtual void OnTlsHandshakeCompleted(void) {} + /** Called when an error is detected on the connection. */ virtual void OnError(int a_ErrorCode, const AString & a_ErrorMsg) = 0; }; @@ -90,6 +99,30 @@ public: Sends the RST packet, queued outgoing and incoming data is lost. */ virtual void Close(void) = 0; + /** Starts a TLS handshake as a client connection. + If a client certificate should be used for the connection, set the certificate into a_OwnCertData and + its corresponding private key to a_OwnPrivKeyData. If both are empty, no client cert is presented. + a_OwnPrivKeyPassword is the password to be used for decoding PrivKey, empty if not passworded. + Returns empty string on success, non-empty error description on failure. */ + virtual AString StartTLSClient( + cX509CertPtr a_OwnCert, + cCryptoKeyPtr a_OwnPrivKey + ) = 0; + + /** Starts a TLS handshake as a server connection. + Set the server certificate into a_CertData and its corresponding private key to a_OwnPrivKeyData. + a_OwnPrivKeyPassword is the password to be used for decoding PrivKey, empty if not passworded. + a_StartTLSData is any data that should be pushed into the TLS before reading more data from the remote. + This is used mainly for protocols starting TLS in the middle of communication, when the TLS start command + can be received together with the TLS Client Hello message in one OnReceivedData() call, to re-queue the + Client Hello message into the TLS handshake buffer. + Returns empty string on success, non-empty error description on failure. */ + virtual AString StartTLSServer( + cX509CertPtr a_OwnCert, + cCryptoKeyPtr a_OwnPrivKey, + const AString & a_StartTLSData + ) = 0; + /** Returns the callbacks that are used. */ cCallbacksPtr GetCallbacks(void) const { return m_Callbacks; } diff --git a/src/OSSupport/TCPLinkImpl.cpp b/src/OSSupport/TCPLinkImpl.cpp index b15b6282f..df70f3f72 100644 --- a/src/OSSupport/TCPLinkImpl.cpp +++ b/src/OSSupport/TCPLinkImpl.cpp @@ -48,6 +48,9 @@ cTCPLinkImpl::cTCPLinkImpl(evutil_socket_t a_Socket, cTCPLink::cCallbacksPtr a_L cTCPLinkImpl::~cTCPLinkImpl() { + // If the TLS context still exists, free it: + m_TlsContext.reset(); + bufferevent_free(m_BufferEvent); } @@ -129,7 +132,16 @@ bool cTCPLinkImpl::Send(const void * a_Data, size_t a_Length) LOGD("%s: Cannot send data, the link is already shut down.", __FUNCTION__); return false; } - return (bufferevent_write(m_BufferEvent, a_Data, a_Length) == 0); + + // If running in TLS mode, push the data into the TLS context instead: + if (m_TlsContext != nullptr) + { + m_TlsContext->Send(a_Data, a_Length); + return true; + } + + // Send the data: + return SendRaw(a_Data, a_Length); } @@ -138,6 +150,14 @@ bool cTCPLinkImpl::Send(const void * a_Data, size_t a_Length) void cTCPLinkImpl::Shutdown(void) { + // If running in TLS mode, notify the TLS layer: + if (m_TlsContext != nullptr) + { + m_TlsContext->NotifyClose(); + m_TlsContext->ResetSelf(); + m_TlsContext.reset(); + } + // If there's no outgoing data, shutdown the socket directly: if (evbuffer_get_length(bufferevent_get_output(m_BufferEvent)) == 0) { @@ -155,6 +175,14 @@ void cTCPLinkImpl::Shutdown(void) void cTCPLinkImpl::Close(void) { + // If running in TLS mode, notify the TLS layer: + if (m_TlsContext != nullptr) + { + m_TlsContext->NotifyClose(); + m_TlsContext->ResetSelf(); + m_TlsContext.reset(); + } + // Disable all events on the socket, but keep it alive: bufferevent_disable(m_BufferEvent, EV_READ | EV_WRITE); if (m_Server == nullptr) @@ -173,18 +201,98 @@ void cTCPLinkImpl::Close(void) +AString cTCPLinkImpl::StartTLSClient( + cX509CertPtr a_OwnCert, + cCryptoKeyPtr a_OwnPrivKey +) +{ + // Check preconditions: + if (m_TlsContext != nullptr) + { + return "TLS is already active on this link"; + } + if ( + ((a_OwnCert == nullptr) && (a_OwnPrivKey != nullptr)) || + ((a_OwnCert != nullptr) && (a_OwnPrivKey != nullptr)) + ) + { + return "Either provide both the certificate and private key, or neither"; + } + + // Create the TLS context: + m_TlsContext.reset(new cLinkTlsContext(*this)); + m_TlsContext->Initialize(true); + if (a_OwnCert != nullptr) + { + m_TlsContext->SetOwnCert(a_OwnCert, a_OwnPrivKey); + } + m_TlsContext->SetSelf(cLinkTlsContextWPtr(m_TlsContext)); + + // Start the handshake: + m_TlsContext->Handshake(); + return ""; +} + + + + + +AString cTCPLinkImpl::StartTLSServer( + cX509CertPtr a_OwnCert, + cCryptoKeyPtr a_OwnPrivKey, + const AString & a_StartTLSData +) +{ + // Check preconditions: + if (m_TlsContext != nullptr) + { + return "TLS is already active on this link"; + } + if ((a_OwnCert == nullptr) || (a_OwnPrivKey == nullptr)) + { + return "Provide the server certificate and private key"; + } + + // Create the TLS context: + m_TlsContext.reset(new cLinkTlsContext(*this)); + m_TlsContext->Initialize(false); + m_TlsContext->SetOwnCert(a_OwnCert, a_OwnPrivKey); + m_TlsContext->SetSelf(cLinkTlsContextWPtr(m_TlsContext)); + + // Push the initial data: + m_TlsContext->StoreReceivedData(a_StartTLSData.data(), a_StartTLSData.size()); + + // Start the handshake: + m_TlsContext->Handshake(); + return ""; +} + + + + + void cTCPLinkImpl::ReadCallback(bufferevent * a_BufferEvent, void * a_Self) { ASSERT(a_Self != nullptr); cTCPLinkImpl * Self = static_cast(a_Self); + ASSERT(Self->m_BufferEvent == a_BufferEvent); ASSERT(Self->m_Callbacks != nullptr); // Read all the incoming data, in 1024-byte chunks: char data[1024]; size_t length; + auto tlsContext = Self->m_TlsContext; while ((length = bufferevent_read(a_BufferEvent, data, sizeof(data))) > 0) { - Self->m_Callbacks->OnReceivedData(data, length); + if (tlsContext != nullptr) + { + ASSERT(tlsContext->IsLink(Self)); + tlsContext->StoreReceivedData(data, length); + } + else + { + Self->ReceivedCleartextData(data, length); + } } } @@ -262,6 +370,13 @@ void cTCPLinkImpl::EventCallback(bufferevent * a_BufferEvent, short a_What, void // If the connection has been closed, call the link callback and remove the connection: if (a_What & BEV_EVENT_EOF) { + // If running in TLS mode and there's data left in the TLS contect, report it: + auto tlsContext = Self->m_TlsContext; + if (tlsContext != nullptr) + { + tlsContext->FlushBuffers(); + } + Self->m_Callbacks->OnRemoteClosed(); if (Self->m_Server != nullptr) { @@ -357,6 +472,178 @@ void cTCPLinkImpl::DoActualShutdown(void) +bool cTCPLinkImpl::SendRaw(const void * a_Data, size_t a_Length) +{ + return (bufferevent_write(m_BufferEvent, a_Data, a_Length) == 0); +} + + + + + +void cTCPLinkImpl::ReceivedCleartextData(const char * a_Data, size_t a_Length) +{ + ASSERT(m_Callbacks != nullptr); + m_Callbacks->OnReceivedData(a_Data, a_Length); +} + + + + + +//////////////////////////////////////////////////////////////////////////////// +// cTCPLinkImpl::cLinkTlsContext: + +cTCPLinkImpl::cLinkTlsContext::cLinkTlsContext(cTCPLinkImpl & a_Link): + m_Link(a_Link) +{ +} + + + + + +void cTCPLinkImpl::cLinkTlsContext::SetSelf(cLinkTlsContextWPtr a_Self) +{ + m_Self = a_Self; +} + + + + + +void cTCPLinkImpl::cLinkTlsContext::ResetSelf(void) +{ + m_Self.reset(); +} + + + + + +void cTCPLinkImpl::cLinkTlsContext::StoreReceivedData(const char * a_Data, size_t a_NumBytes) +{ + // Hold self alive for the duration of this function + cLinkTlsContextPtr Self(m_Self); + + m_EncryptedData.append(a_Data, a_NumBytes); + + // Try to finish a pending handshake: + TryFinishHandshaking(); + + // Flush any cleartext data that can be "received": + FlushBuffers(); +} + + + + + +void cTCPLinkImpl::cLinkTlsContext::FlushBuffers(void) +{ + // Hold self alive for the duration of this function + cLinkTlsContextPtr Self(m_Self); + + // If the handshake didn't complete yet, bail out: + if (!HasHandshaken()) + { + return; + } + + char Buffer[1024]; + int NumBytes; + while ((NumBytes = ReadPlain(Buffer, sizeof(Buffer))) > 0) + { + m_Link.ReceivedCleartextData(Buffer, static_cast(NumBytes)); + if (m_Self.expired()) + { + // The callback closed the SSL context, bail out + return; + } + } +} + + + + + +void cTCPLinkImpl::cLinkTlsContext::TryFinishHandshaking(void) +{ + // Hold self alive for the duration of this function + cLinkTlsContextPtr Self(m_Self); + + // If the handshake hasn't finished yet, retry: + if (!HasHandshaken()) + { + Handshake(); + // If the handshake succeeded, write all the queued plaintext data: + if (HasHandshaken()) + { + m_Link.GetCallbacks()->OnTlsHandshakeCompleted(); + WritePlain(m_CleartextData.data(), m_CleartextData.size()); + m_CleartextData.clear(); + } + } +} + + + + + +void cTCPLinkImpl::cLinkTlsContext::Send(const void * a_Data, size_t a_Length) +{ + // Hold self alive for the duration of this function + cLinkTlsContextPtr Self(m_Self); + + // If the handshake hasn't completed yet, queue the data: + if (!HasHandshaken()) + { + m_CleartextData.append(reinterpret_cast(a_Data), a_Length); + TryFinishHandshaking(); + return; + } + + // The connection is all set up, write the cleartext data into the SSL context: + WritePlain(a_Data, a_Length); + FlushBuffers(); +} + + + + + +int cTCPLinkImpl::cLinkTlsContext::ReceiveEncrypted(unsigned char * a_Buffer, size_t a_NumBytes) +{ + // Hold self alive for the duration of this function + cLinkTlsContextPtr Self(m_Self); + + // If there's nothing queued in the buffer, report empty buffer: + if (m_EncryptedData.empty()) + { + return POLARSSL_ERR_NET_WANT_READ; + } + + // Copy as much data as possible to the provided buffer: + size_t BytesToCopy = std::min(a_NumBytes, m_EncryptedData.size()); + memcpy(a_Buffer, m_EncryptedData.data(), BytesToCopy); + m_EncryptedData.erase(0, BytesToCopy); + return static_cast(BytesToCopy); +} + + + + + +int cTCPLinkImpl::cLinkTlsContext::SendEncrypted(const unsigned char * a_Buffer, size_t a_NumBytes) +{ + m_Link.SendRaw(a_Buffer, a_NumBytes); + return static_cast(a_NumBytes); +} + + + + + //////////////////////////////////////////////////////////////////////////////// // cNetwork API: diff --git a/src/OSSupport/TCPLinkImpl.h b/src/OSSupport/TCPLinkImpl.h index bea21aeff..b54c1a2cc 100644 --- a/src/OSSupport/TCPLinkImpl.h +++ b/src/OSSupport/TCPLinkImpl.h @@ -14,6 +14,7 @@ #include "Network.h" #include #include +#include "../PolarSSL++/SslContext.h" @@ -64,9 +65,73 @@ public: virtual UInt16 GetRemotePort(void) const override { return m_RemotePort; } virtual void Shutdown(void) override; virtual void Close(void) override; + virtual AString StartTLSClient( + cX509CertPtr a_OwnCert, + cCryptoKeyPtr a_OwnPrivKey + ) override; + virtual AString StartTLSServer( + cX509CertPtr a_OwnCert, + cCryptoKeyPtr a_OwnPrivKey, + const AString & a_StartTLSData + ) override; protected: + // fwd: + class cLinkTlsContext; + typedef SharedPtr cLinkTlsContextPtr; + typedef WeakPtr cLinkTlsContextWPtr; + + /** Wrapper around cSslContext that is used when this link is being encrypted by SSL. */ + class cLinkTlsContext : + public cSslContext + { + cTCPLinkImpl & m_Link; + + /** Buffer for storing the incoming encrypted data until it is requested by the SSL decryptor. */ + AString m_EncryptedData; + + /** Buffer for storing the outgoing cleartext data until the link has finished handshaking. */ + AString m_CleartextData; + + /** Shared ownership of self, so that this object can keep itself alive for as long as it needs. */ + cLinkTlsContextWPtr m_Self; + + public: + cLinkTlsContext(cTCPLinkImpl & a_Link); + + /** Shares ownership of self, so that this object can keep itself alive for as long as it needs. */ + void SetSelf(cLinkTlsContextWPtr a_Self); + + /** Removes the self ownership so that we can detect the SSL closure. */ + void ResetSelf(void); + + /** Stores the specified block of data into the buffer of the data to be decrypted (incoming from remote). + Also flushes the SSL buffers by attempting to read any data through the SSL context. */ + void StoreReceivedData(const char * a_Data, size_t a_NumBytes); + + /** Tries to read any cleartext data available through the SSL, reports it in the link. */ + void FlushBuffers(void); + + /** Tries to finish handshaking the SSL. */ + void TryFinishHandshaking(void); + + /** Sends the specified cleartext data over the SSL to the remote peer. + If the handshake hasn't been completed yet, queues the data for sending when it completes. */ + void Send(const void * a_Data, size_t a_Length); + + // cSslContext overrides: + virtual int ReceiveEncrypted(unsigned char * a_Buffer, size_t a_NumBytes) override; + virtual int SendEncrypted(const unsigned char * a_Buffer, size_t a_NumBytes) override; + + /** Returns true if the context's associated TCP link is the same link as a_Link. */ + bool IsLink(cTCPLinkImpl * a_Link) + { + return (a_Link == &m_Link); + } + }; + + /** Callbacks to call when the connection is established. May be NULL if not used. Only used for outgoing connections (cNetwork::Connect()). */ cNetwork::cConnectCallbacksPtr m_ConnectCallbacks; @@ -99,6 +164,10 @@ protected: data is sent to the OS TCP stack, the socket gets shut down. */ bool m_ShouldShutdown; + /** The SSL context used for encryption, if this link uses SSL. + If valid, the link uses encryption through this context. */ + cLinkTlsContextPtr m_TlsContext; + /** Creates a new link to be queued to connect to a specified host:port. Used for outgoing connections created using cNetwork::Connect(). @@ -127,6 +196,12 @@ protected: /** Calls shutdown on the link and disables LibEvent writing. Called after all data from LibEvent buffers is sent to the OS TCP stack and shutdown() has been called before. */ void DoActualShutdown(void); + + /** Sends the data directly to the socket (without the optional TLS). */ + bool SendRaw(const void * a_Data, size_t a_Length); + + /** Called by the TLS when it has decoded a piece of incoming cleartext data from the socket. */ + void ReceivedCleartextData(const char * a_Data, size_t a_Length); }; -- cgit v1.2.3