From 68493524ff3e8d85e6843ea5527cbbef26d138f9 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Tue, 21 Feb 2017 10:24:08 -0800 Subject: [PATCH 1/1] Add getCommonName and getSubjectAltName Cert utility helpers Summary: `getCommonName` and `getSubjectAltName` are useful shared utility functions. Reviewed By: anirudhvr Differential Revision: D4546866 fbshipit-source-id: bcefc1ff3a02df93fbf0cd2d4f24569928bd1753 --- folly/Makefile.am | 2 + folly/ssl/OpenSSLCertUtils.cpp | 84 +++++++++++++++++++ folly/ssl/OpenSSLCertUtils.h | 36 ++++++++ folly/ssl/test/OpenSSLCertUtilsTest.cpp | 107 ++++++++++++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 folly/ssl/OpenSSLCertUtils.cpp create mode 100644 folly/ssl/OpenSSLCertUtils.h create mode 100644 folly/ssl/test/OpenSSLCertUtilsTest.cpp diff --git a/folly/Makefile.am b/folly/Makefile.am index 39c46367..ba3a9abf 100644 --- a/folly/Makefile.am +++ b/folly/Makefile.am @@ -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 index 00000000..ae619048 --- /dev/null +++ b/folly/ssl/OpenSSLCertUtils.cpp @@ -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 + +#include +#include + +#include + +namespace folly { +namespace ssl { + +Optional 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(ASN1_STRING_data(cnAsn)); + auto cnLen = ASN1_STRING_length(cnAsn); + if (!cnData || cnLen <= 0) { + return none; + } + + return Optional(std::string(cnData, cnLen)); +} + +std::vector OpenSSLCertUtils::getSubjectAltNames(X509& x509) { + auto names = reinterpret_cast( + 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 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(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 index 00000000..82e30ffb --- /dev/null +++ b/folly/ssl/OpenSSLCertUtils.h @@ -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 +#include + +#include + +#include + +namespace folly { +namespace ssl { + +class OpenSSLCertUtils { + public: + // Note: non-const until OpenSSL 1.1.0 + static Optional getCommonName(X509& x509); + + static std::vector getSubjectAltNames(X509& x509); +}; +} +} diff --git a/folly/ssl/test/OpenSSLCertUtilsTest.cpp b/folly/ssl/test/OpenSSLCertUtilsTest.cpp new file mode 100644 index 00000000..e41d0b67 --- /dev/null +++ b/folly/ssl/test/OpenSSLCertUtilsTest.cpp @@ -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 + +#include +#include + +#include +#include +#include +#include +#include + +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"); +} -- 2.34.1