Add getCommonName and getSubjectAltName Cert utility helpers
authorMichael Lee <mzlee@fb.com>
Tue, 21 Feb 2017 18:24:08 +0000 (10:24 -0800)
committerFacebook Github Bot <facebook-github-bot@users.noreply.github.com>
Tue, 21 Feb 2017 18:35:47 +0000 (10:35 -0800)
Summary: `getCommonName` and `getSubjectAltName` are useful shared utility functions.

Reviewed By: anirudhvr

Differential Revision: D4546866

fbshipit-source-id: bcefc1ff3a02df93fbf0cd2d4f24569928bd1753

folly/Makefile.am
folly/ssl/OpenSSLCertUtils.cpp [new file with mode: 0644]
folly/ssl/OpenSSLCertUtils.h [new file with mode: 0644]
folly/ssl/test/OpenSSLCertUtilsTest.cpp [new file with mode: 0644]

index 39c4636..ba3a9ab 100644 (file)
@@ -341,6 +341,7 @@ nobase_follyinclude_HEADERS = \
        SpinLock.h \
        SpookyHashV1.h \
        SpookyHashV2.h \
+       ssl/OpenSSLCertUtils.h \
        ssl/OpenSSLHash.h \
        ssl/OpenSSLVersionFinder.h \
        ssl/SSLSession.h \
@@ -506,6 +507,7 @@ libfolly_la_SOURCES = \
        SocketAddress.cpp \
        SpookyHashV1.cpp \
        SpookyHashV2.cpp \
+       ssl/OpenSSLCertUtils.cpp \
        ssl/OpenSSLHash.cpp \
        ssl/detail/SSLSessionImpl.cpp \
        stats/Instantiations.cpp \
diff --git a/folly/ssl/OpenSSLCertUtils.cpp b/folly/ssl/OpenSSLCertUtils.cpp
new file mode 100644 (file)
index 0000000..ae61904
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017-present Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <folly/ssl/OpenSSLCertUtils.h>
+
+#include <openssl/x509.h>
+#include <openssl/x509v3.h>
+
+#include <folly/ScopeGuard.h>
+
+namespace folly {
+namespace ssl {
+
+Optional<std::string> OpenSSLCertUtils::getCommonName(X509& x509) {
+  auto subject = X509_get_subject_name(&x509);
+  if (!subject) {
+    return none;
+  }
+
+  auto cnLoc = X509_NAME_get_index_by_NID(subject, NID_commonName, -1);
+  if (cnLoc < 0) {
+    return none;
+  }
+
+  auto cnEntry = X509_NAME_get_entry(subject, cnLoc);
+  if (!cnEntry) {
+    return none;
+  }
+
+  auto cnAsn = X509_NAME_ENTRY_get_data(cnEntry);
+  if (!cnAsn) {
+    return none;
+  }
+
+  auto cnData = reinterpret_cast<const char*>(ASN1_STRING_data(cnAsn));
+  auto cnLen = ASN1_STRING_length(cnAsn);
+  if (!cnData || cnLen <= 0) {
+    return none;
+  }
+
+  return Optional<std::string>(std::string(cnData, cnLen));
+}
+
+std::vector<std::string> OpenSSLCertUtils::getSubjectAltNames(X509& x509) {
+  auto names = reinterpret_cast<STACK_OF(GENERAL_NAME)*>(
+      X509_get_ext_d2i(&x509, NID_subject_alt_name, nullptr, nullptr));
+  if (!names) {
+    return {};
+  }
+  SCOPE_EXIT {
+    sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);
+  };
+
+  std::vector<std::string> ret;
+  auto count = sk_GENERAL_NAME_num(names);
+  for (int i = 0; i < count; i++) {
+    auto genName = sk_GENERAL_NAME_value(names, i);
+    if (!genName || genName->type != GEN_DNS) {
+      continue;
+    }
+    auto nameData =
+        reinterpret_cast<const char*>(ASN1_STRING_data(genName->d.dNSName));
+    auto nameLen = ASN1_STRING_length(genName->d.dNSName);
+    if (!nameData || nameLen <= 0) {
+      continue;
+    }
+    ret.emplace_back(nameData, nameLen);
+  }
+  return ret;
+}
+}
+}
diff --git a/folly/ssl/OpenSSLCertUtils.h b/folly/ssl/OpenSSLCertUtils.h
new file mode 100644 (file)
index 0000000..82e30ff
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <openssl/x509.h>
+
+#include <folly/Optional.h>
+
+namespace folly {
+namespace ssl {
+
+class OpenSSLCertUtils {
+ public:
+  // Note: non-const until OpenSSL 1.1.0
+  static Optional<std::string> getCommonName(X509& x509);
+
+  static std::vector<std::string> getSubjectAltNames(X509& x509);
+};
+}
+}
diff --git a/folly/ssl/test/OpenSSLCertUtilsTest.cpp b/folly/ssl/test/OpenSSLCertUtilsTest.cpp
new file mode 100644 (file)
index 0000000..e41d0b6
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <folly/ssl/OpenSSLCertUtils.h>
+
+#include <openssl/bio.h>
+#include <openssl/evp.h>
+
+#include <folly/Range.h>
+#include <folly/String.h>
+#include <folly/io/async/ssl/OpenSSLPtrTypes.h>
+#include <folly/portability/GTest.h>
+#include <folly/portability/OpenSSL.h>
+
+using namespace testing;
+using namespace folly;
+
+const char* kTestCertWithoutSan = "folly/io/async/test/certs/tests-cert.pem";
+
+// Test key
+// -----BEGIN EC PRIVATE KEY-----
+// MHcCAQEEIBskFwVZ9miFN+SKCFZPe9WEuFGmP+fsecLUnsTN6bOcoAoGCCqGSM49
+// AwEHoUQDQgAE7/f4YYOYunAM/VkmjDYDg3AWUgyyTIraWmmQZsnu0bYNV/lLLfNz
+// CtHggxGSwEtEe40nNb9C8wQmHUvb7VBBlw==
+// -----END EC PRIVATE KEY-----
+const std::string kTestCertWithSan = folly::stripLeftMargin(R"(
+  -----BEGIN CERTIFICATE-----
+  MIIDXDCCAkSgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBQMQswCQYDVQQGEwJVUzEL
+  MAkGA1UECAwCQ0ExDTALBgNVBAoMBEFzb3gxJTAjBgNVBAMMHEFzb3ggQ2VydGlm
+  aWNhdGlvbiBBdXRob3JpdHkwHhcNMTcwMjEzMjMyMTAzWhcNNDQwNzAxMjMyMTAz
+  WjAwMQswCQYDVQQGEwJVUzENMAsGA1UECgwEQXNveDESMBAGA1UEAwwJMTI3LjAu
+  MC4xMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7/f4YYOYunAM/VkmjDYDg3AW
+  UgyyTIraWmmQZsnu0bYNV/lLLfNzCtHggxGSwEtEe40nNb9C8wQmHUvb7VBBl6OC
+  ASowggEmMAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJh
+  dGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBRx1kmdZEfXHmWLHpSDI0Lh8hmfwzAf
+  BgNVHSMEGDAWgBQX3ykJKb97nxp/6UZJyDvts7noezAxBgNVHREEKjAoghJhbm90
+  aGVyZXhhbXBsZS5jb22CEioudGhpcmRleGFtcGxlLmNvbTB4BggrBgEFBQcBAQRs
+  MGowaAYIKwYBBQUHMAKGXGh0dHBzOi8vcGhhYnJpY2F0b3IuZmIuY29tL2RpZmZ1
+  c2lvbi9GQkNPREUvYnJvd3NlL21hc3Rlci90aS90ZXN0X2NlcnRzL2NhX2NlcnQu
+  cGVtP3ZpZXc9cmF3MA0GCSqGSIb3DQEBCwUAA4IBAQCj3FLjLMLudaFDiYo9pAPQ
+  NBYNpG27aajQCvnEsYaMAGnNBxUUhv/E4xpnJEhatiCJWlPgGebdjXkpXYkLxnFj
+  38UmpfZbNcvPPKxXmjIlkpYeFwcHTAUpFmMXVHdr8FjkDSN+qWHLllMFNAAqp0U6
+  4VWjDlq9xCjzNw+8fdcEpwylpPrbNyQHqSO1k+DhM2qPuQfiWPmHe2PbJv8JB3no
+  HWGi9SNe0FjtJM3066L0Gj8g/bFDo/pnyKguQyGkS7PaepK5/u5Y2fMMBO/m4+U0
+  b9Yb0TvatsqL688CoZcSn73A0yAjptwbD/4HmcVlG2j/y8eTVpXisugu6Xz+QQGu
+  -----END CERTIFICATE-----
+)");
+
+static folly::ssl::X509UniquePtr readCertFromFile(const std::string& filename) {
+  folly::ssl::BioUniquePtr bio(BIO_new(BIO_s_file()));
+  if (!bio) {
+    throw std::runtime_error("Couldn't create BIO");
+  }
+
+  if (BIO_read_filename(bio.get(), filename.c_str()) != 1) {
+    throw std::runtime_error("Couldn't read cert file: " + filename);
+  }
+  return folly::ssl::X509UniquePtr(
+      PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr));
+}
+
+static folly::ssl::X509UniquePtr readCertFromData(
+    const folly::StringPiece data) {
+  folly::ssl::BioUniquePtr bio(BIO_new_mem_buf(data.data(), data.size()));
+  if (!bio) {
+    throw std::runtime_error("Couldn't create BIO");
+  }
+  return folly::ssl::X509UniquePtr(
+      PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr));
+}
+
+TEST(OpenSSLCertUtilsTest, TestX509CN) {
+  OpenSSL_add_all_algorithms();
+
+  auto x509 = readCertFromFile(kTestCertWithoutSan);
+  EXPECT_NE(x509, nullptr);
+  auto identity = folly::ssl::OpenSSLCertUtils::getCommonName(*x509);
+  EXPECT_EQ(identity.value(), "Asox Company");
+  auto sans = folly::ssl::OpenSSLCertUtils::getSubjectAltNames(*x509);
+  EXPECT_EQ(sans.size(), 0);
+}
+
+TEST(OpenSSLCertUtilsTest, TestX509Sans) {
+  OpenSSL_add_all_algorithms();
+
+  auto x509 = readCertFromData(kTestCertWithSan);
+  EXPECT_NE(x509, nullptr);
+  auto identity = folly::ssl::OpenSSLCertUtils::getCommonName(*x509);
+  EXPECT_EQ(identity.value(), "127.0.0.1");
+  auto altNames = folly::ssl::OpenSSLCertUtils::getSubjectAltNames(*x509);
+  EXPECT_EQ(altNames.size(), 2);
+  EXPECT_EQ(altNames[0], "anotherexample.com");
+  EXPECT_EQ(altNames[1], "*.thirdexample.com");
+}