From 67dac89cc0d3f35c4646537ec8f01a22b20e253d Mon Sep 17 00:00:00 2001 From: Anirudh Ramachandran Date: Mon, 26 Sep 2016 13:14:55 -0700 Subject: [PATCH] AsyncSSLSocket::getSSLClientCiphers using static map Summary: OpenSSL SSL_METHOD->get_cipher_by_char is not present in either OpenSSL 1.1.0 or BoringSSL. In addition, knekritz reports that time's being spent in binary searching for cipher names. Since the ciphercodes and names are (fairly) static, we store these in a static hash map. Reviewed By: siyengar Differential Revision: D3275185 fbshipit-source-id: 08b36f3e73239b415b74c6ecc30ed65832d9ebd0 --- folly/io/async/AsyncSSLSocket.cpp | 110 +++++++++++++++++++++ folly/io/async/AsyncSSLSocket.h | 110 ++------------------- folly/io/async/ssl/OpenSSLUtils.cpp | 56 ++++++++++- folly/io/async/ssl/OpenSSLUtils.h | 10 ++ folly/io/async/ssl/TLSDefinitions.h | 2 - folly/io/async/test/AsyncSSLSocketTest.cpp | 18 +++- 6 files changed, 194 insertions(+), 112 deletions(-) diff --git a/folly/io/async/AsyncSSLSocket.cpp b/folly/io/async/AsyncSSLSocket.cpp index 8175f8a5..bccfd421 100644 --- a/folly/io/async/AsyncSSLSocket.cpp +++ b/folly/io/async/AsyncSSLSocket.cpp @@ -1739,4 +1739,114 @@ void AsyncSSLSocket::clientHelloParsingCallback(int written, sock->resetClientHelloParsing(ssl); } +void AsyncSSLSocket::getSSLClientCiphers( + std::string& clientCiphers, + bool convertToString) const { + std::string ciphers; + + if (parseClientHello_ == false + || clientHelloInfo_->clientHelloCipherSuites_.empty()) { + clientCiphers = ""; + return; + } + + bool first = true; + for (auto originalCipherCode : clientHelloInfo_->clientHelloCipherSuites_) + { + if (first) { + first = false; + } else { + ciphers += ":"; + } + + bool nameFound = convertToString; + + if (convertToString) { + const auto& name = OpenSSLUtils::getCipherName(originalCipherCode); + if (name.empty()) { + nameFound = false; + } else { + ciphers += name; + } + } + + if (!nameFound) { + folly::hexlify( + std::array{{ + static_cast((originalCipherCode >> 8) & 0xffL), + static_cast(originalCipherCode & 0x00ffL) }}, + ciphers, + /* append to ciphers = */ true); + } + } + + clientCiphers = std::move(ciphers); +} + +std::string AsyncSSLSocket::getSSLClientComprMethods() const { + if (!parseClientHello_) { + return ""; + } + return folly::join(":", clientHelloInfo_->clientHelloCompressionMethods_); +} + +std::string AsyncSSLSocket::getSSLClientExts() const { + if (!parseClientHello_) { + return ""; + } + return folly::join(":", clientHelloInfo_->clientHelloExtensions_); +} + +std::string AsyncSSLSocket::getSSLClientSigAlgs() const { + if (!parseClientHello_) { + return ""; + } + + std::string sigAlgs; + sigAlgs.reserve(clientHelloInfo_->clientHelloSigAlgs_.size() * 4); + for (size_t i = 0; i < clientHelloInfo_->clientHelloSigAlgs_.size(); i++) { + if (i) { + sigAlgs.push_back(':'); + } + sigAlgs.append(folly::to( + clientHelloInfo_->clientHelloSigAlgs_[i].first)); + sigAlgs.push_back(','); + sigAlgs.append(folly::to( + clientHelloInfo_->clientHelloSigAlgs_[i].second)); + } + + return sigAlgs; +} + +std::string AsyncSSLSocket::getSSLAlertsReceived() const { + std::string ret; + + for (const auto& alert : alertsReceived_) { + if (!ret.empty()) { + ret.append(","); + } + ret.append(folly::to(alert.first, ": ", alert.second)); + } + + return ret; +} + +void AsyncSSLSocket::getSSLSharedCiphers(std::string& sharedCiphers) const { + char ciphersBuffer[1024]; + ciphersBuffer[0] = '\0'; + SSL_get_shared_ciphers(ssl_, ciphersBuffer, sizeof(ciphersBuffer) - 1); + sharedCiphers = ciphersBuffer; +} + +void AsyncSSLSocket::getSSLServerCiphers(std::string& serverCiphers) const { + serverCiphers = SSL_get_cipher_list(ssl_, 0); + int i = 1; + const char *cipher; + while ((cipher = SSL_get_cipher_list(ssl_, i)) != nullptr) { + serverCiphers.append(":"); + serverCiphers.append(cipher); + i++; + } +} + } // namespace diff --git a/folly/io/async/AsyncSSLSocket.h b/folly/io/async/AsyncSSLSocket.h index 45fbaea4..50ec4478 100644 --- a/folly/io/async/AsyncSSLSocket.h +++ b/folly/io/async/AsyncSSLSocket.h @@ -547,129 +547,33 @@ class AsyncSSLSocket : public virtual AsyncSocket { */ void getSSLClientCiphers( std::string& clientCiphers, - bool convertToString = true) const { - std::stringstream ciphersStream; - std::string cipherName; - - if (parseClientHello_ == false - || clientHelloInfo_->clientHelloCipherSuites_.empty()) { - clientCiphers = ""; - return; - } - - for (auto originalCipherCode : clientHelloInfo_->clientHelloCipherSuites_) - { - const SSL_CIPHER* cipher = nullptr; - if (convertToString) { - // OpenSSL expects code as a big endian char array - auto cipherCode = htons(originalCipherCode); - -#if defined(SSL_OP_NO_TLSv1_2) - cipher = - TLSv1_2_method()->get_cipher_by_char((unsigned char*)&cipherCode); -#elif defined(SSL_OP_NO_TLSv1_1) - cipher = - TLSv1_1_method()->get_cipher_by_char((unsigned char*)&cipherCode); -#elif defined(SSL_OP_NO_TLSv1) - cipher = - TLSv1_method()->get_cipher_by_char((unsigned char*)&cipherCode); -#else - cipher = - SSLv3_method()->get_cipher_by_char((unsigned char*)&cipherCode); -#endif - } - - if (cipher == nullptr) { - ciphersStream << std::setfill('0') << std::setw(4) << std::hex - << originalCipherCode << ":"; - } else { - ciphersStream << SSL_CIPHER_get_name(cipher) << ":"; - } - } - - clientCiphers = ciphersStream.str(); - clientCiphers.erase(clientCiphers.end() - 1); - } + bool convertToString = true) const; /** * Get the list of compression methods sent by the client in TLS Hello. */ - std::string getSSLClientComprMethods() const { - if (!parseClientHello_) { - return ""; - } - return folly::join(":", clientHelloInfo_->clientHelloCompressionMethods_); - } + std::string getSSLClientComprMethods() const; /** * Get the list of TLS extensions sent by the client in the TLS Hello. */ - std::string getSSLClientExts() const { - if (!parseClientHello_) { - return ""; - } - return folly::join(":", clientHelloInfo_->clientHelloExtensions_); - } - - std::string getSSLClientSigAlgs() const { - if (!parseClientHello_) { - return ""; - } - - std::string sigAlgs; - sigAlgs.reserve(clientHelloInfo_->clientHelloSigAlgs_.size() * 4); - for (size_t i = 0; i < clientHelloInfo_->clientHelloSigAlgs_.size(); i++) { - if (i) { - sigAlgs.push_back(':'); - } - sigAlgs.append(folly::to( - clientHelloInfo_->clientHelloSigAlgs_[i].first)); - sigAlgs.push_back(','); - sigAlgs.append(folly::to( - clientHelloInfo_->clientHelloSigAlgs_[i].second)); - } + std::string getSSLClientExts() const; - return sigAlgs; - } - - std::string getSSLAlertsReceived() const { - std::string ret; + std::string getSSLClientSigAlgs() const; - for (const auto& alert : alertsReceived_) { - if (!ret.empty()) { - ret.append(","); - } - ret.append(folly::to(alert.first, ": ", alert.second)); - } - - return ret; - } + std::string getSSLAlertsReceived() const; /** * Get the list of shared ciphers between the server and the client. * Works well for only SSLv2, not so good for SSLv3 or TLSv1. */ - void getSSLSharedCiphers(std::string& sharedCiphers) const { - char ciphersBuffer[1024]; - ciphersBuffer[0] = '\0'; - SSL_get_shared_ciphers(ssl_, ciphersBuffer, sizeof(ciphersBuffer) - 1); - sharedCiphers = ciphersBuffer; - } + void getSSLSharedCiphers(std::string& sharedCiphers) const; /** * Get the list of ciphers supported by the server in the server's * preference order. */ - void getSSLServerCiphers(std::string& serverCiphers) const { - serverCiphers = SSL_get_cipher_list(ssl_, 0); - int i = 1; - const char *cipher; - while ((cipher = SSL_get_cipher_list(ssl_, i)) != nullptr) { - serverCiphers.append(":"); - serverCiphers.append(cipher); - i++; - } - } + void getSSLServerCiphers(std::string& serverCiphers) const; static int getSSLExDataIndex(); static AsyncSSLSocket* getFromSSL(const SSL *ssl); diff --git a/folly/io/async/ssl/OpenSSLUtils.cpp b/folly/io/async/ssl/OpenSSLUtils.cpp index 81bdc1a3..309327ed 100644 --- a/folly/io/async/ssl/OpenSSLUtils.cpp +++ b/folly/io/async/ssl/OpenSSLUtils.cpp @@ -16,14 +16,13 @@ #include #include #include - +#include #include #include #include #include #include - -#include +#include #define OPENSSL_IS_101 (OPENSSL_VERSION_NUMBER >= 0x1000105fL && \ OPENSSL_VERSION_NUMBER < 0x1000200fL) @@ -147,6 +146,57 @@ bool OpenSSLUtils::validatePeerCertNames(X509* cert, return false; } +static std::unordered_map getOpenSSLCipherNames() { + std::unordered_map ret; + SSL_CTX* ctx = nullptr; + SSL* ssl = nullptr; + + const SSL_METHOD* meth = SSLv23_server_method(); + OpenSSL_add_ssl_algorithms(); + + if ((ctx = SSL_CTX_new(meth)) == nullptr) { + return ret; + } + SCOPE_EXIT { + SSL_CTX_free(ctx); + }; + + if ((ssl = SSL_new(ctx)) == nullptr) { + return ret; + } + SCOPE_EXIT { + SSL_free(ssl); + }; + + STACK_OF(SSL_CIPHER)* sk = SSL_get_ciphers(ssl); + for (size_t i = 0; i < (size_t)sk_SSL_CIPHER_num(sk); i++) { + SSL_CIPHER* c; + + c = sk_SSL_CIPHER_value(sk, i); + unsigned long id = SSL_CIPHER_get_id(c); + // OpenSSL 1.0.2 and prior does weird things such as stuff the SSL/TLS + // version into the top 16 bits. Let's ignore those for now. This is + // BoringSSL compatible (their id can be cast as uint16_t) + uint16_t cipherCode = id & 0xffffL; + ret[cipherCode] = SSL_CIPHER_get_name(c); + } + return ret; +} + +const std::string& OpenSSLUtils::getCipherName(uint16_t cipherCode) { + // Having this in a hash map saves the binary search inside OpenSSL + static std::unordered_map cipherCodeToName( + getOpenSSLCipherNames()); + + const auto& iter = cipherCodeToName.find(cipherCode); + if (iter != cipherCodeToName.end()) { + return iter->second; + } else { + static std::string empty(""); + return empty; + } +} + bool OpenSSLUtils::setCustomBioReadMethod( BIO_METHOD* bioMeth, int (*meth)(BIO*, char*, int)) { diff --git a/folly/io/async/ssl/OpenSSLUtils.h b/folly/io/async/ssl/OpenSSLUtils.h index 8f5ea87a..199ecfdf 100644 --- a/folly/io/async/ssl/OpenSSLUtils.h +++ b/folly/io/async/ssl/OpenSSLUtils.h @@ -81,6 +81,16 @@ class OpenSSLUtils { sockaddr_storage* addrStorage, socklen_t* addrLen); + /** + * Get a stringified cipher name (e.g., ECDHE-ECDSA-CHACHA20-POLY1305) given + * the 2-byte code (e.g., 0xcca9) for the cipher. The name conversion only + * works for the ciphers built into the linked OpenSSL library + * + * @param cipherCode A 16-bit IANA cipher code (machine endianness) + * @return Cipher name, or empty if the code is not found + */ + static const std::string& getCipherName(uint16_t cipherCode); + /** * Wrappers for BIO operations that may be different across different * versions/flavors of OpenSSL (including forks like BoringSSL) diff --git a/folly/io/async/ssl/TLSDefinitions.h b/folly/io/async/ssl/TLSDefinitions.h index cf803d10..17209100 100644 --- a/folly/io/async/ssl/TLSDefinitions.h +++ b/folly/io/async/ssl/TLSDefinitions.h @@ -19,8 +19,6 @@ #include #include #include -#include -#include #include namespace folly { diff --git a/folly/io/async/test/AsyncSSLSocketTest.cpp b/folly/io/async/test/AsyncSSLSocketTest.cpp index 09201229..65c056f6 100644 --- a/folly/io/async/test/AsyncSSLSocketTest.cpp +++ b/folly/io/async/test/AsyncSSLSocketTest.cpp @@ -1008,14 +1008,14 @@ TEST(AsyncSSLSocketTest, SSLParseClientHelloSuccess) { auto clientCtx = std::make_shared(); auto serverCtx = std::make_shared(); serverCtx->setVerificationOption(SSLContext::SSLVerifyPeerEnum::VERIFY); - serverCtx->ciphers("RSA:!SHA:!NULL:!SHA256@STRENGTH"); + serverCtx->ciphers("ECDHE-RSA-AES128-SHA:AES128-SHA:AES256-SHA"); serverCtx->loadPrivateKey(testKey); serverCtx->loadCertificate(testCert); serverCtx->loadTrustedCertificates(testCA); serverCtx->loadClientCAList(testCA); clientCtx->setVerificationOption(SSLContext::SSLVerifyPeerEnum::VERIFY); - clientCtx->ciphers("RC4-SHA:AES128-SHA:AES256-SHA:RC4-MD5"); + clientCtx->ciphers("AES256-SHA:RC4-MD5"); clientCtx->loadPrivateKey(testKey); clientCtx->loadCertificate(testCert); clientCtx->loadTrustedCertificates(testCA); @@ -1033,8 +1033,8 @@ TEST(AsyncSSLSocketTest, SSLParseClientHelloSuccess) { eventBase.loop(); - EXPECT_EQ(server.clientCiphers_, - "RC4-SHA:AES128-SHA:AES256-SHA:RC4-MD5:00ff"); + EXPECT_EQ(server.clientCiphers_, "AES256-SHA:RC4-MD5:00ff"); + EXPECT_EQ(server.chosenCipher_, "AES256-SHA"); EXPECT_TRUE(client.handshakeVerify_); EXPECT_TRUE(client.handshakeSuccess_); EXPECT_TRUE(!client.handshakeError_); @@ -1695,6 +1695,16 @@ TEST(AsyncSSLSocketTest, ConnOpenSSLErrorString) { std::string::npos); } +TEST(AsyncSSLSocketTest, TestSSLCipherCodeToNameMap) { + using folly::ssl::OpenSSLUtils; + EXPECT_EQ( + OpenSSLUtils::getCipherName(0xc02c), "ECDHE-ECDSA-AES256-GCM-SHA384"); + // TLS_DHE_RSA_WITH_DES_CBC_SHA - We shouldn't be building with this + EXPECT_EQ(OpenSSLUtils::getCipherName(0x0015), ""); + // This indicates TLS_EMPTY_RENEGOTIATION_INFO_SCSV, no name expected + EXPECT_EQ(OpenSSLUtils::getCipherName(0x00ff), ""); +} + #if FOLLY_ALLOW_TFO class MockAsyncTFOSSLSocket : public AsyncSSLSocket { -- 2.34.1