diff --git a/src/iocore/net/CMakeLists.txt b/src/iocore/net/CMakeLists.txt index ac3f12d7cc8..29618c96764 100644 --- a/src/iocore/net/CMakeLists.txt +++ b/src/iocore/net/CMakeLists.txt @@ -52,6 +52,7 @@ add_library( SSLSessionCache.cc SSLSessionTicket.cc SSLUtils.cc + SSLKeyUtils.cc OCSPStapling.cc TLSBasicSupport.cc TLSEventSupport.cc @@ -144,6 +145,9 @@ if(BUILD_TESTING) unit_tests/unit_test_main.cc unit_tests/benchmark_TLSCertCompression.cc ) + if(SSLLIB_IS_OPENSSL3) + target_sources(test_net PRIVATE unit_tests/test_SSLDHParams.cc) + endif() # Use link groups to solve circular dependency set(LINK_GROUP_LIBS ts::logging diff --git a/src/iocore/net/P_SSLUtils.h b/src/iocore/net/P_SSLUtils.h index 8e2cbf57884..af5728e4869 100644 --- a/src/iocore/net/P_SSLUtils.h +++ b/src/iocore/net/P_SSLUtils.h @@ -25,6 +25,10 @@ #include "iocore/net/SSLTypes.h" #include "tscore/Diags.h" +#ifdef OPENSSL_IS_OPENSSL3 +#include +#include +#endif #define OPENSSL_THREAD_DEFINES #if __has_include() #include @@ -110,6 +114,24 @@ namespace detail } }; +#ifdef OPENSSL_IS_OPENSSL3 + struct PKEYCTXDeleter { + void + operator()(EVP_PKEY_CTX *pctx) + { + EVP_PKEY_CTX_free(pctx); + } + }; + + struct DecoderCTXDeleter { + void + operator()(OSSL_DECODER_CTX *dctx) + { + OSSL_DECODER_CTX_free(dctx); + } + }; +#endif + } // namespace detail } // namespace ssl @@ -134,3 +156,7 @@ struct ats_wildcard_matcher { using scoped_X509 = std::unique_ptr; using scoped_BIO = std::unique_ptr; +#ifdef OPENSSL_IS_OPENSSL3 +using scoped_PKEY_CTX = std::unique_ptr; +using scoped_Decoder_CTX = std::unique_ptr; +#endif diff --git a/src/iocore/net/SSLKeyUtils.cc b/src/iocore/net/SSLKeyUtils.cc new file mode 100644 index 00000000000..9153f810781 --- /dev/null +++ b/src/iocore/net/SSLKeyUtils.cc @@ -0,0 +1,218 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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 "SSLKeyUtils.h" +#include "P_SSLUtils.h" + +#include +#ifdef OPENSSL_IS_OPENSSL3 +#include +#else +#include +#endif + +#include +#include +#include +#include +#include + +#ifdef OPENSSL_IS_OPENSSL3 +#include +#include +#else +#include +#endif + +#ifdef OPENSSL_IS_OPENSSL3 + +EVP_PKEY * +gen_dh_2048_256_pkey() +{ + scoped_PKEY_CTX pctx{EVP_PKEY_CTX_new_from_name(NULL, "DH", NULL)}; + if (!pctx) { + Error("failed to create OpenSSL pkey context"); + return nullptr; + } + + if (EVP_PKEY_keygen_init(pctx.get()) <= 0) { + Error("failed to initialize OpenSSL keygen"); + return nullptr; + } + + char prime_group[]{"dh_2048_256"}; + OSSL_PARAM const params[]{OSSL_PARAM_utf8_string("group", prime_group, 0), OSSL_PARAM_END}; + + if (!EVP_PKEY_CTX_set_params(pctx.get(), params)) { + Error("SSL dhparams source returned invalid parameters"); + return nullptr; + } + + EVP_PKEY *pkey{}; + EVP_PKEY_generate(pctx.get(), &pkey); + + return pkey; +} + +EVP_PKEY * +load_dhparams_file(char const *dhparams_file) +{ + EVP_PKEY *pkey{}; + scoped_Decoder_CTX dctx{OSSL_DECODER_CTX_new_for_pkey(&pkey, "PEM", NULL, "DH", OSSL_KEYMGMT_SELECT_ALL_PARAMETERS, NULL, NULL)}; + if (!dctx) { + Error("failed to create OpenSSL decoder context"); + return nullptr; + } + + ink_assert(OSSL_DECODER_CTX_get_num_decoders(dctx.get()) > 0); + scoped_BIO bio{BIO_new_file(dhparams_file, "r")}; + if (!OSSL_DECODER_from_bio(dctx.get(), bio.get())) { + Error("SSL dhparams source returned invalid parameters"); + return nullptr; + } + + return pkey; +} + +bool +set_ctx_dh(SSL_CTX *ctx, dh_key_t *pkey) +{ + bool result{SSL_CTX_set_options(ctx, SSL_OP_SINGLE_DH_USE) && SSL_CTX_set0_tmp_dh_pkey(ctx, pkey)}; + if (!result) { + EVP_PKEY_free(pkey); + } + return result; +} + +#else + +DH * +load_dhparams_file(char const *dhparams_file) +{ + scoped_BIO bio(BIO_new_file(dhparams_file, "r")); + DH *dh{PEM_read_bio_DHparams(bio.get(), nullptr, nullptr, nullptr)}; + if (!dh) { + Error("SSL dhparams source returned invalid parameters"); + return nullptr; + } + + return dh; +} + +#if TS_USE_GET_DH_2048_256 +DH * +gen_dh_2048_256_pkey() +{ + return DH_get_2048_256(); +} +#else +DH * +gen_dh_2048_256_pkey() +{ + /* Build 2048-bit MODP Group with 256-bit Prime Order Subgroup from RFC 5114 */ + static const unsigned char dh2048_p[] = { + 0x87, 0xA8, 0xE6, 0x1D, 0xB4, 0xB6, 0x66, 0x3C, 0xFF, 0xBB, 0xD1, 0x9C, 0x65, 0x19, 0x59, 0x99, 0x8C, 0xEE, 0xF6, 0x08, + 0x66, 0x0D, 0xD0, 0xF2, 0x5D, 0x2C, 0xEE, 0xD4, 0x43, 0x5E, 0x3B, 0x00, 0xE0, 0x0D, 0xF8, 0xF1, 0xD6, 0x19, 0x57, 0xD4, + 0xFA, 0xF7, 0xDF, 0x45, 0x61, 0xB2, 0xAA, 0x30, 0x16, 0xC3, 0xD9, 0x11, 0x34, 0x09, 0x6F, 0xAA, 0x3B, 0xF4, 0x29, 0x6D, + 0x83, 0x0E, 0x9A, 0x7C, 0x20, 0x9E, 0x0C, 0x64, 0x97, 0x51, 0x7A, 0xBD, 0x5A, 0x8A, 0x9D, 0x30, 0x6B, 0xCF, 0x67, 0xED, + 0x91, 0xF9, 0xE6, 0x72, 0x5B, 0x47, 0x58, 0xC0, 0x22, 0xE0, 0xB1, 0xEF, 0x42, 0x75, 0xBF, 0x7B, 0x6C, 0x5B, 0xFC, 0x11, + 0xD4, 0x5F, 0x90, 0x88, 0xB9, 0x41, 0xF5, 0x4E, 0xB1, 0xE5, 0x9B, 0xB8, 0xBC, 0x39, 0xA0, 0xBF, 0x12, 0x30, 0x7F, 0x5C, + 0x4F, 0xDB, 0x70, 0xC5, 0x81, 0xB2, 0x3F, 0x76, 0xB6, 0x3A, 0xCA, 0xE1, 0xCA, 0xA6, 0xB7, 0x90, 0x2D, 0x52, 0x52, 0x67, + 0x35, 0x48, 0x8A, 0x0E, 0xF1, 0x3C, 0x6D, 0x9A, 0x51, 0xBF, 0xA4, 0xAB, 0x3A, 0xD8, 0x34, 0x77, 0x96, 0x52, 0x4D, 0x8E, + 0xF6, 0xA1, 0x67, 0xB5, 0xA4, 0x18, 0x25, 0xD9, 0x67, 0xE1, 0x44, 0xE5, 0x14, 0x05, 0x64, 0x25, 0x1C, 0xCA, 0xCB, 0x83, + 0xE6, 0xB4, 0x86, 0xF6, 0xB3, 0xCA, 0x3F, 0x79, 0x71, 0x50, 0x60, 0x26, 0xC0, 0xB8, 0x57, 0xF6, 0x89, 0x96, 0x28, 0x56, + 0xDE, 0xD4, 0x01, 0x0A, 0xBD, 0x0B, 0xE6, 0x21, 0xC3, 0xA3, 0x96, 0x0A, 0x54, 0xE7, 0x10, 0xC3, 0x75, 0xF2, 0x63, 0x75, + 0xD7, 0x01, 0x41, 0x03, 0xA4, 0xB5, 0x43, 0x30, 0xC1, 0x98, 0xAF, 0x12, 0x61, 0x16, 0xD2, 0x27, 0x6E, 0x11, 0x71, 0x5F, + 0x69, 0x38, 0x77, 0xFA, 0xD7, 0xEF, 0x09, 0xCA, 0xDB, 0x09, 0x4A, 0xE9, 0x1E, 0x1A, 0x15, 0x97}; + static const unsigned char dh2048_g[] = { + 0x3F, 0xB3, 0x2C, 0x9B, 0x73, 0x13, 0x4D, 0x0B, 0x2E, 0x77, 0x50, 0x66, 0x60, 0xED, 0xBD, 0x48, 0x4C, 0xA7, 0xB1, 0x8F, + 0x21, 0xEF, 0x20, 0x54, 0x07, 0xF4, 0x79, 0x3A, 0x1A, 0x0B, 0xA1, 0x25, 0x10, 0xDB, 0xC1, 0x50, 0x77, 0xBE, 0x46, 0x3F, + 0xFF, 0x4F, 0xED, 0x4A, 0xAC, 0x0B, 0xB5, 0x55, 0xBE, 0x3A, 0x6C, 0x1B, 0x0C, 0x6B, 0x47, 0xB1, 0xBC, 0x37, 0x73, 0xBF, + 0x7E, 0x8C, 0x6F, 0x62, 0x90, 0x12, 0x28, 0xF8, 0xC2, 0x8C, 0xBB, 0x18, 0xA5, 0x5A, 0xE3, 0x13, 0x41, 0x00, 0x0A, 0x65, + 0x01, 0x96, 0xF9, 0x31, 0xC7, 0x7A, 0x57, 0xF2, 0xDD, 0xF4, 0x63, 0xE5, 0xE9, 0xEC, 0x14, 0x4B, 0x77, 0x7D, 0xE6, 0x2A, + 0xAA, 0xB8, 0xA8, 0x62, 0x8A, 0xC3, 0x76, 0xD2, 0x82, 0xD6, 0xED, 0x38, 0x64, 0xE6, 0x79, 0x82, 0x42, 0x8E, 0xBC, 0x83, + 0x1D, 0x14, 0x34, 0x8F, 0x6F, 0x2F, 0x91, 0x93, 0xB5, 0x04, 0x5A, 0xF2, 0x76, 0x71, 0x64, 0xE1, 0xDF, 0xC9, 0x67, 0xC1, + 0xFB, 0x3F, 0x2E, 0x55, 0xA4, 0xBD, 0x1B, 0xFF, 0xE8, 0x3B, 0x9C, 0x80, 0xD0, 0x52, 0xB9, 0x85, 0xD1, 0x82, 0xEA, 0x0A, + 0xDB, 0x2A, 0x3B, 0x73, 0x13, 0xD3, 0xFE, 0x14, 0xC8, 0x48, 0x4B, 0x1E, 0x05, 0x25, 0x88, 0xB9, 0xB7, 0xD2, 0xBB, 0xD2, + 0xDF, 0x01, 0x61, 0x99, 0xEC, 0xD0, 0x6E, 0x15, 0x57, 0xCD, 0x09, 0x15, 0xB3, 0x35, 0x3B, 0xBB, 0x64, 0xE0, 0xEC, 0x37, + 0x7F, 0xD0, 0x28, 0x37, 0x0D, 0xF9, 0x2B, 0x52, 0xC7, 0x89, 0x14, 0x28, 0xCD, 0xC6, 0x7E, 0xB6, 0x18, 0x4B, 0x52, 0x3D, + 0x1D, 0xB2, 0x46, 0xC3, 0x2F, 0x63, 0x07, 0x84, 0x90, 0xF0, 0x0E, 0xF8, 0xD6, 0x47, 0xD1, 0x48, 0xD4, 0x79, 0x54, 0x51, + 0x5E, 0x23, 0x27, 0xCF, 0xEF, 0x98, 0xC5, 0x82, 0x66, 0x4B, 0x4C, 0x0F, 0x6C, 0xC4, 0x16, 0x59}; + DH *dh; + BIGNUM *p; + BIGNUM *g; + + if ((dh = DH_new()) == nullptr) { + return nullptr; + } + p = BN_bin2bn(dh2048_p, sizeof(dh2048_p), nullptr); + g = BN_bin2bn(dh2048_g, sizeof(dh2048_g), nullptr); + if (p == nullptr || g == nullptr) { + DH_free(dh); + BN_free(p); + BN_free(g); + return nullptr; + } + DH_set0_pqg(dh, p, nullptr, g); + return (dh); +} +#endif // TS_USE_GET_DH_2048_256 + +bool +set_ctx_dh(SSL_CTX *ctx, dh_key_t *pkey) +{ + bool result{SSL_CTX_set_options(ctx, SSL_OP_SINGLE_DH_USE) && SSL_CTX_set_tmp_dh(ctx, pkey)}; + DH_free(pkey); + return result; +} + +#endif // OPENSSL_IS_OPENSSL3 + +bool +use_rsa_pkey_from_file(SSL_CTX *ctx, const char *keyPath) +{ + ink_assert(keyPath && keyPath[0] != '\0'); + int const result{SSL_CTX_use_RSAPrivateKey_file(ctx, keyPath, SSL_FILETYPE_PEM)}; + if (1 != result) { + char err_buf[256]{}; + ERR_error_string_n(ERR_get_error(), err_buf, sizeof(err_buf)); + Error("failed to load RSA key %s: %s", keyPath, err_buf); + } + return 1 == result; +} + +bool +use_rsa_pkey_from_secret_data(SSL_CTX *ctx, const char *secret_data, int secret_data_len) +{ + scoped_BIO bio(BIO_new_mem_buf(secret_data, secret_data_len)); + + pem_password_cb *password_cb = SSL_CTX_get_default_passwd_cb(ctx); + void *u = SSL_CTX_get_default_passwd_cb_userdata(ctx); + EVP_PKEY *pkey = PEM_read_bio_PrivateKey(bio.get(), nullptr, password_cb, u); + if (nullptr == pkey) { + return false; + } + if (!SSL_CTX_use_PrivateKey(ctx, pkey)) { + EVP_PKEY_free(pkey); + return false; + } + return true; +} diff --git a/src/iocore/net/SSLKeyUtils.h b/src/iocore/net/SSLKeyUtils.h new file mode 100644 index 00000000000..ffcd3446eb9 --- /dev/null +++ b/src/iocore/net/SSLKeyUtils.h @@ -0,0 +1,45 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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 + +#if OPENSSL_IS_OPENSSL3 +#include +#else +#include +#endif +#include + +#ifdef OPENSSL_IS_OPENSSL3 +using dh_key_t = EVP_PKEY; +#else +using dh_key_t = DH; +#endif + +// Both gen_dh_2048_256_pkey and load_dhparams_file return owning pointers. +dh_key_t *gen_dh_2048_256_pkey(); +dh_key_t *load_dhparams_file(char const *dhparams_file); + +// Takes ownership of pkey. +bool set_ctx_dh(SSL_CTX *ctx, dh_key_t *pkey); + +bool use_rsa_pkey_from_file(SSL_CTX *ctx, const char *keyPath); +bool use_rsa_pkey_from_secret_data(SSL_CTX *ctx, const char *secret_data, int secret_data_len); diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc index 7c7637affa1..840194f1bed 100644 --- a/src/iocore/net/SSLUtils.cc +++ b/src/iocore/net/SSLUtils.cc @@ -26,6 +26,7 @@ #include "P_SSLConfig.h" #include "P_SSLNetVConnection.h" #include "P_TLSKeyLogger.h" +#include "SSLKeyUtils.h" #include "SSLStats.h" #include "SSLSessionCache.h" #include "SSLSessionTicket.h" @@ -42,6 +43,7 @@ #include "tscore/ink_config.h" #include "tscore/SimpleTokenizer.h" #include "tscore/Layout.h" +#include "tscore/ink_assert.h" #include "tscore/ink_cap.h" #include "tscore/ink_mutex.h" #include "tscore/Filenames.h" @@ -52,8 +54,10 @@ #include "swoc/Errata.h" #include #include -#include #include +#ifdef OPENSSL_IS_OPENSSL3 +#include +#endif #include #include #if HAVE_ENGINE_LOAD_DYNAMIC @@ -383,59 +387,6 @@ ssl_alpn_select_callback(SSL *ssl, const unsigned char **out, unsigned char *out return SSL_TLSEXT_ERR_NOACK; } -#if TS_USE_GET_DH_2048_256 == 0 -/* Build 2048-bit MODP Group with 256-bit Prime Order Subgroup from RFC 5114 */ -static DH * -DH_get_2048_256() -{ - static const unsigned char dh2048_p[] = { - 0x87, 0xA8, 0xE6, 0x1D, 0xB4, 0xB6, 0x66, 0x3C, 0xFF, 0xBB, 0xD1, 0x9C, 0x65, 0x19, 0x59, 0x99, 0x8C, 0xEE, 0xF6, 0x08, - 0x66, 0x0D, 0xD0, 0xF2, 0x5D, 0x2C, 0xEE, 0xD4, 0x43, 0x5E, 0x3B, 0x00, 0xE0, 0x0D, 0xF8, 0xF1, 0xD6, 0x19, 0x57, 0xD4, - 0xFA, 0xF7, 0xDF, 0x45, 0x61, 0xB2, 0xAA, 0x30, 0x16, 0xC3, 0xD9, 0x11, 0x34, 0x09, 0x6F, 0xAA, 0x3B, 0xF4, 0x29, 0x6D, - 0x83, 0x0E, 0x9A, 0x7C, 0x20, 0x9E, 0x0C, 0x64, 0x97, 0x51, 0x7A, 0xBD, 0x5A, 0x8A, 0x9D, 0x30, 0x6B, 0xCF, 0x67, 0xED, - 0x91, 0xF9, 0xE6, 0x72, 0x5B, 0x47, 0x58, 0xC0, 0x22, 0xE0, 0xB1, 0xEF, 0x42, 0x75, 0xBF, 0x7B, 0x6C, 0x5B, 0xFC, 0x11, - 0xD4, 0x5F, 0x90, 0x88, 0xB9, 0x41, 0xF5, 0x4E, 0xB1, 0xE5, 0x9B, 0xB8, 0xBC, 0x39, 0xA0, 0xBF, 0x12, 0x30, 0x7F, 0x5C, - 0x4F, 0xDB, 0x70, 0xC5, 0x81, 0xB2, 0x3F, 0x76, 0xB6, 0x3A, 0xCA, 0xE1, 0xCA, 0xA6, 0xB7, 0x90, 0x2D, 0x52, 0x52, 0x67, - 0x35, 0x48, 0x8A, 0x0E, 0xF1, 0x3C, 0x6D, 0x9A, 0x51, 0xBF, 0xA4, 0xAB, 0x3A, 0xD8, 0x34, 0x77, 0x96, 0x52, 0x4D, 0x8E, - 0xF6, 0xA1, 0x67, 0xB5, 0xA4, 0x18, 0x25, 0xD9, 0x67, 0xE1, 0x44, 0xE5, 0x14, 0x05, 0x64, 0x25, 0x1C, 0xCA, 0xCB, 0x83, - 0xE6, 0xB4, 0x86, 0xF6, 0xB3, 0xCA, 0x3F, 0x79, 0x71, 0x50, 0x60, 0x26, 0xC0, 0xB8, 0x57, 0xF6, 0x89, 0x96, 0x28, 0x56, - 0xDE, 0xD4, 0x01, 0x0A, 0xBD, 0x0B, 0xE6, 0x21, 0xC3, 0xA3, 0x96, 0x0A, 0x54, 0xE7, 0x10, 0xC3, 0x75, 0xF2, 0x63, 0x75, - 0xD7, 0x01, 0x41, 0x03, 0xA4, 0xB5, 0x43, 0x30, 0xC1, 0x98, 0xAF, 0x12, 0x61, 0x16, 0xD2, 0x27, 0x6E, 0x11, 0x71, 0x5F, - 0x69, 0x38, 0x77, 0xFA, 0xD7, 0xEF, 0x09, 0xCA, 0xDB, 0x09, 0x4A, 0xE9, 0x1E, 0x1A, 0x15, 0x97}; - static const unsigned char dh2048_g[] = { - 0x3F, 0xB3, 0x2C, 0x9B, 0x73, 0x13, 0x4D, 0x0B, 0x2E, 0x77, 0x50, 0x66, 0x60, 0xED, 0xBD, 0x48, 0x4C, 0xA7, 0xB1, 0x8F, - 0x21, 0xEF, 0x20, 0x54, 0x07, 0xF4, 0x79, 0x3A, 0x1A, 0x0B, 0xA1, 0x25, 0x10, 0xDB, 0xC1, 0x50, 0x77, 0xBE, 0x46, 0x3F, - 0xFF, 0x4F, 0xED, 0x4A, 0xAC, 0x0B, 0xB5, 0x55, 0xBE, 0x3A, 0x6C, 0x1B, 0x0C, 0x6B, 0x47, 0xB1, 0xBC, 0x37, 0x73, 0xBF, - 0x7E, 0x8C, 0x6F, 0x62, 0x90, 0x12, 0x28, 0xF8, 0xC2, 0x8C, 0xBB, 0x18, 0xA5, 0x5A, 0xE3, 0x13, 0x41, 0x00, 0x0A, 0x65, - 0x01, 0x96, 0xF9, 0x31, 0xC7, 0x7A, 0x57, 0xF2, 0xDD, 0xF4, 0x63, 0xE5, 0xE9, 0xEC, 0x14, 0x4B, 0x77, 0x7D, 0xE6, 0x2A, - 0xAA, 0xB8, 0xA8, 0x62, 0x8A, 0xC3, 0x76, 0xD2, 0x82, 0xD6, 0xED, 0x38, 0x64, 0xE6, 0x79, 0x82, 0x42, 0x8E, 0xBC, 0x83, - 0x1D, 0x14, 0x34, 0x8F, 0x6F, 0x2F, 0x91, 0x93, 0xB5, 0x04, 0x5A, 0xF2, 0x76, 0x71, 0x64, 0xE1, 0xDF, 0xC9, 0x67, 0xC1, - 0xFB, 0x3F, 0x2E, 0x55, 0xA4, 0xBD, 0x1B, 0xFF, 0xE8, 0x3B, 0x9C, 0x80, 0xD0, 0x52, 0xB9, 0x85, 0xD1, 0x82, 0xEA, 0x0A, - 0xDB, 0x2A, 0x3B, 0x73, 0x13, 0xD3, 0xFE, 0x14, 0xC8, 0x48, 0x4B, 0x1E, 0x05, 0x25, 0x88, 0xB9, 0xB7, 0xD2, 0xBB, 0xD2, - 0xDF, 0x01, 0x61, 0x99, 0xEC, 0xD0, 0x6E, 0x15, 0x57, 0xCD, 0x09, 0x15, 0xB3, 0x35, 0x3B, 0xBB, 0x64, 0xE0, 0xEC, 0x37, - 0x7F, 0xD0, 0x28, 0x37, 0x0D, 0xF9, 0x2B, 0x52, 0xC7, 0x89, 0x14, 0x28, 0xCD, 0xC6, 0x7E, 0xB6, 0x18, 0x4B, 0x52, 0x3D, - 0x1D, 0xB2, 0x46, 0xC3, 0x2F, 0x63, 0x07, 0x84, 0x90, 0xF0, 0x0E, 0xF8, 0xD6, 0x47, 0xD1, 0x48, 0xD4, 0x79, 0x54, 0x51, - 0x5E, 0x23, 0x27, 0xCF, 0xEF, 0x98, 0xC5, 0x82, 0x66, 0x4B, 0x4C, 0x0F, 0x6C, 0xC4, 0x16, 0x59}; - DH *dh; - BIGNUM *p; - BIGNUM *g; - - if ((dh = DH_new()) == nullptr) { - return nullptr; - } - p = BN_bin2bn(dh2048_p, sizeof(dh2048_p), nullptr); - g = BN_bin2bn(dh2048_g, sizeof(dh2048_g), nullptr); - if (p == nullptr || g == nullptr) { - DH_free(dh); - BN_free(p); - BN_free(g); - return nullptr; - } - DH_set0_pqg(dh, p, nullptr, g); - return (dh); -} -#endif - bool SSLMultiCertConfigLoader::_enable_cert_compression(SSL_CTX *ctx) { @@ -490,28 +441,19 @@ SSLMultiCertConfigLoader::_enable_early_data([[maybe_unused]] SSL_CTX *ctx) static SSL_CTX * ssl_context_enable_dhe(const char *dhparams_file, SSL_CTX *ctx) { - DH *server_dh; + dh_key_t *pkey{}; if (dhparams_file) { - scoped_BIO bio(BIO_new_file(dhparams_file, "r")); - server_dh = PEM_read_bio_DHparams(bio.get(), nullptr, nullptr, nullptr); + pkey = load_dhparams_file(dhparams_file); } else { - server_dh = DH_get_2048_256(); - } - - if (!server_dh) { - Error("SSL dhparams source returned invalid parameters"); - return nullptr; + pkey = gen_dh_2048_256_pkey(); } - if (!SSL_CTX_set_options(ctx, SSL_OP_SINGLE_DH_USE) || !SSL_CTX_set_tmp_dh(ctx, server_dh)) { - DH_free(server_dh); + if (!pkey || !set_ctx_dh(ctx, pkey)) { Error("failed to configure SSL DH"); return nullptr; } - DH_free(server_dh); - return ctx; } @@ -914,43 +856,24 @@ SSLMultiCertConfigLoader::default_server_ssl_ctx() static bool SSLPrivateKeyHandler(SSL_CTX *ctx, const char *keyPath, const char *secret_data, int secret_data_len) { - EVP_PKEY *pkey = nullptr; -#if HAVE_ENGINE_GET_DEFAULT_RSA && HAVE_ENGINE_LOAD_PRIVATE_KEY - ENGINE *e = ENGINE_get_default_RSA(); - if (e != nullptr) { - pkey = ENGINE_load_private_key(e, keyPath, nullptr, nullptr); - if (pkey) { - if (!SSL_CTX_use_PrivateKey(ctx, pkey)) { - Dbg(dbg_ctl_ssl_load, "failed to load server private key from engine"); - EVP_PKEY_free(pkey); - return false; - } - } + bool result{false}; + if (keyPath && keyPath[0] != '\0') { + result = use_rsa_pkey_from_file(ctx, keyPath); } -#else - void *e = nullptr; -#endif - if (pkey == nullptr) { - scoped_BIO bio(BIO_new_mem_buf(secret_data, secret_data_len)); - - pem_password_cb *password_cb = SSL_CTX_get_default_passwd_cb(ctx); - void *u = SSL_CTX_get_default_passwd_cb_userdata(ctx); - pkey = PEM_read_bio_PrivateKey(bio.get(), nullptr, password_cb, u); - if (nullptr == pkey) { - Dbg(dbg_ctl_ssl_load, "failed to load server private key (%.*s) from %s", secret_data_len < 50 ? secret_data_len : 50, - secret_data, (!keyPath || keyPath[0] == '\0') ? "[empty key path]" : keyPath); - return false; - } - if (!SSL_CTX_use_PrivateKey(ctx, pkey)) { - Dbg(dbg_ctl_ssl_load, "failed to attach server private key loaded from %s", - (!keyPath || keyPath[0] == '\0') ? "[empty key path]" : keyPath); - EVP_PKEY_free(pkey); - return false; - } - if (e == nullptr && !SSL_CTX_check_private_key(ctx)) { - Dbg(dbg_ctl_ssl_load, "server private key does not match the certificate public key"); - return false; - } + + if (!result) { + result = use_rsa_pkey_from_secret_data(ctx, secret_data, secret_data_len); + } + + if (!result) { + Dbg(dbg_ctl_ssl_load, "failed to load server private key (%.*s) from %s", secret_data_len < 50 ? secret_data_len : 50, + secret_data, (!keyPath || keyPath[0] == '\0') ? "[empty key path]" : keyPath); + return false; + } + + if (!SSL_CTX_check_private_key(ctx)) { + Dbg(dbg_ctl_ssl_load, "server private key does not match the certificate public key"); + return false; } return true; diff --git a/src/iocore/net/unit_tests/test_SSLDHParams.cc b/src/iocore/net/unit_tests/test_SSLDHParams.cc new file mode 100644 index 00000000000..49a0ffb761e --- /dev/null +++ b/src/iocore/net/unit_tests/test_SSLDHParams.cc @@ -0,0 +1,339 @@ +/** @file + + Catch based unit tests for two pieces of inknet SSL_CTX setup, each + exercised through its public SSLMultiCertConfigLoader boundary: + + * The DH-parameter handling of init_server_ssl_ctx, which transitively + invokes ssl_context_enable_dhe and (when a file is configured) + load_dhparams_file. + + * The private key handling of load_certs, which transitively invokes the + file-static SSLPrivateKeyHandler. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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 "../P_SSLCertLookup.h" +#include "../P_SSLConfig.h" +#include "../P_SSLUtils.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace +{ + +std::string +bio_to_string(BIO *bio) +{ + BUF_MEM *bm = nullptr; + BIO_get_mem_ptr(bio, &bm); + return std::string{bm->data, bm->length}; +} + +std::string +make_valid_dh_pem() +{ + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_from_name(nullptr, "DH", nullptr); + REQUIRE(pctx != nullptr); + REQUIRE(EVP_PKEY_paramgen_init(pctx) > 0); + char prime_group[]{"dh_2048_256"}; + OSSL_PARAM const params[2] = { + OSSL_PARAM_construct_utf8_string("group", prime_group, 0), + OSSL_PARAM_construct_end(), + }; + REQUIRE(EVP_PKEY_CTX_set_params(pctx, params) > 0); + EVP_PKEY *pkey = nullptr; + REQUIRE(EVP_PKEY_generate(pctx, &pkey) > 0); + + BIO *bio = BIO_new(BIO_s_mem()); + REQUIRE(PEM_write_bio_Parameters(bio, pkey) == 1); + std::string const out{bio_to_string(bio)}; + BIO_free(bio); + EVP_PKEY_free(pkey); + EVP_PKEY_CTX_free(pctx); + return out; +} + +// PEM-encodes pkey as a private key, optionally encrypting it with the given +// cipher and passphrase (cipher==nullptr leaves it unencrypted). +std::string +key_to_pem(EVP_PKEY *pkey, EVP_CIPHER const *cipher, char *pass) +{ + BIO *bio = BIO_new(BIO_s_mem()); + int passlen{pass ? static_cast(std::strlen(pass)) : 0}; + REQUIRE(PEM_write_bio_PrivateKey(bio, pkey, cipher, reinterpret_cast(pass), passlen, nullptr, nullptr) == 1); + std::string out{bio_to_string(bio)}; + BIO_free(bio); + return out; +} + +std::string +make_rsa_pem() +{ + EVP_PKEY *pkey = EVP_RSA_gen(1024); + std::string const out{key_to_pem(pkey, nullptr, nullptr)}; + EVP_PKEY_free(pkey); + return out; +} + +// A self-signed certificate paired with the matching 2048-bit RSA private key, +// both PEM-encoded. Each call produces a fresh, independent key pair. When a +// cipher is given the key PEM is encrypted under the passphrase. +struct CertAndKey { + std::string cert_pem; + std::string key_pem; +}; + +CertAndKey +make_cert_and_key(EVP_CIPHER const *cipher = nullptr, char *pass = nullptr) +{ + EVP_PKEY *pkey = EVP_RSA_gen(2048); + REQUIRE(pkey != nullptr); + + X509 *x509 = X509_new(); + REQUIRE(x509 != nullptr); + ASN1_INTEGER_set(X509_get_serialNumber(x509), 1); + X509_gmtime_adj(X509_getm_notBefore(x509), 0); + X509_gmtime_adj(X509_getm_notAfter(x509), 60L * 60L * 24L * 365L); + REQUIRE(X509_set_pubkey(x509, pkey) == 1); + + X509_NAME *name = X509_get_subject_name(x509); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast("ats-test"), -1, -1, 0); + REQUIRE(X509_set_issuer_name(x509, name) == 1); + REQUIRE(X509_sign(x509, pkey, EVP_sha256()) > 0); + + BIO *cert_bio = BIO_new(BIO_s_mem()); + REQUIRE(PEM_write_bio_X509(cert_bio, x509) == 1); + std::string const cert_pem{bio_to_string(cert_bio)}; + BIO_free(cert_bio); + X509_free(x509); + + std::string const key_pem{key_to_pem(pkey, cipher, pass)}; + EVP_PKEY_free(pkey); + return {cert_pem, key_pem}; +} + +class TempFile +{ +public: + explicit TempFile(std::string const &contents) + { + char tmpl[] = "/tmp/ats_dhparams_XXXXXX"; + int fd = mkstemp(tmpl); + REQUIRE(fd != -1); + this->path = tmpl; + if (!contents.empty()) { + REQUIRE(write(fd, contents.data(), contents.size()) == static_cast(contents.size())); + } + close(fd); + } + TempFile(TempFile const &) = delete; + TempFile(TempFile &&) = delete; + TempFile &operator=(TempFile const &) = delete; + TempFile &operator=(TempFile &&) = delete; + ~TempFile() { unlink(this->path.c_str()); } + + char const * + get_path() const + { + return this->path.c_str(); + } + +private: + std::string path; +}; + +// Drives ssl_context_enable_dhe via init_server_ssl_ctx, holding every +// non-DHE input fixed and varying only dhparamsFile. An empty CertLoadData +// selects the "default generated ctx" branch which still traverses +// ssl_context_enable_dhe but skips cert/key loading entirely, so a non-empty +// returned vector with a non-null SSL_CTX is observable iff DHE configuration +// succeeded. +bool +init_with_dhparams(char const *dhparams_file) +{ + SSLConfigParams params; + params.dhparamsFile = dhparams_file ? ats_strdup(dhparams_file) : nullptr; + + SSLMultiCertConfigLoader loader{¶ms}; + SSLMultiCertConfigLoader::CertLoadData data; + auto contexts = loader.init_server_ssl_ctx(data, nullptr); + + bool ok = !contexts.empty() && contexts.front().ctx != nullptr; + for (auto const &lc : contexts) { + SSL_CTX_free(lc.ctx); + } + return ok; +} + +// A fixed-passphrase callback, matching how SSLPrivateKeyHandler consults the +// SSL_CTX default password callback to decrypt an encrypted private key. +char test_passphrase[]{"ats-secret-pass"}; + +int +fixed_passphrase_cb(char *buf, int size, int /* rwflag */, void * /* u */) +{ + int len{static_cast(std::strlen(test_passphrase))}; + if (len > size) { + len = size; + } + std::memcpy(buf, test_passphrase, len); + return len; +} + +// Drives SSLPrivateKeyHandler via the public static load_certs boundary, +// holding the certificate fixed and valid so the only variable under test is +// the private key material. The certificate and key are read from real files, +// exactly as a production ssl_multicert entry would be, so that the file-load +// path (load_rsa_pkey_from_file) is genuinely exercised. +// +// An empty key_path selects the "key bundled in the certificate file" branch, +// where the file load is skipped and the key is read from the certificate +// secret. A non-null passwd_cb is installed as the SSL_CTX default password +// callback, exactly as init_server_ssl_ctx's dialog setup would do for an +// encrypted key. +bool +load_key_via_load_certs(char const *cert_path, char const *key_path, pem_password_cb *passwd_cb = nullptr) +{ + SSLConfigParams params; + SSLMultiCertConfigParams settings; + settings.cert = ats_strdup(cert_path); + + SSLMultiCertConfigLoader::CertLoadData data; + data.cert_names_list.emplace_back(cert_path); + data.key_list.emplace_back(key_path); + + SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); + REQUIRE(ctx != nullptr); + if (passwd_cb != nullptr) { + SSL_CTX_set_default_passwd_cb(ctx, passwd_cb); + } + + bool ok = SSLMultiCertConfigLoader::load_certs(ctx, data.cert_names_list, data.key_list, data, ¶ms, &settings); + + SSL_CTX_free(ctx); + return ok; +} + +} // namespace + +TEST_CASE("ssl_context_enable_dhe: nullptr dhparams file falls back to built-in DH parameters") +{ + CHECK(init_with_dhparams(nullptr)); +} + +TEST_CASE("ssl_context_enable_dhe: valid ffdhe2048 DH PEM file is accepted") +{ + TempFile dh{make_valid_dh_pem()}; + CHECK(init_with_dhparams(dh.get_path())); +} + +TEST_CASE("ssl_context_enable_dhe: nonexistent dhparams path is rejected") +{ + CHECK_FALSE(init_with_dhparams("/tmp/ats_dhparams_does_not_exist_zzz_xyz")); +} + +TEST_CASE("ssl_context_enable_dhe: empty dhparams file is rejected") +{ + TempFile empty{""}; + CHECK_FALSE(init_with_dhparams(empty.get_path())); +} + +TEST_CASE("ssl_context_enable_dhe: non-PEM garbage in dhparams file is rejected") +{ + TempFile garbage{"this is definitely not a PEM-encoded DH parameter block\n"}; + CHECK_FALSE(init_with_dhparams(garbage.get_path())); +} + +TEST_CASE("ssl_context_enable_dhe: PEM of wrong key type (RSA) is rejected by DH-only decoder") +{ + TempFile rsa{make_rsa_pem()}; + CHECK_FALSE(init_with_dhparams(rsa.get_path())); +} + +TEST_CASE("ssl_context_enable_dhe: truncated DH PEM (missing END marker) is rejected") +{ + std::string pem = make_valid_dh_pem(); + auto end = pem.find("-----END"); + REQUIRE(end != std::string::npos); + TempFile truncated{pem.substr(0, end)}; + CHECK_FALSE(init_with_dhparams(truncated.get_path())); +} + +TEST_CASE("SSLPrivateKeyHandler: a key file matching the certificate is loaded") +{ + CertAndKey ck = make_cert_and_key(); + TempFile cert{ck.cert_pem}; + TempFile key{ck.key_pem}; + CHECK(load_key_via_load_certs(cert.get_path(), key.get_path())); +} + +TEST_CASE("SSLPrivateKeyHandler: an empty key path loads the key bundled in the certificate file") +{ + CertAndKey ck = make_cert_and_key(); + TempFile cert{ck.cert_pem + ck.key_pem}; + CHECK(load_key_via_load_certs(cert.get_path(), "")); +} + +TEST_CASE("SSLPrivateKeyHandler: a valid key file not matching the certificate is rejected") +{ + TempFile cert{make_cert_and_key().cert_pem}; + TempFile key{make_cert_and_key().key_pem}; + CHECK_FALSE(load_key_via_load_certs(cert.get_path(), key.get_path())); +} + +TEST_CASE("SSLPrivateKeyHandler: an unparseable key file is rejected") +{ + TempFile cert{make_cert_and_key().cert_pem}; + TempFile key{"-----BEGIN PRIVATE KEY-----\nnot base64\n-----END PRIVATE KEY-----\n"}; + CHECK_FALSE(load_key_via_load_certs(cert.get_path(), key.get_path())); +} + +TEST_CASE("SSLPrivateKeyHandler: an encrypted key file is decrypted via the SSL_CTX password callback") +{ + CertAndKey ck = make_cert_and_key(EVP_aes_256_cbc(), test_passphrase); + TempFile cert{ck.cert_pem}; + TempFile key{ck.key_pem}; + CHECK(load_key_via_load_certs(cert.get_path(), key.get_path(), fixed_passphrase_cb)); +} + +TEST_CASE("SSLPrivateKeyHandler: an encrypted key file with the wrong passphrase is rejected") +{ + char wrong_pass[]{"the-wrong-passphrase"}; + CertAndKey ck = make_cert_and_key(EVP_aes_256_cbc(), wrong_pass); + TempFile cert{ck.cert_pem}; + TempFile key{ck.key_pem}; + CHECK_FALSE(load_key_via_load_certs(cert.get_path(), key.get_path(), fixed_passphrase_cb)); +} diff --git a/src/iocore/net/unit_tests/unit_test_main.cc b/src/iocore/net/unit_tests/unit_test_main.cc index 25b355f0b64..41b96438d98 100644 --- a/src/iocore/net/unit_tests/unit_test_main.cc +++ b/src/iocore/net/unit_tests/unit_test_main.cc @@ -23,6 +23,7 @@ #include "iocore/eventsystem/EventSystem.h" #include "../P_SSLConfig.h" +#include "api/LifecycleAPIHooks.h" #include "records/RecordsConfig.h" #include "tscore/BaseLogFile.h" #include "tscore/Diags.h" @@ -55,6 +56,10 @@ class EventProcessorListener final : public Catch::EventListenerBase RecProcessInit(); LibRecordsConfigInit(); + // SSLSecret::loadSecret consults the global lifecycle hooks for the + // SSL_SECRET hook, so they must be allocated before any secret is loaded. + init_global_lifecycle_hooks(); + ink_event_system_init(EVENT_SYSTEM_MODULE_PUBLIC_VERSION); eventProcessor.start(test_threads);