AsyncSSLSocket::getSSLClientCiphers using static map
authorAnirudh Ramachandran <avr@fb.com>
Mon, 26 Sep 2016 20:14:55 +0000 (13:14 -0700)
committerFacebook Github Bot 3 <facebook-github-bot-3-bot@fb.com>
Mon, 26 Sep 2016 20:23:36 +0000 (13:23 -0700)
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
folly/io/async/AsyncSSLSocket.h
folly/io/async/ssl/OpenSSLUtils.cpp
folly/io/async/ssl/OpenSSLUtils.h
folly/io/async/ssl/TLSDefinitions.h
folly/io/async/test/AsyncSSLSocketTest.cpp

index 8175f8a5ac9157a3b31d8700abbe5abff206a82e..bccfd421ce366f9365b6d4332d2411a9384ccce9 100644 (file)
@@ -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<uint8_t, 2>{{
+              static_cast<uint8_t>((originalCipherCode >> 8) & 0xffL),
+              static_cast<uint8_t>(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<std::string>(
+        clientHelloInfo_->clientHelloSigAlgs_[i].first));
+    sigAlgs.push_back(',');
+    sigAlgs.append(folly::to<std::string>(
+        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<std::string>(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
index 45fbaea4a97d5f391da1ee23c466a90309153cae..50ec447848ac813e36704f793f816155b15d9b71 100644 (file)
@@ -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<std::string>(
-          clientHelloInfo_->clientHelloSigAlgs_[i].first));
-      sigAlgs.push_back(',');
-      sigAlgs.append(folly::to<std::string>(
-          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<std::string>(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);
index 81bdc1a33e1319524026d8dedb7731cedac88649..309327ed2de7f08da544f4e15d69d0dc6d1016f6 100644 (file)
 #include <folly/io/async/ssl/OpenSSLUtils.h>
 #include <folly/ScopeGuard.h>
 #include <folly/portability/Sockets.h>
-
+#include <glog/logging.h>
 #include <openssl/bio.h>
 #include <openssl/err.h>
 #include <openssl/rand.h>
 #include <openssl/ssl.h>
 #include <openssl/x509v3.h>
-
-#include <glog/logging.h>
+#include <unordered_map>
 
 #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<uint16_t, std::string> getOpenSSLCipherNames() {
+  std::unordered_map<uint16_t, std::string> 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<uint16_t, std::string> 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)) {
index 8f5ea87ae28622f2e6d6a01f53d590a61f9fbcd9..199ecfdf8b10b9efd42b101171beb77ff2db49e4 100644 (file)
@@ -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)
index cf803d10357be1a37e321980108458442a44218a..1720910046ec9a864669572ba5db539b8b67dfd1 100644 (file)
@@ -19,8 +19,6 @@
 #include <folly/io/Cursor.h>
 #include <folly/io/IOBuf.h>
 #include <map>
-#include <openssl/ssl.h>
-#include <openssl/tls1.h>
 #include <vector>
 
 namespace folly {
index 0920122930bbf15557ac6850bef01a41a95419fb..65c056f6e8a6d5d7ab1baa2385ff22bfc0ba4d25 100644 (file)
@@ -1008,14 +1008,14 @@ TEST(AsyncSSLSocketTest, SSLParseClientHelloSuccess) {
   auto clientCtx = std::make_shared<SSLContext>();
   auto serverCtx = std::make_shared<SSLContext>();
   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 {