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 8175f8a..bccfd42 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 45fbaea..50ec447 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 81bdc1a..309327e 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 8f5ea87..199ecfd 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 cf803d1..1720910 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 0920122..65c056f 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 {