| /** @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 "BoringSSLUtils.h" |
| #include "P_SSLUtils.h" |
| #include "P_Net.h" |
| #include "P_OCSPStapling.h" |
| #include "P_SSLConfig.h" |
| #include "P_SSLNetVConnection.h" |
| #include "P_TLSKeyLogger.h" |
| #include "SSLKeyUtils.h" |
| #include "SSLStats.h" |
| #include "SSLSessionCache.h" |
| #include "SSLSessionTicket.h" |
| #include "SSLDynlock.h" // IWYU pragma: keep - for ssl_dyn_* |
| #include "TLSCertCompression.h" |
| |
| #include "iocore/net/SSLMultiCertConfigLoader.h" |
| #include "config/ssl_multicert.h" |
| #include "iocore/net/SSLAPIHooks.h" |
| #include "iocore/net/SSLDiags.h" |
| #include "iocore/net/TLSSessionResumptionSupport.h" |
| #include "records/RecHttp.h" |
| #include "tscore/MatcherUtils.h" |
| #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" |
| #if TS_USE_QUIC == 1 |
| #include "iocore/net/QUICSupport.h" |
| #endif |
| #include "swoc/swoc_file.h" |
| #include "swoc/Errata.h" |
| #include <openssl/asn1.h> |
| #include <openssl/bio.h> |
| #include <openssl/conf.h> |
| #ifdef OPENSSL_IS_AT_LEAST_OPENSSL3 |
| #include <openssl/evp.h> |
| #endif |
| #include <openssl/dh.h> |
| #include <openssl/ec.h> |
| #include <openssl/err.h> |
| #include <openssl/evp.h> |
| #include <openssl/objects.h> |
| #include <openssl/pem.h> |
| #include <openssl/rand.h> |
| #include <openssl/x509.h> |
| #include <openssl/x509v3.h> |
| #if HAVE_OPENSSL_TS_H |
| #include <openssl/ts.h> |
| #endif |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <thread> |
| #include <utility> |
| #include <string> |
| #include <unistd.h> |
| #include <termios.h> |
| #include <vector> |
| |
| using namespace std::literals; |
| |
| static constexpr char SSL_CERT_SEPARATE_DELIM = ','; |
| |
| #ifndef evp_md_func |
| #ifdef OPENSSL_NO_SHA256 |
| #define evp_md_func EVP_sha1() |
| #else |
| #define evp_md_func EVP_sha256() |
| #endif |
| #endif |
| |
| static int ssl_vc_index = -1; |
| |
| static ink_mutex *mutex_buf = nullptr; |
| static bool open_ssl_initialized = false; |
| |
| static DbgCtl dbg_ctl_ssl{"ssl"}; |
| static DbgCtl dbg_ctl_ssl_load{"ssl_load"}; |
| static DbgCtl dbg_ctl_ssl_session_cache{"ssl.session_cache"}; |
| static DbgCtl dbg_ctl_ssl_error{"ssl.error"}; |
| static DbgCtl dbg_ctl_ssl_verify{"ssl_verify"}; |
| |
| #if TS_HAS_TLS_SESSION_TICKET |
| static bool ssl_context_enable_ticket_callback(SSL_CTX *ctx); |
| static bool ssl_apply_sni_session_ticket_properties(SSL *ssl); |
| static bool ssl_set_session_ticket_number(SSL *ssl, size_t num_tickets); |
| #endif |
| |
| /* Using pthread thread ID and mutex functions directly, instead of |
| * ATS this_ethread / ProxyMutex, so that other linked libraries |
| * may use pthreads and openssl without confusing us here. (TS-2271). |
| */ |
| |
| #if !defined(CRYPTO_THREADID_set_callback) |
| static void |
| SSL_pthreads_thread_id(CRYPTO_THREADID *id) |
| { |
| CRYPTO_THREADID_set_numeric(id, (unsigned long)pthread_self()); |
| } |
| #endif |
| |
| // The locking callback goes away with openssl 1.1 and CRYPTO_LOCK is on longer defined |
| #if defined(CRYPTO_LOCK) && !defined(CRYPTO_set_locking_callback) |
| static void |
| SSL_locking_callback(int mode, int type, const char *file, int line) |
| { |
| DbgCtl dbg_ctl{"v_ssl_lock"}; |
| Dbg(dbg_ctl, "file: %s line: %d type: %d", file, line, type); |
| ink_assert(type < CRYPTO_num_locks()); |
| |
| #ifdef OPENSSL_FIPS |
| // don't need to lock for FIPS if it has POSTed and we are not going to change the mode on the fly |
| if (type == CRYPTO_LOCK_FIPS || type == CRYPTO_LOCK_FIPS2) { |
| return; |
| } |
| #endif |
| |
| if (mode & CRYPTO_LOCK) { |
| ink_mutex_acquire(&mutex_buf[type]); |
| } else if (mode & CRYPTO_UNLOCK) { |
| ink_mutex_release(&mutex_buf[type]); |
| } else { |
| Dbg(dbg_ctl_ssl_load, "invalid SSL locking mode 0x%x", mode); |
| ink_assert(0); |
| } |
| } |
| #endif |
| |
| static bool |
| SSL_CTX_add_extra_chain_cert_bio(SSL_CTX *ctx, BIO *bio) |
| { |
| X509 *cert; |
| |
| for (;;) { |
| cert = PEM_read_bio_X509_AUX(bio, nullptr, nullptr, nullptr); |
| |
| if (!cert) { |
| // No more the certificates in this file. |
| break; |
| } |
| |
| // This transfers ownership of the cert (X509) to the SSL context, if successful. |
| #ifdef SSL_CTX_add0_chain_cert |
| if (!SSL_CTX_add0_chain_cert(ctx, cert)) { |
| #else |
| if (!SSL_CTX_add_extra_chain_cert(ctx, cert)) { |
| #endif |
| X509_free(cert); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| static bool |
| SSL_CTX_add_extra_chain_cert_file(SSL_CTX *ctx, const char *chainfile) |
| { |
| scoped_BIO bio(BIO_new_file(chainfile, "r")); |
| return SSL_CTX_add_extra_chain_cert_bio(ctx, bio.get()); |
| } |
| |
| // Callback function for verifying client certificate |
| static int |
| ssl_verify_client_callback(int preverify_ok, X509_STORE_CTX *ctx) |
| { |
| Dbg(dbg_ctl_ssl_verify, "Callback: verify client cert"); |
| auto *ssl = static_cast<SSL *>(X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); |
| SSLNetVConnection *netvc = SSLNetVCAccess(ssl); |
| TLSBasicSupport *tbs = TLSBasicSupport::getInstance(ssl); |
| |
| if (tbs == nullptr) { |
| Dbg(dbg_ctl_ssl_verify, "call back on stale netvc"); |
| return false; |
| } |
| |
| if (tbs->verify_certificate(ctx) == 1) { // hook moved the handshake state to terminal |
| Warning("TS_EVENT_SSL_VERIFY_CLIENT plugin failed the client certificate check for %s.", netvc->options.sni_servername.get()); |
| return false; |
| } |
| |
| return preverify_ok; |
| } |
| |
| #if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB |
| // Pausable callback |
| static int |
| ssl_client_hello_callback(SSL *s, int * /* al ATS_UNUSED */, void * /* arg ATS_UNUSED */) |
| { |
| TLSSNISupport::ClientHello ch = {s}; |
| #elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB |
| static ssl_select_cert_result_t |
| ssl_client_hello_callback(const SSL_CLIENT_HELLO *client_hello) |
| { |
| SSL *s = client_hello->ssl; |
| TLSSNISupport::ClientHello ch = {client_hello}; |
| #endif |
| |
| TLSSNISupport *snis = TLSSNISupport::getInstance(s); |
| if (snis) { |
| snis->on_client_hello(ch); |
| int ret = snis->perform_sni_action(*s); |
| if (ret != SSL_TLSEXT_ERR_OK) { |
| return CLIENT_HELLO_ERROR; |
| } |
| } else { |
| // This error suggests either of these: |
| // 1) Call back on unsupported netvc -- Don't register callback unnecessarily |
| // 2) Call back on stale netvc |
| Dbg(dbg_ctl_ssl_error, "ssl_client_hello_callback was called unexpectedly"); |
| return CLIENT_HELLO_ERROR; |
| } |
| |
| TLSEventSupport *es = TLSEventSupport::getInstance(s); |
| if (es) { |
| bool reenabled = es->callHooks(TS_EVENT_SSL_CLIENT_HELLO); |
| if (!reenabled) { |
| return CLIENT_HELLO_RETRY; |
| } |
| } else { |
| Dbg(dbg_ctl_ssl_error, "ssl_client_hello_callback call back on stale netvc"); |
| return CLIENT_HELLO_ERROR; |
| } |
| |
| return CLIENT_HELLO_SUCCESS; |
| } |
| |
| /** |
| * Called before either the server or the client certificate is used |
| * Return 1 on success, 0 on error, or -1 to pause, -2 to retry |
| */ |
| static int |
| ssl_cert_callback(SSL *ssl, [[maybe_unused]] void *arg) |
| { |
| TLSCertSwitchSupport *tcss = TLSCertSwitchSupport::getInstance(ssl); |
| TLSEventSupport *tes = TLSEventSupport::getInstance(ssl); |
| SSLNetVConnection *sslnetvc = dynamic_cast<SSLNetVConnection *>(tcss); |
| bool reenabled; |
| int retval = 1; |
| |
| // If we are in tunnel mode, don't select a cert. Pause! |
| if (sslnetvc) { |
| NetVConnection *netvc = reinterpret_cast<NetVConnection *>(sslnetvc); |
| if (HttpProxyPort::TRANSPORT_BLIND_TUNNEL == netvc->attributes) { |
| #ifdef OPENSSL_IS_BORINGSSL |
| return -2; // Retry |
| #else |
| return -1; // Pause |
| #endif |
| } |
| } |
| |
| SSLCertContextType ctxType = SSLCertContextType::GENERIC; |
| #ifndef HAVE_NATIVE_DUAL_CERT_SUPPORT |
| if (arg != nullptr) { |
| const SSL_CLIENT_HELLO *client_hello = (const SSL_CLIENT_HELLO *)arg; |
| const bool client_ecdsa_capable = BoringSSLUtils::isClientEcdsaCapable(client_hello); |
| ctxType = client_ecdsa_capable ? SSLCertContextType::EC : SSLCertContextType::RSA; |
| } |
| #endif |
| |
| if (tcss) { |
| if (tes) { |
| // Do the common certificate lookup only once. If we pause |
| // and restart processing, do not execute the common logic again |
| if (!tes->calledHooks(TS_EVENT_SSL_CERT)) { |
| retval = tcss->selectCertificate(ssl, ctxType); |
| if (retval != 1) { |
| return retval; |
| } |
| } |
| |
| // Call the plugin cert code |
| reenabled = tes->callHooks(TS_EVENT_SSL_CERT); |
| // If it did not re-enable, return the code to |
| // stop the accept processing |
| if (!reenabled) { |
| retval = -1; // Pause |
| } |
| } else { |
| if (tcss->selectCertificate(ssl, ctxType) == 1) { |
| retval = 1; |
| } else { |
| retval = 0; |
| } |
| } |
| } |
| |
| #if TS_HAS_TLS_SESSION_TICKET |
| if (retval == 1) { |
| // After replacing the SSL_CTX, make sure the overridden ca_cert_file is still set |
| if (sslnetvc) { |
| setClientCertCACerts(ssl, sslnetvc->get_ca_cert_file(), sslnetvc->get_ca_cert_dir()); |
| } |
| |
| if (!ssl_apply_sni_session_ticket_properties(ssl)) { |
| retval = 0; |
| } |
| } |
| #endif |
| |
| // Return 1 for success, 0 for error, or -1 to pause |
| return retval; |
| } |
| |
| /* |
| * Cannot stop this callback. Always reeneabled |
| */ |
| static int |
| ssl_servername_callback(SSL *ssl, int *al, void *arg) |
| { |
| TLSSNISupport *snis = TLSSNISupport::getInstance(ssl); |
| if (snis) { |
| if (TLSEventSupport *es = TLSEventSupport::getInstance(ssl); es) { |
| es->callHooks(TS_EVENT_SSL_SERVERNAME); |
| } |
| snis->on_servername(ssl, al, arg); |
| #if !TS_USE_HELLO_CB |
| // Only call the SNI actions here if not already performed in the HELLO_CB |
| int ret = snis->perform_sni_action(*ssl); |
| if (ret != SSL_TLSEXT_ERR_OK) { |
| return SSL_TLSEXT_ERR_ALERT_FATAL; |
| } |
| #endif |
| } else { |
| // This error suggests either of these: |
| // 1) Call back on unsupported netvc -- Don't register callback unnecessarily |
| // 2) Call back on stale netvc |
| Dbg(dbg_ctl_ssl_error, "ssl_servername_callback was called unexpectedly"); |
| return SSL_TLSEXT_ERR_ALERT_FATAL; |
| } |
| |
| return SSL_TLSEXT_ERR_OK; |
| } |
| |
| // NextProtocolNegotiation TLS extension callback. The NPN extension |
| // allows the client to select a preferred protocol, so all we have |
| // to do here is tell them what out protocol set is. |
| int |
| ssl_next_protos_advertised_callback(SSL *ssl, const unsigned char **out, unsigned *outlen, void *) |
| { |
| ALPNSupport *alpns = ALPNSupport::getInstance(ssl); |
| |
| ink_assert(alpns); |
| if (alpns) { |
| return alpns->advertise_next_protocol(out, outlen); |
| } |
| |
| return SSL_TLSEXT_ERR_NOACK; |
| } |
| |
| static bool |
| is_http2_prohibited_cipher(const SSL_CIPHER *cipher) |
| { |
| struct CipherRange { |
| uint16_t first; |
| uint16_t last; |
| }; |
| |
| // RFC 9113 Appendix A lists 276 prohibited TLS 1.2 cipher suites. The IANA |
| // identifiers for those suites form these 24 contiguous ranges. |
| static constexpr CipherRange prohibited_ranges[] = { |
| {0x0000, 0x001b}, |
| {0x001e, 0x0046}, |
| {0x0067, 0x006d}, |
| {0x0084, 0x009d}, |
| {0x00a0, 0x00a1}, |
| {0x00a4, 0x00a9}, |
| {0x00ac, 0x00c5}, |
| {0x00ff, 0x00ff}, |
| {0xc001, 0xc02a}, |
| {0xc02d, 0xc02e}, |
| {0xc031, 0xc051}, |
| {0xc054, 0xc055}, |
| {0xc058, 0xc05b}, |
| {0xc05e, 0xc05f}, |
| {0xc062, 0xc06b}, |
| {0xc06e, 0xc07b}, |
| {0xc07e, 0xc07f}, |
| {0xc082, 0xc085}, |
| {0xc088, 0xc089}, |
| {0xc08c, 0xc08f}, |
| {0xc092, 0xc09d}, |
| {0xc0a0, 0xc0a1}, |
| {0xc0a4, 0xc0a5}, |
| {0xc0a8, 0xc0a9}, |
| }; |
| |
| if (cipher == nullptr) { |
| return false; |
| } |
| |
| const uint16_t cipher_id = SSL_CIPHER_get_protocol_id(cipher); |
| |
| return std::any_of(std::begin(prohibited_ranges), std::end(prohibited_ranges), |
| [cipher_id](const CipherRange &range) { return cipher_id >= range.first && cipher_id <= range.last; }); |
| } |
| |
| int |
| ssl_alpn_select_callback(SSL *ssl, const unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned inlen, |
| void *) |
| { |
| ALPNSupport *alpns = ALPNSupport::getInstance(ssl); |
| |
| ink_assert(alpns); |
| if (alpns) { |
| if (const SSL_CIPHER *cipher = SSL_get_pending_cipher(ssl); is_http2_prohibited_cipher(cipher)) { |
| Dbg(dbg_ctl_ssl, "disabling HTTP/2 for prohibited cipher %s", SSL_CIPHER_get_name(cipher)); |
| alpns->disableProtocol(TS_ALPN_PROTOCOL_INDEX_HTTP_2_0); |
| } |
| return alpns->select_next_protocol(out, outlen, in, inlen); |
| } |
| |
| return SSL_TLSEXT_ERR_NOACK; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_enable_cert_compression(SSL_CTX *ctx) |
| { |
| std::vector<std::string> algs; |
| |
| if (this->_params->server_cert_compression_algorithms) { |
| SimpleTokenizer tok(this->_params->server_cert_compression_algorithms, ','); |
| for (const char *token = tok.getNext(); token; token = tok.getNext()) { |
| algs.emplace_back(token); |
| } |
| } |
| |
| if (register_certificate_compression_preference(ctx, algs, this->_params->server_cert_compression_cache) == 1) { |
| return true; |
| } else { |
| SSLError("Failed to enable certificate compression"); |
| return false; |
| } |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_enable_ktls([[maybe_unused]] SSL_CTX *ctx) |
| { |
| #ifdef SSL_OP_ENABLE_KTLS |
| if (SSLConfigParams::ssl_ktls_enabled) { |
| if (SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS)) { |
| static DbgCtl dbg_ctl{"ssl.ktls"}; |
| Dbg(dbg_ctl, "KTLS is enabled"); |
| } else { |
| return false; |
| } |
| } |
| #endif |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_enable_early_data([[maybe_unused]] SSL_CTX *ctx) |
| { |
| #if TS_HAS_TLS_EARLY_DATA |
| if (SSLConfigParams::server_max_early_data > 0) { |
| #if HAVE_SSL_IN_EARLY_DATA |
| // If SSL_in_early_data is available, it's probably BoringSSL |
| // and SSL_set_early_data_enabled should be available. |
| SSL_CTX_set_early_data_enabled(ctx, 1); |
| #endif |
| } |
| #endif |
| return true; |
| } |
| |
| static SSL_CTX * |
| ssl_context_enable_dhe(const char *dhparams_file, SSL_CTX *ctx) |
| { |
| dh_key_t *pkey{}; |
| |
| if (dhparams_file) { |
| pkey = load_dhparams_file(dhparams_file); |
| } else { |
| pkey = gen_dh_2048_256_pkey(); |
| } |
| |
| if (!pkey || !set_ctx_dh(ctx, pkey)) { |
| Error("failed to configure SSL DH"); |
| return nullptr; |
| } |
| |
| return ctx; |
| } |
| |
| #if TS_HAS_TLS_SESSION_TICKET |
| static bool |
| ssl_context_enable_ticket_callback(SSL_CTX *ctx) |
| { |
| #ifdef HAVE_SSL_CTX_SET_TLSEXT_TICKET_KEY_EVP_CB |
| if (SSL_CTX_set_tlsext_ticket_key_evp_cb(ctx, ssl_callback_session_ticket) == 0) { |
| #else |
| if (SSL_CTX_set_tlsext_ticket_key_cb(ctx, ssl_callback_session_ticket) == 0) { |
| #endif |
| Error("failed to set session ticket callback"); |
| return false; |
| } |
| return true; |
| } |
| |
| static bool |
| ssl_set_session_ticket_number(SSL *ssl, size_t num_tickets) |
| { |
| #if defined(OPENSSL_IS_BORINGSSL) |
| // BoringSSL only exposes SSL_CTX_set_num_tickets(), so the per-connection |
| // sni.yaml override is not available here. |
| (void)ssl; |
| (void)num_tickets; |
| return true; |
| #else |
| return SSL_set_num_tickets(ssl, num_tickets) == 1; |
| #endif |
| } |
| |
| static bool |
| ssl_apply_sni_session_ticket_properties(SSL *ssl) |
| { |
| auto snis = TLSSNISupport::getInstance(ssl); |
| if (snis == nullptr) { |
| return true; |
| } |
| |
| auto const &hints = snis->hints_from_sni; |
| if (!hints.ssl_ticket_enabled.has_value() && !hints.ssl_ticket_number.has_value()) { |
| return true; |
| } |
| |
| std::optional<size_t> num_tickets; |
| |
| if (hints.ssl_ticket_enabled.has_value()) { |
| if (hints.ssl_ticket_enabled.value() != 0) { |
| SSL_clear_options(ssl, SSL_OP_NO_TICKET); |
| Dbg(dbg_ctl_ssl_load, "Enabled session tickets due to sni.yaml override"); |
| } else { |
| SSL_set_options(ssl, SSL_OP_NO_TICKET); |
| num_tickets = 0; |
| Dbg(dbg_ctl_ssl_load, "Disabled session tickets due to sni.yaml override"); |
| } |
| } |
| |
| if ((!hints.ssl_ticket_enabled.has_value() || hints.ssl_ticket_enabled.value() != 0) && hints.ssl_ticket_number.has_value()) { |
| num_tickets = hints.ssl_ticket_number.value() > 0 ? static_cast<size_t>(hints.ssl_ticket_number.value()) : 0; |
| } |
| |
| if (num_tickets.has_value()) { |
| if (!ssl_set_session_ticket_number(ssl, num_tickets.value())) { |
| Error("failed to set session ticket number from sni.yaml"); |
| return false; |
| } |
| Dbg(dbg_ctl_ssl_load, "Set session ticket number from sni.yaml to %zu", num_tickets.value()); |
| } |
| |
| return true; |
| } |
| #endif |
| |
| static bool |
| ssl_context_enable_tickets(SSL_CTX *ctx) |
| { |
| #if TS_HAS_TLS_SESSION_TICKET |
| // Setting the callback can only fail if OpenSSL does not recognize the |
| // SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB constant. we set the callback first |
| // so that we don't leave a ticket-key callback attached if it fails. |
| if (!ssl_context_enable_ticket_callback(ctx)) { |
| return false; |
| } |
| SSL_CTX_clear_options(ctx, SSL_OP_NO_TICKET); |
| return true; |
| #else /* !TS_HAS_TLS_SESSION_TICKET */ |
| (void)ctx; |
| return false; |
| #endif /* TS_HAS_TLS_SESSION_TICKET */ |
| } |
| |
| // RAII implementation for struct termios |
| struct ssl_termios : public termios { |
| ssl_termios(int fd) |
| { |
| _fd = -1; |
| // populate base class data |
| if (tcgetattr(fd, this) == 0) { // success |
| _fd = fd; |
| } |
| // save our copy |
| _initialAttr = *this; |
| } |
| |
| ~ssl_termios() |
| { |
| if (_fd != -1) { |
| tcsetattr(_fd, 0, &_initialAttr); |
| } |
| } |
| |
| bool |
| ok() const |
| { |
| return (_fd != -1); |
| } |
| |
| private: |
| int _fd; |
| struct termios _initialAttr; |
| }; |
| |
| static int |
| ssl_getpassword(const char *prompt, char *buffer, int size) |
| { |
| fprintf(stdout, "%s", prompt); |
| |
| // disable echo and line buffering |
| ssl_termios tty_attr(STDIN_FILENO); |
| |
| if (!tty_attr.ok()) { |
| return -1; |
| } |
| |
| tty_attr.c_lflag &= ~ICANON; // no buffer, no backspace |
| tty_attr.c_lflag &= ~ECHO; // no echo |
| tty_attr.c_lflag &= ~ISIG; // no signal for ctrl-c |
| |
| if (tcsetattr(STDIN_FILENO, 0, &tty_attr) < 0) { |
| return -1; |
| } |
| |
| int i = 0; |
| int ch = 0; |
| |
| *buffer = 0; |
| while ((ch = getchar()) != '\n' && ch != EOF) { |
| // make sure room in buffer |
| if (i >= size - 1) { |
| return -1; |
| } |
| |
| buffer[i] = ch; |
| buffer[++i] = 0; |
| } |
| |
| return i; |
| } |
| |
| static int |
| ssl_private_key_passphrase_callback_exec(char *buf, int size, int rwflag, void *userdata) |
| { |
| if (0 == size) { |
| return 0; |
| } |
| |
| *buf = 0; |
| const SSLMultiCertConfigParams *sslMultCertSettings = static_cast<SSLMultiCertConfigParams *>(userdata); |
| |
| Dbg(dbg_ctl_ssl_load, "ssl_private_key_passphrase_callback_exec rwflag=%d dialog=%s", rwflag, sslMultCertSettings->dialog.get()); |
| |
| // only respond to reading private keys, not writing them (does ats even do that?) |
| if (0 == rwflag) { |
| // execute the dialog program and use the first line output as the passphrase |
| ink_assert(strncmp(sslMultCertSettings->dialog, "exec:", 5) == 0); |
| const char *serverDialog = &sslMultCertSettings->dialog[5]; |
| FILE *f = popen(serverDialog, "r"); |
| if (f) { |
| if (fgets(buf, size, f)) { |
| // remove any ending CR or LF |
| for (char *pass = buf; *pass; pass++) { |
| if (*pass == '\n' || *pass == '\r') { |
| *pass = 0; |
| break; |
| } |
| } |
| } |
| pclose(f); |
| } else { // popen failed |
| Error("could not open dialog '%s' - %s", serverDialog, strerror(errno)); |
| } |
| } |
| return strlen(buf); |
| } |
| |
| static int |
| ssl_private_key_passphrase_callback_builtin(char *buf, int size, int rwflag, void *userdata) |
| { |
| if (0 == size) { |
| return 0; |
| } |
| |
| *buf = 0; |
| const SSLMultiCertConfigParams *sslMultCertSettings = static_cast<SSLMultiCertConfigParams *>(userdata); |
| |
| Dbg(dbg_ctl_ssl_load, "ssl_private_key_passphrase_callback rwflag=%d dialog=%s", rwflag, sslMultCertSettings->dialog.get()); |
| |
| // only respond to reading private keys, not writing them (does ats even do that?) |
| if (0 == rwflag) { |
| // output request |
| fprintf(stdout, "Some of your private key files are encrypted for security reasons.\n"); |
| fprintf(stdout, "In order to read them you have to provide the pass phrases.\n"); |
| fprintf(stdout, "ssl_cert_name=%s", sslMultCertSettings->cert.get()); |
| if (sslMultCertSettings->key.get()) { // output ssl_key_name if provided |
| fprintf(stdout, " ssl_key_name=%s", sslMultCertSettings->key.get()); |
| } |
| fprintf(stdout, "\n"); |
| // get passphrase |
| // if error, then no passphrase |
| if (ssl_getpassword("Enter passphrase:", buf, size) <= 0) { |
| *buf = 0; |
| } |
| fprintf(stdout, "\n"); |
| } |
| return strlen(buf); |
| } |
| |
| static bool |
| ssl_private_key_validate_exec(const char *cmdLine) |
| { |
| if (nullptr == cmdLine) { |
| errno = EINVAL; |
| return false; |
| } |
| |
| bool bReturn = false; |
| char *cmdLineCopy = ats_strdup(cmdLine); |
| char *ptr = cmdLineCopy; |
| |
| while (*ptr && !isspace(*ptr)) { |
| ++ptr; |
| } |
| *ptr = 0; |
| if (access(cmdLineCopy, X_OK) != -1) { |
| bReturn = true; |
| } |
| ats_free(cmdLineCopy); |
| return bReturn; |
| } |
| |
| #if defined(LIBRESSL_VERSION_NUMBER) |
| #define ssl_malloc(size, file, line) ssl_malloc(size) |
| #define ssl_realloc(ptr, size, file, line) ssl_realloc(ptr, size) |
| #define ssl_free(ptr, file, line) ssl_free(ptr) |
| #define ssl_track_malloc(size, file, line) ssl_track_malloc(size) |
| #define ssl_track_realloc(ptr, size, file, line) ssl_track_realloc(ptr, size) |
| #define ssl_track_free(ptr, file, line) ssl_track_free(ptr) |
| #endif |
| |
| void * |
| ssl_malloc(size_t size, const char * /*filename */, int /*lineno*/) |
| { |
| return ats_malloc(size); |
| } |
| |
| void * |
| ssl_realloc(void *ptr, size_t size, const char * /*filename*/, int /*lineno*/) |
| { |
| return ats_realloc(ptr, size); |
| } |
| |
| void |
| ssl_free(void *ptr, const char * /*filename*/, int /*lineno*/) |
| { |
| ats_free(ptr); |
| } |
| |
| void * |
| ssl_track_malloc(size_t size, const char * /*filename*/, int /*lineno*/) |
| { |
| return ats_track_malloc(size, &ssl_memory_allocated); |
| } |
| |
| void * |
| ssl_track_realloc(void *ptr, size_t size, const char * /*filename*/, int /*lineno*/) |
| { |
| return ats_track_realloc(ptr, size, &ssl_memory_allocated, &ssl_memory_freed); |
| } |
| |
| void |
| ssl_track_free(void *ptr, const char * /*filename*/, int /*lineno*/) |
| { |
| ats_track_free(ptr, &ssl_memory_freed); |
| } |
| |
| /* |
| * Some items are only initialized if certain config values are set |
| * Must have a second pass that initializes after loading the SSL config |
| */ |
| void |
| SSLPostConfigInitialize() |
| { |
| if (SSLConfigParams::engine_conf_file) { |
| OPENSSL_load_builtin_modules(); |
| if (CONF_modules_load_file(SSLConfigParams::engine_conf_file, nullptr, 0) <= 0) { |
| char err_buf[256] = {0}; |
| ERR_error_string_n(ERR_get_error(), err_buf, sizeof(err_buf)); |
| Error("Could not load SSL engine configuration file %s: %s", SSLConfigParams::engine_conf_file, err_buf); |
| } |
| } |
| } |
| |
| void |
| SSLInitializeLibrary() |
| { |
| if (!open_ssl_initialized) { |
| // BoringSSL does not have the memory functions |
| #ifdef HAVE_CRYPTO_SET_MEM_FUNCTIONS |
| if (res_track_memory >= 2) { |
| CRYPTO_set_mem_functions(ssl_track_malloc, ssl_track_realloc, ssl_track_free); |
| } else { |
| CRYPTO_set_mem_functions(ssl_malloc, ssl_realloc, ssl_free); |
| } |
| #endif |
| |
| SSL_load_error_strings(); |
| SSL_library_init(); |
| |
| #ifdef OPENSSL_FIPS |
| // calling FIPS_mode_set() will force FIPS to POST (Power On Self Test) |
| // After POST we don't have to lock for FIPS |
| int mode = FIPS_mode(); |
| FIPS_mode_set(mode); |
| Dbg(dbg_ctl_ssl_load, "FIPS_mode: %d", mode); |
| #endif |
| |
| mutex_buf = static_cast<ink_mutex *>(OPENSSL_malloc(CRYPTO_num_locks() * sizeof(ink_mutex))); |
| |
| for (int i = 0; i < CRYPTO_num_locks(); i++) { |
| ink_mutex_init(&mutex_buf[i]); |
| } |
| |
| CRYPTO_set_locking_callback(SSL_locking_callback); |
| #if !defined(CRYPTO_THREADID_set_callback) |
| CRYPTO_THREADID_set_callback(SSL_pthreads_thread_id); |
| #endif |
| CRYPTO_set_dynlock_create_callback(ssl_dyn_create_callback); |
| CRYPTO_set_dynlock_lock_callback(ssl_dyn_lock_callback); |
| CRYPTO_set_dynlock_destroy_callback(ssl_dyn_destroy_callback); |
| } |
| |
| ssl_stapling_ex_init(); |
| |
| #if HAVE_SSL_CTX_ADD_CERT_COMPRESSION_ALG |
| cert_compress_cache_init(); |
| #endif |
| |
| // Reserve an application data index so that we can attach |
| // the SSLNetVConnection to the SSL session. |
| ssl_vc_index = SSL_get_ex_new_index(0, (void *)"NetVC index", nullptr, nullptr, nullptr); |
| |
| TLSBasicSupport::initialize(); |
| TLSEventSupport::initialize(); |
| ALPNSupport::initialize(); |
| TLSSessionResumptionSupport::initialize(); |
| TLSSNISupport::initialize(); |
| TLSEarlyDataSupport::initialize(); |
| TLSTunnelSupport::initialize(); |
| TLSCertSwitchSupport::initialize(); |
| #if TS_USE_QUIC == 1 |
| QUICSupport::initialize(); |
| #endif |
| |
| open_ssl_initialized = true; |
| } |
| |
| SSL_CTX * |
| SSLMultiCertConfigLoader::default_server_ssl_ctx() |
| { |
| return SSL_CTX_new(SSLv23_server_method()); |
| } |
| |
| static bool |
| SSLPrivateKeyHandler(SSL_CTX *ctx, const char *keyPath, const char *secret_data, int secret_data_len) |
| { |
| // SSL_CTX_use_PrivateKey() takes its own reference on the key, so this |
| // reference must be released on every exit. |
| 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); |
| |
| scoped_PKEY const 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.get())) { |
| Dbg(dbg_ctl_ssl_load, "failed to attach server private key loaded from %s", |
| (!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; |
| } |
| |
| /** |
| returns 0 on OK or negative value on failure and update log as appropriate. |
| |
| Will check: |
| - if file exists, and has read permissions |
| - for truncation or other PEM read fail |
| - current time is between notBefore and notAfter dates of certificate |
| if anything is not kosher, a negative value is returned and appropriate error logged. |
| |
| @static |
| */ |
| int |
| SSLMultiCertConfigLoader::check_server_cert_now(X509 *cert, const char *certname) |
| { |
| int timeCmpValue; |
| |
| if (!cert) { |
| // a truncated certificate would fall into here |
| Error("invalid certificate %s: file is truncated or corrupted", certname); |
| return -3; |
| } |
| |
| // XXX we should log the notBefore and notAfter dates in the errors ... |
| |
| timeCmpValue = X509_cmp_current_time(X509_get_notBefore(cert)); |
| if (timeCmpValue == 0) { |
| // an error occurred parsing the time, which we'll call a bogosity |
| Error("invalid certificate %s: unable to parse notBefore time", certname); |
| return -3; |
| } else if (timeCmpValue > 0) { |
| // cert contains a date before the notBefore |
| Error("invalid certificate %s: notBefore date is in the future", certname); |
| return -4; |
| } |
| |
| timeCmpValue = X509_cmp_current_time(X509_get_notAfter(cert)); |
| if (timeCmpValue == 0) { |
| // an error occurred parsing the time, which we'll call a bogosity |
| Error("invalid certificate %s: unable to parse notAfter time", certname); |
| return -3; |
| } else if (timeCmpValue < 0) { |
| // cert is expired |
| Error("invalid certificate %s: certificate expired", certname); |
| return -5; |
| } |
| |
| Dbg(dbg_ctl_ssl_load, "server certificate %s passed accessibility and date checks", certname); |
| return 0; // all good |
| |
| } /* CheckServerCertNow() */ |
| |
| static char * |
| asn1_strdup(const ASN1_STRING *s) |
| { |
| // Make sure we have an 8-bit encoding. |
| ink_assert(ASN1_STRING_type(s) == V_ASN1_IA5STRING || ASN1_STRING_type(s) == V_ASN1_UTF8STRING || |
| ASN1_STRING_type(s) == V_ASN1_PRINTABLESTRING || ASN1_STRING_type(s) == V_ASN1_T61STRING); |
| |
| return ats_strndup(reinterpret_cast<const char *>(ASN1_STRING_get0_data(s)), ASN1_STRING_length(s)); |
| } |
| |
| // This callback function is executed while OpenSSL processes the SSL |
| // handshake and does SSL record layer stuff. It's used to trap |
| // client-initiated renegotiations and update cipher stats |
| static void |
| ssl_callback_info(const SSL *ssl, int where, int ret) |
| { |
| Dbg(dbg_ctl_ssl_load, "ssl_callback_info ssl: %p, where: %d, ret: %d, State: %s", ssl, where, ret, SSL_state_string_long(ssl)); |
| |
| SSLNetVConnection *netvc = SSLNetVCAccess(ssl); |
| |
| if (!netvc || netvc->ssl != ssl) { |
| Dbg(dbg_ctl_ssl_error, "ssl_callback_info call back on stale netvc"); |
| return; |
| } |
| |
| if ((where & SSL_CB_ACCEPT_LOOP) && netvc->getSSLHandShakeComplete() == true && |
| SSLConfigParams::ssl_allow_client_renegotiation == false) { |
| int state = SSL_get_state(ssl); |
| |
| // TODO: ifdef can be removed in the future |
| // Support for SSL23 only if we have it |
| #ifdef SSL23_ST_SR_CLNT_HELLO_A |
| if (state == SSL3_ST_SR_CLNT_HELLO_A || state == SSL23_ST_SR_CLNT_HELLO_A) { |
| #else |
| #ifdef SSL3_ST_SR_CLNT_HELLO_A |
| if (state == SSL3_ST_SR_CLNT_HELLO_A) { |
| #else |
| #ifdef SSL_ST_RENEGOTIATE |
| // This is for BoringSSL |
| if (state == SSL_ST_RENEGOTIATE) { |
| #else |
| if (state == TLS_ST_SR_CLNT_HELLO) { |
| #endif |
| #endif |
| #endif |
| #ifdef TLS1_3_VERSION |
| // TLSv1.3 has no renegotiation. |
| if (SSL_version(ssl) >= TLS1_3_VERSION) { |
| Dbg(dbg_ctl_ssl_load, "TLSv1.3 has no renegotiation."); |
| return; |
| } |
| #endif |
| netvc->setSSLClientRenegotiationAbort(true); |
| Dbg(dbg_ctl_ssl_load, "ssl_callback_info trying to renegotiate from the client"); |
| } |
| } |
| if (where & SSL_CB_HANDSHAKE_DONE) { |
| // handshake is complete |
| const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl); |
| if (cipher) { |
| const char *cipherName = SSL_CIPHER_get_name(cipher); |
| // lookup index of stat by name and incr count |
| auto it = cipher_map.find(cipherName); |
| if (it == cipher_map.end()) { |
| it = cipher_map.find(SSL_CIPHER_STAT_OTHER); |
| ink_assert(it != cipher_map.end()); |
| } |
| Metrics::Counter::increment(it->second); |
| } |
| |
| if (netvc && netvc->get_context() == NET_VCONNECTION_IN) { |
| uint64_t bytes_in = 0, bytes_out = 0; |
| auto tbs = TLSBasicSupport::getInstance(const_cast<SSL *>(ssl)); |
| |
| if (tbs && tbs->get_tls_handshake_bytes(bytes_in, bytes_out)) { |
| Metrics::Counter::increment(ssl_rsb.tls_handshake_bytes_in_total, bytes_in); |
| Metrics::Counter::increment(ssl_rsb.tls_handshake_bytes_out_total, bytes_out); |
| } |
| } |
| } |
| } |
| |
| void |
| SSLMultiCertConfigLoader::_set_handshake_callbacks(SSL_CTX *ctx) |
| { |
| // Make sure the callbacks are set |
| #if !HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB |
| SSL_CTX_set_cert_cb(ctx, ssl_cert_callback, nullptr); |
| SSL_CTX_set_tlsext_servername_callback(ctx, ssl_servername_callback); |
| #endif |
| |
| #if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB |
| SSL_CTX_set_client_hello_cb(ctx, ssl_client_hello_callback, nullptr); |
| #elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB |
| SSL_CTX_set_select_certificate_cb(ctx, [](const SSL_CLIENT_HELLO *client_hello) -> ssl_select_cert_result_t { |
| ssl_select_cert_result_t res; |
| TLSEventSupport *es = TLSEventSupport::getInstance(client_hello->ssl); |
| |
| // BoringSSL runs this whole callback again once a paused hook reenables, whereas OpenSSL calls |
| // only the callback that paused. After a pause in the cert hook, the client hello and |
| // servername work is already done and must not run twice. |
| if (es == nullptr || !es->reached_cert_hooks()) { |
| res = ssl_client_hello_callback(client_hello); |
| if (res != ssl_select_cert_success) { |
| return res; |
| } |
| |
| res = (ssl_servername_callback(client_hello->ssl, nullptr, nullptr) == SSL_TLSEXT_ERR_OK) ? ssl_select_cert_success : |
| ssl_select_cert_error; |
| if (res == ssl_select_cert_error) { |
| return res; |
| } |
| } |
| |
| int cbres = ssl_cert_callback(client_hello->ssl, (void *)client_hello); |
| switch (cbres) { |
| case -2: |
| case -1: |
| res = ssl_select_cert_retry; |
| break; |
| case 0: |
| res = ssl_select_cert_error; |
| break; |
| case 1: |
| res = ssl_select_cert_success; |
| break; |
| default: |
| ink_assert(!"unhandled cert result"); |
| } |
| |
| return res; |
| }); |
| #endif |
| } |
| |
| void |
| setClientCertLevel(SSL *ssl, uint8_t certLevel) |
| { |
| SSLConfig::scoped_config params; |
| int server_verify_client = SSL_VERIFY_NONE; |
| |
| if (certLevel == 2) { |
| server_verify_client = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT | SSL_VERIFY_CLIENT_ONCE; |
| } else if (certLevel == 1) { |
| server_verify_client = SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE; |
| } else if (certLevel == 0) { |
| server_verify_client = SSL_VERIFY_NONE; |
| } else { |
| ink_release_assert(!"Invalid client verify level"); |
| } |
| |
| Dbg(dbg_ctl_ssl_load, "setting cert level to %d", server_verify_client); |
| SSL_set_verify(ssl, server_verify_client, ssl_verify_client_callback); |
| SSL_set_verify_depth(ssl, params->verify_depth); // might want to make configurable at some point. |
| } |
| |
| void |
| setClientCertCACerts(SSL *ssl, const char *file, const char *dir) |
| { |
| if ((file != nullptr && file[0] != '\0') || (dir != nullptr && dir[0] != '\0')) { |
| #if TS_HAS_VERIFY_CERT_STORE |
| // The set0 version will take ownership of the X509_STORE object |
| X509_STORE *ctx = X509_STORE_new(); |
| if (ctx != nullptr && |
| X509_STORE_load_locations(ctx, file && file[0] != '\0' ? file : nullptr, dir && dir[0] != '\0' ? dir : nullptr)) { |
| SSL_set0_verify_cert_store(ssl, ctx); |
| } else { |
| X509_STORE_free(ctx); |
| } |
| |
| // SSL_set_client_CA_list takes ownership of the STACK_OF(X509) structure |
| // So we load it each time to pass into the SSL object |
| if (file != nullptr && file[0] != '\0') { |
| SSL_set_client_CA_list(ssl, SSL_load_client_CA_file(file)); |
| } |
| #else |
| ink_assert(!"Configuration checking should prevent reaching this code"); |
| #endif |
| } |
| } |
| |
| /** |
| Initialize SSL_CTX for server |
| This is public function because of used by SSLCreateServerContext. |
| */ |
| std::vector<SSLLoadingContext> |
| SSLMultiCertConfigLoader::init_server_ssl_ctx(CertLoadData const &data, const SSLMultiCertConfigParams *sslMultCertSettings) |
| { |
| std::vector<std::vector<std::string>> cert_names; |
| std::vector<std::vector<std::string>> key_names; |
| std::vector<std::string> key_names_list; |
| |
| bool generate_default_ctx = data.cert_names_list.empty(); |
| if (!generate_default_ctx) { |
| #ifndef HAVE_NATIVE_DUAL_CERT_SUPPORT |
| for (auto const &name : data.cert_names_list) { |
| cert_names.emplace_back(std::vector({name})); |
| } |
| for (auto const &name : data.key_list) { |
| key_names.emplace_back(std::vector({name})); |
| } |
| #else |
| if (!data.cert_names_list.empty()) { |
| cert_names.emplace_back(data.cert_names_list); |
| key_names.emplace_back(data.key_list); |
| } |
| #endif |
| } else { |
| // In the case of no cert_names, we still want to create a |
| // ctx with all the bells and whistles (as much as possible) |
| cert_names.emplace_back(std::vector({std::string("default")})); |
| key_names.emplace_back(std::vector({std::string("default")})); |
| } |
| |
| SSLCertContextType ctx_type = SSLCertContextType::GENERIC; |
| std::vector<SSLLoadingContext> ret; |
| unsigned int i = 0; |
| SSL_CTX *ctx = nullptr; |
| for (auto const &cert_names_list : cert_names) { |
| if (i < key_names.size()) { |
| key_names_list = key_names[i]; |
| } else { |
| key_names_list.clear(); |
| } |
| |
| ctx = this->default_server_ssl_ctx(); |
| |
| ctx_type = (!generate_default_ctx && i < data.cert_type_list.size()) ? data.cert_type_list[i] : SSLCertContextType::GENERIC; |
| |
| Dbg(dbg_ctl_ssl_load, "Creating new context %p cert_count=%ld initial: %s", ctx, cert_names_list.size(), |
| cert_names_list[0].c_str()); |
| |
| SSL_CTX_set_min_proto_version(ctx, TLS1_VERSION); |
| SSL_CTX_set_options(ctx, _params->ssl_ctx_options); |
| |
| if (_params->server_tls_ver_min >= 0 || _params->server_tls_ver_max >= 0) { |
| int ver = 0; |
| if (_params->server_tls_ver_min >= 0) { |
| ver = TLS1_VERSION + _params->server_tls_ver_min; |
| } |
| // Setting 0 enables version down to the lowest version supported by the SSL library |
| SSL_CTX_set_min_proto_version(ctx, ver); |
| |
| ver = 0; |
| if (_params->server_tls_ver_max >= 0) { |
| ver = TLS1_VERSION + _params->server_tls_ver_max; |
| } |
| // Setting 0 enables version up to the highest version supported by the SSL library |
| SSL_CTX_set_max_proto_version(ctx, ver); |
| } |
| |
| SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF | SSL_SESS_CACHE_NO_INTERNAL); |
| |
| #ifdef SSL_MODE_RELEASE_BUFFERS |
| Dbg(dbg_ctl_ssl_load, "enabling SSL_MODE_RELEASE_BUFFERS"); |
| SSL_CTX_set_mode(ctx, SSL_MODE_RELEASE_BUFFERS); |
| #endif |
| |
| #ifdef SSL_OP_SAFARI_ECDHE_ECDSA_BUG |
| SSL_CTX_set_options(ctx, SSL_OP_SAFARI_ECDHE_ECDSA_BUG); |
| #endif |
| |
| if (sslMultCertSettings) { |
| if (!this->_setup_dialog(ctx, sslMultCertSettings)) { |
| goto fail; |
| } |
| |
| if (sslMultCertSettings->cert && !generate_default_ctx) { |
| if (!SSLMultiCertConfigLoader::load_certs(ctx, cert_names_list, key_names_list, data, _params, sslMultCertSettings)) { |
| goto fail; |
| } |
| } |
| |
| if (!this->_set_verify_path(ctx, sslMultCertSettings)) { |
| goto fail; |
| } |
| |
| if (!this->_setup_session_ticket(ctx, sslMultCertSettings)) { |
| goto fail; |
| } |
| } |
| |
| #if TS_HAS_TLS_SESSION_TICKET |
| if (!ssl_context_enable_ticket_callback(ctx)) { |
| goto fail; |
| } |
| #endif |
| |
| if (!this->_setup_client_cert_verification(ctx)) { |
| goto fail; |
| } |
| |
| if (!SSLMultiCertConfigLoader::set_session_id_context(ctx, _params, sslMultCertSettings)) { |
| goto fail; |
| } |
| |
| if (!this->_set_cipher_suites_for_legacy_versions(ctx)) { |
| goto fail; |
| } |
| |
| if (!this->_set_cipher_suites(ctx)) { |
| goto fail; |
| } |
| |
| if (!this->_set_curves(ctx)) { |
| goto fail; |
| } |
| |
| if (!this->_enable_cert_compression(ctx)) { |
| goto fail; |
| } |
| |
| if (!this->_enable_ktls(ctx)) { |
| goto fail; |
| } |
| |
| if (!this->_enable_early_data(ctx)) { |
| goto fail; |
| } |
| |
| if (!ssl_context_enable_dhe(_params->dhparamsFile, ctx)) { |
| goto fail; |
| } |
| |
| if (sslMultCertSettings && sslMultCertSettings->dialog) { |
| SSLMultiCertConfigLoader::clear_pw_references(ctx); |
| } |
| |
| if (!this->_set_info_callback(ctx)) { |
| goto fail; |
| } |
| |
| if (!this->_set_npn_callback(ctx)) { |
| goto fail; |
| } |
| |
| if (!this->_set_alpn_callback(ctx)) { |
| goto fail; |
| } |
| |
| #if TS_HAS_TLS_KEYLOGGING |
| if (unlikely(TLSKeyLogger::is_enabled()) && !this->_set_keylog_callback(ctx)) { |
| goto fail; |
| } |
| #endif |
| |
| if (SSLConfigParams::init_ssl_ctx_cb) { |
| SSLConfigParams::init_ssl_ctx_cb(ctx, true); |
| } |
| |
| ret.emplace_back(SSLLoadingContext(ctx, ctx_type)); |
| i++; |
| } |
| return ret; |
| |
| fail: |
| ink_assert(ctx != nullptr); |
| SSLMultiCertConfigLoader::clear_pw_references(ctx); |
| SSL_CTX_free(ctx); |
| |
| return ret; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_setup_session_cache(SSL_CTX * /* ctx ATS_UNUSED */) |
| { |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_setup_dialog(SSL_CTX *ctx, const SSLMultiCertConfigParams *sslMultCertSettings) |
| { |
| if (sslMultCertSettings->dialog) { |
| // pass phrase dialog configuration |
| pem_password_cb *passwd_cb = nullptr; |
| if (strncmp(sslMultCertSettings->dialog, "exec:", 5) == 0) { |
| const char *serverDialog = &sslMultCertSettings->dialog[5]; |
| Dbg(dbg_ctl_ssl_load, "exec:%s", serverDialog); |
| // validate the exec program |
| if (!ssl_private_key_validate_exec(serverDialog)) { |
| SSLError("failed to access '%s' pass phrase program: %s", serverDialog, strerror(errno)); |
| return false; |
| } |
| passwd_cb = ssl_private_key_passphrase_callback_exec; |
| } else if (strcmp(sslMultCertSettings->dialog, "builtin") == 0) { |
| passwd_cb = ssl_private_key_passphrase_callback_builtin; |
| } else { // unknown config |
| SSLError("unknown ssl_key_dialog configuration value '%s'", (const char *)sslMultCertSettings->dialog); |
| return false; |
| } |
| SSL_CTX_set_default_passwd_cb(ctx, passwd_cb); |
| SSL_CTX_set_default_passwd_cb_userdata(ctx, const_cast<SSLMultiCertConfigParams *>(sslMultCertSettings)); |
| } |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_set_verify_path(SSL_CTX *ctx, const SSLMultiCertConfigParams *sslMultCertSettings) |
| { |
| // SSL_CTX_load_verify_locations() builds the cert chain from the |
| // serverCACertFilename if that is not nullptr. Otherwise, it uses the hashed |
| // symlinks in serverCACertPath. |
| // |
| // if ssl_ca_name is NOT configured for this cert in ssl_multicert.yaml |
| // AND |
| // if proxy.config.ssl.CA.cert.filename and proxy.config.ssl.CA.cert.path |
| // are configured |
| // pass that file as the chain (include all certs in that file) |
| // else if proxy.config.ssl.CA.cert.path is configured (and |
| // proxy.config.ssl.CA.cert.filename is nullptr) |
| // use the hashed symlinks in that directory to build the chain |
| const SSLConfigParams *params = this->_params; |
| if (!sslMultCertSettings->ca && params->serverCACertPath != nullptr) { |
| if ((!SSL_CTX_load_verify_locations(ctx, params->serverCACertFilename, params->serverCACertPath)) || |
| (!SSL_CTX_set_default_verify_paths(ctx))) { |
| SSLError("invalid CA Certificate file: %s or CA Certificate path: %s", |
| (!params->serverCACertFilename || params->serverCACertFilename[0] == '\0') ? "[empty file name]" : |
| params->serverCACertFilename, |
| (!params->serverCACertPath || params->serverCACertPath[0] == '\0') ? "[empty path]" : params->serverCACertPath); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_setup_session_ticket(SSL_CTX *ctx, const SSLMultiCertConfigParams *sslMultCertSettings) |
| { |
| // Session tickets are enabled by default. Disable if explicitly requested. |
| if (sslMultCertSettings->session_ticket_enabled == 0) { |
| SSL_CTX_set_options(ctx, SSL_OP_NO_TICKET); |
| Dbg(dbg_ctl_ssl_load, "ssl session ticket is disabled"); |
| } |
| |
| if (!(this->_params->ssl_ctx_options & SSL_OP_NO_TLSv1_3)) { |
| SSL_CTX_set_num_tickets(ctx, sslMultCertSettings->session_ticket_number); |
| Dbg(dbg_ctl_ssl_load, "ssl session ticket number set to %d", sslMultCertSettings->session_ticket_number); |
| } |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_setup_client_cert_verification(SSL_CTX *ctx) |
| { |
| int server_verify_client; |
| const SSLConfigParams *params = this->_params; |
| |
| if (params->clientCertLevel != 0) { |
| if (params->serverCACertFilename != nullptr && params->serverCACertPath != nullptr) { |
| if ((!SSL_CTX_load_verify_locations(ctx, params->serverCACertFilename, params->serverCACertPath)) || |
| (!SSL_CTX_set_default_verify_paths(ctx))) { |
| SSLError("invalid CA Certificate file: %s or CA Certificate path: %s", |
| (!params->serverCACertFilename || params->serverCACertFilename[0] == '\0') ? "[empty file name]" : |
| params->serverCACertFilename, |
| (!params->serverCACertPath || params->serverCACertPath[0] == '\0') ? "[empty path]" : params->serverCACertPath); |
| return false; |
| } |
| } |
| |
| if (params->clientCertLevel == 2) { |
| server_verify_client = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT | SSL_VERIFY_CLIENT_ONCE; |
| } else if (params->clientCertLevel == 1) { |
| server_verify_client = SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE; |
| } else { |
| // disable client cert support |
| server_verify_client = SSL_VERIFY_NONE; |
| Error("illegal client certification level %d in %s", server_verify_client, ts::filename::RECORDS); |
| } |
| SSL_CTX_set_verify(ctx, server_verify_client, ssl_verify_client_callback); |
| SSL_CTX_set_verify_depth(ctx, params->verify_depth); // might want to make configurable at some point. |
| } |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_set_cipher_suites_for_legacy_versions(SSL_CTX *ctx) |
| { |
| if (this->_params->cipherSuite != nullptr) { |
| if (!SSL_CTX_set_cipher_list(ctx, this->_params->cipherSuite)) { |
| SSLError("invalid cipher suite in %s", ts::filename::RECORDS); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_set_cipher_suites([[maybe_unused]] SSL_CTX *ctx) |
| { |
| #if TS_USE_TLS_SET_CIPHERSUITES |
| if (this->_params->server_tls13_cipher_suites != nullptr) { |
| if (!SSL_CTX_set_ciphersuites(ctx, this->_params->server_tls13_cipher_suites)) { |
| SSLError("invalid tls server cipher suites in %s", ts::filename::RECORDS); |
| return false; |
| } |
| } |
| #endif |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_set_curves(SSL_CTX *ctx) |
| { |
| if (this->_params->server_groups_list != nullptr) { |
| if (!SSL_CTX_set1_groups_list(ctx, this->_params->server_groups_list)) { |
| SSLError("invalid groups list for server in %s", ts::filename::RECORDS); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_set_info_callback(SSL_CTX *ctx) |
| { |
| SSL_CTX_set_info_callback(ctx, ssl_callback_info); |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_set_npn_callback(SSL_CTX *ctx) |
| { |
| SSL_CTX_set_next_protos_advertised_cb(ctx, ssl_next_protos_advertised_callback, nullptr); |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_set_alpn_callback(SSL_CTX *ctx) |
| { |
| SSL_CTX_set_alpn_select_cb(ctx, ssl_alpn_select_callback, nullptr); |
| return true; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_set_keylog_callback(SSL_CTX *ctx) |
| { |
| #if TS_HAS_TLS_KEYLOGGING |
| SSL_CTX_set_keylog_callback(ctx, TLSKeyLogger::ssl_keylog_cb); |
| #endif |
| return true; |
| } |
| |
| SSL_CTX * |
| SSLCreateServerContext(const SSLConfigParams *params, const SSLMultiCertConfigParams *sslMultiCertSettings, const char *cert_path, |
| const char *key_path) |
| { |
| SSLMultiCertConfigLoader loader(params); |
| std::vector<X509 *> cert_list; |
| std::set<std::string> common_names; |
| std::unordered_map<int, std::set<std::string>> unique_names; |
| SSLMultiCertConfigLoader::CertLoadData data; |
| SSLCertContextType cert_type; |
| bool loaded = loader.load_certs_and_cross_reference_names(cert_list, data, params, sslMultiCertSettings, common_names, |
| unique_names, &cert_type); |
| // cert_list may hold parsed leaves even when loading failed partway. |
| for (auto &i : cert_list) { |
| X509_free(i); |
| } |
| if (!loaded) { |
| return nullptr; |
| } |
| |
| std::unique_ptr<SSL_CTX, decltype(&SSL_CTX_free)> ctx(nullptr, &SSL_CTX_free); |
| |
| std::vector<SSLLoadingContext> ctxs = loader.init_server_ssl_ctx(data, sslMultiCertSettings); |
| for (auto const &loaderctx : ctxs) { |
| ctx.reset(loaderctx.ctx); |
| |
| if (ctx && cert_path) { |
| if (!SSL_CTX_use_certificate_file(ctx.get(), cert_path, SSL_FILETYPE_PEM)) { |
| SSLError("SSLCreateServerContext(): failed to load server certificate file: %s", |
| (!cert_path || cert_path[0] == '\0') ? "[empty file]" : cert_path); |
| ctx = nullptr; |
| } else if (!key_path || key_path[0] == '\0') { |
| key_path = cert_path; |
| } |
| if (ctx) { |
| if (!SSL_CTX_use_PrivateKey_file(ctx.get(), key_path, SSL_FILETYPE_PEM)) { |
| SSLError("SSLCreateServerContext(): failed to load server private key: %s", |
| (!key_path || key_path[0] == '\0') ? "[empty file]" : key_path); |
| ctx = nullptr; |
| } else if (!SSL_CTX_check_private_key(ctx.get())) { |
| SSLError("SSLCreateServerContext(): server private key: %s does not match server certificate: %s", |
| (!key_path || key_path[0] == '\0') ? "[empty file]" : key_path, |
| (!cert_path || cert_path[0] == '\0') ? "[empty file]" : cert_path); |
| ctx = nullptr; |
| } |
| } |
| } |
| } |
| return ctx.release(); |
| } |
| |
| /** |
| * Common name resolution and cert validation |
| */ |
| bool |
| SSLMultiCertConfigLoader::_prep_ssl_ctx(const shared_SSLMultiCertConfigParams &sslMultCertSettings, |
| SSLMultiCertConfigLoader::CertLoadData &data, std::set<std::string> &common_names, |
| std::unordered_map<int, std::set<std::string>> &unique_names) |
| { |
| std::vector<X509 *> cert_list; |
| const SSLConfigParams *params = this->_params; |
| |
| SSLCertContextType cert_type; |
| bool good_certs = this->load_certs_and_cross_reference_names(cert_list, data, params, sslMultCertSettings.get(), common_names, |
| unique_names, &cert_type); |
| |
| if (good_certs) { |
| int i = 0; |
| for (auto const &cert : cert_list) { |
| const char *current_cert_name = data.cert_names_list[i].c_str(); |
| if (0 > SSLMultiCertConfigLoader::check_server_cert_now(cert, current_cert_name)) { |
| /* At this point, we know cert is bad, and we've already printed a |
| descriptive reason as to why cert is bad to the log file */ |
| Dbg(this->_dbg_ctl(), "Marking certificate as NOT VALID: %s", current_cert_name); |
| good_certs = false; |
| } |
| i++; |
| } |
| } |
| |
| // cert_list may hold parsed leaves even when loading failed partway. |
| for (auto &cert : cert_list) { |
| X509_free(cert); |
| } |
| return good_certs; |
| } |
| |
| /** |
| Insert SSLCertContext (SSL_CTX and options) into SSLCertLookup with key. |
| Do NOT call SSL_CTX_set_* functions from here. SSL_CTX should be set up by SSLMultiCertConfigLoader::init_server_ssl_ctx(). |
| */ |
| bool |
| SSLMultiCertConfigLoader::_store_ssl_ctx(SSLCertLookup *lookup, const shared_SSLMultiCertConfigParams &sslMultCertSettings) |
| { |
| bool retval = true; |
| std::set<std::string> common_names; |
| std::unordered_map<int, std::set<std::string>> unique_names; |
| SSLMultiCertConfigLoader::CertLoadData data; |
| |
| if (!this->_prep_ssl_ctx(sslMultCertSettings, data, common_names, unique_names)) { |
| { |
| std::lock_guard<std::mutex> lock(_loader_mutex); |
| lookup->is_valid = false; |
| } |
| return false; |
| } |
| |
| std::vector<SSLLoadingContext> ctxs = this->init_server_ssl_ctx(data, sslMultCertSettings.get()); |
| |
| // Serialize all mutations to the shared SSLCertLookup. |
| // The expensive work above (_prep_ssl_ctx + init_server_ssl_ctx) runs |
| // without the lock, allowing parallel cert loading across threads. |
| std::lock_guard<std::mutex> lock(_loader_mutex); |
| |
| for (const auto &loadingctx : ctxs) { |
| if (!sslMultCertSettings || |
| !this->_store_single_ssl_ctx(lookup, sslMultCertSettings, shared_SSL_CTX{loadingctx.ctx, SSL_CTX_free}, loadingctx.ctx_type, |
| common_names)) { |
| if (!common_names.empty()) { |
| std::string names; |
| for (auto const &name : data.cert_names_list) { |
| names.append(name); |
| names.append(" "); |
| } |
| Warning("(%s) Failed to insert SSL_CTX for certificate %s entries for names already made", this->_debug_tag(), |
| names.c_str()); |
| } else { |
| Warning("(%s) Failed to insert SSL_CTX", this->_debug_tag()); |
| } |
| } else { |
| if (!common_names.empty()) { |
| lookup->register_cert_secrets(data.cert_names_list, common_names); |
| } |
| } |
| } |
| |
| for (auto iter = unique_names.begin(); retval && iter != unique_names.end(); ++iter) { |
| size_t i = iter->first; |
| |
| SSLMultiCertConfigLoader::CertLoadData single_data; |
| single_data.cert_names_list.push_back(data.cert_names_list[i]); |
| if (i < data.key_list.size()) { |
| single_data.key_list.push_back(data.key_list[i]); |
| } |
| single_data.ca_list.push_back(i < data.ca_list.size() ? data.ca_list[i] : ""); |
| single_data.ocsp_list.push_back(i < data.ocsp_list.size() ? data.ocsp_list[i] : ""); |
| |
| std::vector<SSLLoadingContext> ctxs = this->init_server_ssl_ctx(single_data, sslMultCertSettings.get()); |
| for (const auto &loadingctx : ctxs) { |
| if (!this->_store_single_ssl_ctx(lookup, sslMultCertSettings, shared_SSL_CTX{loadingctx.ctx, SSL_CTX_free}, |
| loadingctx.ctx_type, iter->second)) { |
| retval = false; |
| } else { |
| lookup->register_cert_secrets(data.cert_names_list, iter->second); |
| } |
| } |
| } |
| return retval; |
| } |
| |
| /** |
| * Much like _store_ssl_ctx, but this updates the existing lookup entries rather than creating them |
| * If it fails to create the new SSL_CTX, don't invalidate the lookup structure, just keep working with the |
| * previous entry |
| */ |
| bool |
| SSLMultiCertConfigLoader::update_ssl_ctx(const std::string &secret_name) |
| { |
| bool retval = true; |
| |
| SSLCertificateConfig::scoped_config lookup; |
| if (!lookup) { |
| // SSLCertificateConfig is still being configured, thus there are no SSL |
| // contexts to update. This situation can happen during startup if a |
| // registered hook updates certs before SSLCertContext configuration is |
| // complete. |
| return retval; |
| } |
| std::set<shared_SSLMultiCertConfigParams> policies; |
| lookup->getPolicies(secret_name, policies); |
| |
| for (auto policy_iter = policies.begin(); policy_iter != policies.end() && retval; ++policy_iter) { |
| std::set<std::string> common_names; |
| std::unordered_map<int, std::set<std::string>> unique_names; |
| SSLMultiCertConfigLoader::CertLoadData data; |
| if (!this->_prep_ssl_ctx(*policy_iter, data, common_names, unique_names)) { |
| retval = false; |
| break; |
| } |
| |
| std::vector<SSLLoadingContext> ctxs = this->init_server_ssl_ctx(data, policy_iter->get()); |
| for (const auto &loadingctx : ctxs) { |
| shared_SSL_CTX ctx(loadingctx.ctx, SSL_CTX_free); |
| |
| if (!ctx) { |
| retval = false; |
| } else { |
| if ((*policy_iter)->addr) { |
| SSLCertContext *cc = nullptr; |
| if (strcmp((*policy_iter)->addr, "*") == 0) { |
| this->_set_handshake_callbacks(ctx.get()); |
| cc = lookup->find("*", loadingctx.ctx_type); |
| } else { |
| IpEndpoint ep; |
| if (ats_ip_pton((*policy_iter)->addr, &ep) == 0) { |
| cc = lookup->find(ep, loadingctx.ctx_type); |
| } else { |
| Error("'%s' is not a valid IPv4 or IPv6 address", (const char *)(*policy_iter)->addr); |
| retval = false; |
| } |
| } |
| if (cc && cc->userconfig.get() == policy_iter->get()) { |
| cc->setCtx(ctx); |
| if (strcmp((*policy_iter)->addr, "*") == 0) { |
| lookup->setDefaultContext(ctx); |
| } |
| } |
| } |
| for (auto const &name : common_names) { |
| SSLCertContext *cc = lookup->find(name, loadingctx.ctx_type); |
| if (cc && cc->userconfig.get() == policy_iter->get()) { |
| cc->setCtx(ctx); |
| } |
| } |
| } |
| } |
| |
| for (auto iter = unique_names.begin(); retval && iter != unique_names.end(); ++iter) { |
| size_t i = iter->first; |
| |
| SSLMultiCertConfigLoader::CertLoadData single_data; |
| single_data.cert_names_list.push_back(data.cert_names_list[i]); |
| single_data.key_list.push_back(i < data.key_list.size() ? data.key_list[i] : ""); |
| single_data.ca_list.push_back(i < data.ca_list.size() ? data.ca_list[i] : ""); |
| single_data.ocsp_list.push_back(i < data.ocsp_list.size() ? data.ocsp_list[i] : ""); |
| |
| std::vector<SSLLoadingContext> ctxs = this->init_server_ssl_ctx(single_data, policy_iter->get()); |
| for (auto const &loadingctx : ctxs) { |
| shared_SSL_CTX unique_ctx(loadingctx.ctx, SSL_CTX_free); |
| |
| if (!unique_ctx) { |
| retval = false; |
| } else { |
| for (auto const &name : iter->second) { |
| SSLCertContext *cc = lookup->find(name, loadingctx.ctx_type); |
| if (cc && cc->userconfig.get() == policy_iter->get()) { |
| cc->setCtx(unique_ctx); |
| } |
| } |
| } |
| } |
| } |
| } |
| return retval; |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_store_single_ssl_ctx(SSLCertLookup *lookup, const shared_SSLMultiCertConfigParams &sslMultCertSettings, |
| shared_SSL_CTX ctx, SSLCertContextType ctx_type, std::set<std::string> &names) |
| { |
| bool inserted = false; |
| // Load the session ticket key if session tickets are not disabled |
| if (sslMultCertSettings->session_ticket_enabled != 0) { |
| if (!ssl_context_enable_tickets(ctx.get())) { |
| return false; |
| } |
| } |
| |
| // Index this certificate by the specified IP(v6) address. If the address is "*", make it the default context. |
| if (sslMultCertSettings->addr) { |
| if (strcmp(sslMultCertSettings->addr, "*") == 0) { |
| Dbg(dbg_ctl_ssl_load, "Addr is '*'; setting %p to default", ctx.get()); |
| if (lookup->insert(sslMultCertSettings->addr, SSLCertContext(ctx, ctx_type, sslMultCertSettings)) >= 0) { |
| inserted = true; |
| lookup->setDefaultContext(ctx); |
| this->_set_handshake_callbacks(ctx.get()); |
| } |
| } else { |
| IpEndpoint ep; |
| |
| if (ats_ip_pton(sslMultCertSettings->addr, &ep) == 0) { |
| if (lookup->insert(ep, SSLCertContext(ctx, ctx_type, sslMultCertSettings)) >= 0) { |
| inserted = true; |
| } |
| } else { |
| Error("'%s' is not a valid IPv4 or IPv6 address", (const char *)sslMultCertSettings->addr); |
| lookup->is_valid = false; |
| } |
| } |
| } |
| |
| // Insert additional mappings. Note that this maps multiple keys to the same value, so when |
| // this code is updated to reconfigure the SSL certificates, it will need some sort of |
| // refcounting or alternate way of avoiding double frees. |
| for (auto const &sni_name : names) { |
| if (lookup->insert(sni_name.c_str(), SSLCertContext(ctx, ctx_type, sslMultCertSettings)) >= 0) { |
| inserted = true; |
| } |
| } |
| |
| if (inserted) { |
| if (SSLConfigParams::init_ssl_ctx_cb) { |
| SSLConfigParams::init_ssl_ctx_cb(ctx.get(), true); |
| } |
| } |
| |
| if (!inserted) { |
| ctx = nullptr; |
| } |
| |
| return ctx.get(); |
| } |
| |
| bool |
| SSLMultiCertConfigLoader::_should_track_load_metrics() const |
| { |
| return true; |
| } |
| |
| swoc::Errata |
| SSLMultiCertConfigLoader::load(SSLCertLookup *lookup, bool firstLoad) |
| { |
| const SSLConfigParams *params = this->_params; |
| |
| Note("(%s) %s loading ...", this->_debug_tag(), ts::filename::SSL_MULTICERT); |
| |
| // Optionally elevate/allow file access to read root-only |
| // certificates. The destructor will drop privilege for us. |
| uint32_t elevate_setting = 0; |
| elevate_setting = RecGetRecordInt("proxy.config.ssl.cert.load_elevated").value_or(0); |
| ElevateAccess elevate_access(elevate_setting ? ElevateAccess::FILE_PRIVILEGE : 0); |
| |
| // Guard against nullptr configFilePath which can happen if records aren't initialized. |
| if (params->configFilePath == nullptr) { |
| return swoc::Errata(ERRATA_WARN, "No SSL certificate configuration file path configured"); |
| } |
| |
| config::SSLMultiCertParser parser; |
| config::ConfigResult<config::SSLMultiCertConfig> parse_result = parser.parse(params->configFilePath); |
| if (!parse_result.ok()) { |
| return std::move(parse_result.errata); |
| } |
| |
| swoc::Errata errata(ERRATA_NOTE); |
| |
| static constexpr int MAX_LOAD_THREADS = 256; |
| |
| int num_threads = params->configLoadConcurrency; |
| if (firstLoad) { |
| num_threads = std::clamp(static_cast<int>(std::thread::hardware_concurrency()), 1, MAX_LOAD_THREADS); |
| } |
| num_threads = std::min(num_threads, static_cast<int>(parse_result.value.size())); |
| |
| if (num_threads > 1 && parse_result.value.size() > 1) { |
| std::size_t bucket_size = parse_result.value.size() / num_threads; |
| std::size_t remainder = parse_result.value.size() % num_threads; |
| auto current = parse_result.value.cbegin(); |
| |
| std::vector<std::thread> threads; |
| Note("(%s) loading %zu certs with %d threads", this->_debug_tag(), parse_result.value.size(), num_threads); |
| |
| for (int t = 0; t < num_threads; ++t) { |
| std::size_t this_bucket = bucket_size + (static_cast<std::size_t>(t) < remainder ? 1 : 0); |
| auto end = current + this_bucket; |
| int base_index = static_cast<int>(std::distance(parse_result.value.cbegin(), current)); |
| threads.emplace_back(&SSLMultiCertConfigLoader::_load_items, this, lookup, current, end, base_index, std::ref(errata)); |
| current = end; |
| } |
| |
| for (auto &th : threads) { |
| th.join(); |
| } |
| |
| Note("(%s) loaded %zu certs in %d threads", this->_debug_tag(), parse_result.value.size(), num_threads); |
| } else { |
| _load_items(lookup, parse_result.value.cbegin(), parse_result.value.cend(), 0, errata); |
| Note("(%s) loaded %zu certs (single-threaded)", this->_debug_tag(), parse_result.value.size()); |
| } |
| |
| // We *must* have a default context even if it can't possibly work. The default context is used to |
| // bootstrap the SSL handshake so that we can subsequently do the SNI lookup to switch to the real |
| // context. |
| if (lookup->defaultContext() == nullptr) { |
| shared_SSLMultiCertConfigParams sslMultiCertSettings(new SSLMultiCertConfigParams); |
| sslMultiCertSettings->addr = ats_strdup("*"); |
| if (!this->_store_ssl_ctx(lookup, sslMultiCertSettings)) { |
| errata.note(ERRATA_ERROR, "failed set default context"); |
| } |
| } |
| |
| return errata; |
| } |
| |
| void |
| SSLMultiCertConfigLoader::_load_items(SSLCertLookup *lookup, config::SSLMultiCertConfig::const_iterator begin, |
| config::SSLMultiCertConfig::const_iterator end, int base_index, swoc::Errata &errata) |
| { |
| // Each thread needs its own elevated privileges since POSIX capabilities are per-thread |
| uint32_t elevate_setting = 0; |
| elevate_setting = RecGetRecordInt("proxy.config.ssl.cert.load_elevated").value_or(0); |
| ElevateAccess elevate_access(elevate_setting ? ElevateAccess::FILE_PRIVILEGE : 0); |
| |
| int item_num = base_index; |
| for (auto it = begin; it != end; ++it) { |
| item_num++; |
| const auto &item = *it; |
| |
| shared_SSLMultiCertConfigParams sslMultiCertSettings = std::make_shared<SSLMultiCertConfigParams>(); |
| |
| if (!item.ssl_cert_name.empty()) { |
| sslMultiCertSettings->cert = ats_strdup(item.ssl_cert_name.c_str()); |
| } |
| if (!item.dest_ip.empty()) { |
| sslMultiCertSettings->addr = ats_strdup(item.dest_ip.c_str()); |
| } |
| if (!item.ssl_key_name.empty()) { |
| sslMultiCertSettings->key = ats_strdup(item.ssl_key_name.c_str()); |
| } |
| if (!item.ssl_ca_name.empty()) { |
| sslMultiCertSettings->ca = ats_strdup(item.ssl_ca_name.c_str()); |
| } |
| if (!item.ssl_ocsp_name.empty()) { |
| sslMultiCertSettings->ocsp_response = ats_strdup(item.ssl_ocsp_name.c_str()); |
| } |
| if (!item.ssl_key_dialog.empty()) { |
| sslMultiCertSettings->dialog = ats_strdup(item.ssl_key_dialog.c_str()); |
| } |
| if (!item.dest_fqdn.empty()) { |
| sslMultiCertSettings->servername = ats_strdup(item.dest_fqdn.c_str()); |
| } |
| if (item.ssl_ticket_enabled.has_value()) { |
| sslMultiCertSettings->session_ticket_enabled = item.ssl_ticket_enabled.value(); |
| } |
| if (item.ssl_ticket_number.has_value()) { |
| sslMultiCertSettings->session_ticket_number = item.ssl_ticket_number.value(); |
| } |
| if (item.action == "tunnel") { |
| sslMultiCertSettings->opt = SSLCertContextOption::OPT_TUNNEL; |
| } |
| |
| // There must be a certificate specified unless the tunnel action is set. |
| if (sslMultiCertSettings->cert || sslMultiCertSettings->opt == SSLCertContextOption::OPT_TUNNEL) { |
| if (!this->_store_ssl_ctx(lookup, sslMultiCertSettings)) { |
| std::lock_guard<std::mutex> lock(_loader_mutex); |
| errata.note(ERRATA_ERROR, "Failed to load certificate '{}' at item {}", |
| sslMultiCertSettings->cert ? sslMultiCertSettings->cert : "(unnamed)", item_num); |
| // Guard required: SSLCertificateConfig::startup() ultimately calls _load_items() via |
| // reconfigure() and load(), and runs before SSLInitializeStatistics() in |
| // SSLNetProcessor::start(), so this counter is nullptr during the TLS startup cert load. |
| // The QUIC loader is excluded from this counter entirely via _should_track_load_metrics(), |
| // so the nullptr guard is TLS-startup-specific. |
| if (ssl_rsb.ssl_multicert_load_failures && _should_track_load_metrics()) { |
| Metrics::Counter::increment(ssl_rsb.ssl_multicert_load_failures); |
| } |
| } else if (sslMultiCertSettings->cert) { |
| std::lock_guard<std::mutex> lock(_loader_mutex); |
| ++lookup->user_cert_count; |
| } |
| } else { |
| std::lock_guard<std::mutex> lock(_loader_mutex); |
| errata.note(ERRATA_WARN, "No ssl_cert_name specified and no tunnel action set at item {}", item_num); |
| } |
| } |
| } |
| |
| // Release SSL_CTX and the associated data. This works for both |
| // client and server contexts and gracefully accepts nullptr. |
| void |
| SSLReleaseContext(SSL_CTX *ctx) |
| { |
| SSL_CTX_free(ctx); |
| } |
| |
| void |
| SSLNetVCAttach(SSL *ssl, SSLNetVConnection *vc) |
| { |
| SSL_set_ex_data(ssl, ssl_vc_index, vc); |
| } |
| |
| void |
| SSLNetVCDetach(SSL *ssl) |
| { |
| SSL_set_ex_data(ssl, ssl_vc_index, nullptr); |
| } |
| |
| SSLNetVConnection * |
| SSLNetVCAccess(const SSL *ssl) |
| { |
| SSLNetVConnection *netvc; |
| netvc = static_cast<SSLNetVConnection *>(SSL_get_ex_data(ssl, ssl_vc_index)); |
| |
| ink_assert(dynamic_cast<SSLNetVConnection *>(static_cast<NetVConnection *>(SSL_get_ex_data(ssl, ssl_vc_index)))); |
| |
| return netvc; |
| } |
| |
| std::string |
| get_sni_addr(SSL *ssl) |
| { |
| std::string sni_addr; |
| |
| if (ssl != nullptr) { |
| const char *sni_name = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); |
| if (sni_name) { |
| sni_addr.assign(sni_name); |
| } else { |
| int sock_fd = SSL_get_fd(ssl); |
| sockaddr_storage addr; |
| socklen_t addr_len = sizeof(addr); |
| if (sock_fd >= 0) { |
| getpeername(sock_fd, reinterpret_cast<sockaddr *>(&addr), &addr_len); |
| if (addr.ss_family == AF_INET || addr.ss_family == AF_INET6) { |
| char ip_addr[INET6_ADDRSTRLEN]; |
| ats_ip_ntop(reinterpret_cast<sockaddr *>(&addr), ip_addr, INET6_ADDRSTRLEN); |
| sni_addr.assign(ip_addr); |
| } |
| } |
| } |
| } |
| |
| return sni_addr; |
| } |
| |
| std::string |
| get_verify_str(SSL *ssl) |
| { |
| std::string verify_str; |
| |
| SSLNetVConnection *netvc = SSLNetVCAccess(ssl); |
| if (netvc != nullptr) { |
| std::string policy_str; |
| switch (netvc->options.verifyServerPolicy) { |
| case YamlSNIConfig::Policy::DISABLED: |
| policy_str.assign("DISABLED"); |
| break; |
| case YamlSNIConfig::Policy::PERMISSIVE: |
| policy_str.assign("PERMISSIVE"); |
| break; |
| case YamlSNIConfig::Policy::ENFORCED: |
| policy_str.assign("ENFORCED"); |
| break; |
| case YamlSNIConfig::Policy::UNSET: |
| policy_str.assign("UNSET"); |
| break; |
| } |
| |
| std::string property_str; |
| switch (netvc->options.verifyServerProperties) { |
| case YamlSNIConfig::Property::NONE: |
| property_str.assign("NONE"); |
| break; |
| case YamlSNIConfig::Property::SIGNATURE_MASK: |
| property_str.assign("SIGNATURE_MASK"); |
| break; |
| case YamlSNIConfig::Property::NAME_MASK: |
| property_str.assign("NAME_MASK"); |
| break; |
| case YamlSNIConfig::Property::ALL_MASK: |
| property_str.assign("ALL_MASK"); |
| break; |
| case YamlSNIConfig::Property::UNSET: |
| property_str.assign("UNSET"); |
| break; |
| } |
| |
| swoc::bwprint(verify_str, "{}:{}", policy_str.c_str(), property_str.c_str()); |
| } |
| |
| return verify_str; |
| } |
| |
| /** |
| * Process the config to pull out the list of file names, and process the certs to get the list |
| * of subject and sni names. Thanks to dual cert configurations, there may be multiple files of each type. |
| * If some names are not in all the listed certs they are listed in the uniqe_names map, keyed by the index |
| * of the including certificate |
| */ |
| bool |
| SSLMultiCertConfigLoader::load_certs_and_cross_reference_names( |
| std::vector<X509 *> &cert_list, SSLMultiCertConfigLoader::CertLoadData &data, const SSLConfigParams *params, |
| const SSLMultiCertConfigParams *sslMultCertSettings, std::set<std::string> &common_names, |
| std::unordered_map<int, std::set<std::string>> &unique_names, SSLCertContextType *certType) |
| { |
| SimpleTokenizer cert_tok(sslMultCertSettings && sslMultCertSettings->cert ? sslMultCertSettings->cert : "", |
| SSL_CERT_SEPARATE_DELIM); |
| |
| SimpleTokenizer key_tok(SSL_CERT_SEPARATE_DELIM); |
| if (sslMultCertSettings && sslMultCertSettings->key) { |
| key_tok.setString(sslMultCertSettings->key); |
| } else { |
| key_tok.setString(""); |
| } |
| |
| size_t cert_tok_num = cert_tok.getNumTokensRemaining(); |
| if (sslMultCertSettings && sslMultCertSettings->key && cert_tok_num != key_tok.getNumTokensRemaining()) { |
| Error("the number of certificates in ssl_cert_name (%zu) and ssl_key_name (%zu) do not match", cert_tok_num, |
| key_tok.getNumTokensRemaining()); |
| return false; |
| } |
| |
| SimpleTokenizer ca_tok("", SSL_CERT_SEPARATE_DELIM); |
| if (sslMultCertSettings && sslMultCertSettings->ca) { |
| ca_tok.setString(sslMultCertSettings->ca); |
| if (cert_tok.getNumTokensRemaining() != ca_tok.getNumTokensRemaining()) { |
| Error("the number of certificates in ssl_cert_name (%zu) and ssl_ca_name (%zu) do not match", cert_tok_num, |
| ca_tok.getNumTokensRemaining()); |
| return false; |
| } |
| } |
| |
| SimpleTokenizer ocsp_tok("", SSL_CERT_SEPARATE_DELIM); |
| if (sslMultCertSettings && sslMultCertSettings->ocsp_response) { |
| ocsp_tok.setString(sslMultCertSettings->ocsp_response); |
| if (cert_tok.getNumTokensRemaining() != ocsp_tok.getNumTokensRemaining()) { |
| Error("the number of certificates in ssl_cert_name (%zu) and ssl_ocsp_name (%zu) do not match", cert_tok_num, |
| ocsp_tok.getNumTokensRemaining()); |
| return false; |
| } |
| } |
| |
| for (const char *keyname = key_tok.getNext(); keyname; keyname = key_tok.getNext()) { |
| std::string completeServerKeyPath = Layout::get()->relative_to(params->serverKeyPathOnly, keyname); |
| data.key_list.push_back(std::move(completeServerKeyPath)); |
| } |
| |
| for (const char *caname = ca_tok.getNext(); caname; caname = ca_tok.getNext()) { |
| data.ca_list.push_back(caname); |
| } |
| |
| for (const char *ocspname = ocsp_tok.getNext(); ocspname; ocspname = ocsp_tok.getNext()) { |
| data.ocsp_list.push_back(ocspname); |
| } |
| |
| bool first_pass = true; |
| int cert_index = 0; |
| for (const char *certname = cert_tok.getNext(); certname; certname = cert_tok.getNext()) { |
| std::string completeServerCertPath = Layout::relative_to(params->serverCertPathOnly, certname); |
| data.cert_names_list.push_back(std::move(completeServerCertPath)); |
| } |
| |
| for (size_t i = 0; i < data.cert_names_list.size(); i++) { |
| std::string secret_data; |
| std::string secret_key_data; |
| params->secrets.getOrLoadSecret(data.cert_names_list[i], data.key_list.size() > i ? data.key_list[i] : "", secret_data, |
| secret_key_data); |
| if (secret_data.empty()) { |
| SSLError("failed to load certificate secret for %s with key path %s", data.cert_names_list[i].c_str(), |
| data.key_list.size() > i ? data.key_list[i].c_str() : "[empty key path]"); |
| return false; |
| } |
| scoped_BIO bio(BIO_new_mem_buf(secret_data.data(), secret_data.size())); |
| X509 *cert = nullptr; |
| if (bio) { |
| cert = PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr); |
| } else { |
| SSLError("failed to create bio for certificate secret %s of length %ld", data.cert_names_list[i].c_str(), secret_data.size()); |
| return false; |
| } |
| if (!bio || !cert) { |
| SSLError("failed to load certificate chain from %s of length %ld with key path %s", data.cert_names_list[i].c_str(), |
| secret_data.size(), data.key_list.size() > i ? data.key_list[i].c_str() : "[empty key path]"); |
| return false; |
| } |
| |
| if (certType != nullptr) { |
| #ifndef HAVE_NATIVE_DUAL_CERT_SUPPORT |
| std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> public_key(X509_get_pubkey(cert), &EVP_PKEY_free); |
| int pkey_id = EVP_PKEY_id(public_key.get()); |
| |
| switch (pkey_id) { |
| case EVP_PKEY_EC: |
| *certType = SSLCertContextType::EC; |
| break; |
| case EVP_PKEY_RSA: |
| *certType = SSLCertContextType::RSA; |
| break; |
| default: |
| ink_assert(false); |
| } |
| #else |
| *certType = SSLCertContextType::GENERIC; |
| #endif |
| data.cert_type_list.push_back(*certType); |
| } |
| |
| cert_list.push_back(cert); |
| |
| std::set<std::string> name_set; |
| // Grub through the names in the certs |
| |
| // Insert a key for the subject CN. |
| auto *subject = X509_get_subject_name(cert); |
| ats_scoped_str subj_name; |
| if (subject) { |
| int pos = -1; |
| for (;;) { |
| pos = X509_NAME_get_index_by_NID(subject, NID_commonName, pos); |
| if (pos == -1) { |
| break; |
| } |
| |
| const X509_NAME_ENTRY *e = X509_NAME_get_entry(subject, pos); |
| const ASN1_STRING *cn = X509_NAME_ENTRY_get_data(e); |
| subj_name = asn1_strdup(cn); |
| |
| Dbg(dbg_ctl_ssl_load, "subj '%s' in certificate %s %p", subj_name.get(), data.cert_names_list[i].c_str(), cert); |
| name_set.insert(subj_name.get()); |
| } |
| if (name_set.empty()) { |
| Dbg(dbg_ctl_ssl_load, "no subj name in certificate %s", data.cert_names_list[i].c_str()); |
| } |
| } |
| |
| // Traverse the subjectAltNames (if any) and insert additional keys for the SSL context. |
| GENERAL_NAMES *names = static_cast<GENERAL_NAMES *>(X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr)); |
| if (names) { |
| unsigned count = sk_GENERAL_NAME_num(names); |
| for (unsigned i = 0; i < count; ++i) { |
| GENERAL_NAME *name; |
| |
| name = sk_GENERAL_NAME_value(names, i); |
| if (name->type == GEN_DNS) { |
| ats_scoped_str dns(asn1_strdup(name->d.dNSName)); |
| Dbg(dbg_ctl_ssl_load, "inserting dns '%s' in certificate", dns.get()); |
| name_set.insert(dns.get()); |
| } |
| } |
| sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free); |
| } |
| |
| if (first_pass) { |
| first_pass = false; |
| common_names = std::move(name_set); |
| } else { |
| // Check that all elements in common_names are in name_set |
| auto common_iter = common_names.begin(); |
| while (common_iter != common_names.end()) { |
| auto iter = name_set.find(*common_iter); |
| if (iter == name_set.end()) { |
| // Common_name not in new set, move name to unique set |
| auto iter = unique_names.find(cert_index - 1); |
| if (iter == unique_names.end()) { |
| std::set<std::string> new_set; |
| new_set.insert(*common_iter); |
| unique_names.insert({cert_index - 1, new_set}); |
| } else { |
| iter->second.insert(*common_iter); |
| } |
| auto erase_iter = common_iter; |
| ++common_iter; |
| common_names.erase(erase_iter); |
| } else { |
| // New name already in common set, go ahead and remove it from further consideration |
| name_set.erase(iter); |
| ++common_iter; |
| } |
| } |
| // Anything still in name_set was not in common_names |
| for (auto const &name : name_set) { |
| auto iter = unique_names.find(cert_index); |
| if (iter == unique_names.end()) { |
| std::set<std::string> new_set; |
| new_set.insert(name); |
| unique_names.insert({cert_index, new_set}); |
| } else { |
| iter->second.insert(name); |
| } |
| } |
| } |
| cert_index++; |
| } |
| return true; |
| } |
| |
| /** |
| Load certificates to SSL_CTX |
| @static |
| */ |
| bool |
| SSLMultiCertConfigLoader::load_certs(SSL_CTX *ctx, const std::vector<std::string> &cert_names_list, |
| const std::vector<std::string> &key_list, CertLoadData const &data, |
| const SSLConfigParams *params, const SSLMultiCertConfigParams *sslMultCertSettings) |
| { |
| if (SSLConfigParams::ssl_ocsp_enabled) { |
| Dbg(dbg_ctl_ssl_load, "SSL OCSP Stapling is enabled"); |
| SSL_CTX_set_tlsext_status_cb(ctx, ssl_callback_ocsp_stapling); |
| } else { |
| Dbg(dbg_ctl_ssl_load, "SSL OCSP Stapling is disabled"); |
| } |
| |
| ink_assert(!cert_names_list.empty()); |
| |
| for (size_t i = 0; i < cert_names_list.size(); i++) { |
| std::string keyPath = (i < key_list.size()) ? key_list[i] : ""; |
| std::string secret_data; |
| std::string secret_key_data; |
| params->secrets.getOrLoadSecret(cert_names_list[i], keyPath, secret_data, secret_key_data); |
| if (secret_data.empty()) { |
| SSLError("failed to load certificate secret for %s with key path %s", cert_names_list[i].c_str(), |
| keyPath.empty() ? "[empty key path]" : keyPath.c_str()); |
| return false; |
| } |
| scoped_BIO bio(BIO_new_mem_buf(secret_data.data(), secret_data.size())); |
| scoped_X509 cert; |
| if (bio) { |
| cert.reset(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)); |
| } else { |
| SSLError("failed to create bio for certificate secret %s of length %ld", data.cert_names_list[i].c_str(), secret_data.size()); |
| return false; |
| } |
| |
| if (!bio || !cert) { |
| SSLError("failed to load certificate chain from %s", cert_names_list[i].c_str()); |
| return false; |
| } |
| |
| Dbg(dbg_ctl_ssl_load, "for ctx=%p, using certificate %s", ctx, cert_names_list[i].c_str()); |
| if (!SSL_CTX_use_certificate(ctx, cert.get())) { |
| SSLError("Failed to assign cert from %s to SSL_CTX", cert_names_list[i].c_str()); |
| return false; |
| } |
| |
| // Load up any additional chain certificates |
| if (!SSL_CTX_add_extra_chain_cert_bio(ctx, bio.get())) { |
| Dbg(dbg_ctl_ssl_load, "couldn't add chain to %p", ctx); |
| SSLError("failed to load intermediate certificate chain from %s", cert_names_list[i].c_str()); |
| return false; |
| } |
| |
| if (secret_key_data.empty()) { |
| Dbg(dbg_ctl_ssl_load, "empty private key for public key %s", cert_names_list[i].c_str()); |
| secret_key_data = std::move(secret_data); |
| } |
| if (!SSLPrivateKeyHandler(ctx, keyPath.c_str(), secret_key_data.data(), secret_key_data.size())) { |
| SSLError("failed to load certificate: %s of length %ld with key path: %s", cert_names_list[i].c_str(), secret_key_data.size(), |
| keyPath.empty() ? "[empty key path]" : keyPath.c_str()); |
| return false; |
| } |
| // Must load all the intermediate certificates before starting the next chain |
| |
| // First, load any CA chains from the global chain file. This should probably |
| // eventually be a comma separated list too. For now we will load it in all chains even |
| // though it only makes sense in one chain |
| if (params->serverCertChainFilename) { |
| std::string completeServerCertChainPath(Layout::relative_to(params->serverCertPathOnly, params->serverCertChainFilename)); |
| if (!SSL_CTX_add_extra_chain_cert_file(ctx, completeServerCertChainPath.c_str())) { |
| SSLError("failed to load global certificate chain from %s", completeServerCertChainPath.c_str()); |
| return false; |
| } |
| if (SSLConfigParams::load_ssl_file_cb) { |
| SSLConfigParams::load_ssl_file_cb(completeServerCertChainPath.c_str()); |
| } |
| } |
| |
| // Now, load any additional certificate chains specified in this entry. |
| if (sslMultCertSettings->ca) { |
| const char *ca_name = data.ca_list[i].c_str(); |
| if (ca_name != nullptr) { |
| std::string completeServerCertChainPath(Layout::relative_to(params->serverCertPathOnly, ca_name)); |
| if (!SSL_CTX_add_extra_chain_cert_file(ctx, completeServerCertChainPath.c_str())) { |
| SSLError("failed to load certificate chain from %s", completeServerCertChainPath.c_str()); |
| return false; |
| } |
| if (SSLConfigParams::load_ssl_file_cb) { |
| SSLConfigParams::load_ssl_file_cb(completeServerCertChainPath.c_str()); |
| } |
| } |
| } |
| if (SSLConfigParams::ssl_ocsp_enabled) { |
| if (sslMultCertSettings->ocsp_response) { |
| const char *ocsp_response_name = data.ocsp_list[i].c_str(); |
| std::string completeOCSPResponsePath(Layout::relative_to(params->ssl_ocsp_response_path_only, ocsp_response_name)); |
| if (!ssl_stapling_init_cert(ctx, cert.get(), cert_names_list[i].c_str(), completeOCSPResponsePath.c_str())) { |
| Warning("failed to configure SSL_CTX for OCSP Stapling info for certificate at %s", cert_names_list[i].c_str()); |
| } |
| } else { |
| if (!ssl_stapling_init_cert(ctx, cert.get(), cert_names_list[i].c_str(), nullptr)) { |
| Warning("failed to configure SSL_CTX for OCSP Stapling info for certificate at %s", cert_names_list[i].c_str()); |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| Set session_id context for session reuse |
| @static |
| */ |
| bool |
| SSLMultiCertConfigLoader::set_session_id_context(SSL_CTX *ctx, const SSLConfigParams *params, |
| const SSLMultiCertConfigParams *sslMultCertSettings) |
| { |
| EVP_MD_CTX *digest = EVP_MD_CTX_new(); |
| STACK_OF(X509_NAME) *ca_list = nullptr; |
| unsigned char hash_buf[EVP_MAX_MD_SIZE]; |
| unsigned int hash_len = 0; |
| const char *setting_cert = sslMultCertSettings ? sslMultCertSettings->cert.get() : nullptr; |
| bool result = false; |
| |
| if (params->serverCACertFilename) { |
| ca_list = SSL_load_client_CA_file(params->serverCACertFilename); |
| } |
| |
| if (EVP_DigestInit_ex(digest, evp_md_func, nullptr) == 0) { |
| SSLError("EVP_DigestInit_ex failed"); |
| goto fail; |
| } |
| |
| if (nullptr != setting_cert) { |
| Dbg(dbg_ctl_ssl_load, "Using '%s' in hash for session id context", sslMultCertSettings->cert.get()); |
| if (EVP_DigestUpdate(digest, sslMultCertSettings->cert, strlen(setting_cert)) == 0) { |
| SSLError("EVP_DigestUpdate failed using '%s' in hash for session id context", sslMultCertSettings->cert.get()); |
| goto fail; |
| } |
| } |
| |
| if (ca_list != nullptr) { |
| size_t num_certs = sk_X509_NAME_num(ca_list); |
| |
| for (size_t i = 0; i < num_certs; i++) { |
| X509_NAME *name = sk_X509_NAME_value(ca_list, i); |
| if (X509_NAME_digest(name, evp_md_func, hash_buf /* borrow our final hash buffer. */, &hash_len) == 0 || |
| EVP_DigestUpdate(digest, hash_buf, hash_len) == 0) { |
| SSLError("Adding X509 name to digest failed using '%s' in hash for session id context", hash_buf); |
| goto fail; |
| } |
| } |
| |
| // Set the list of CA's to send to client if we ask for a client certificate |
| SSL_CTX_set_client_CA_list(ctx, ca_list); |
| ca_list = nullptr; // ownership transferred to ctx |
| } |
| |
| if (EVP_DigestFinal_ex(digest, hash_buf, &hash_len) == 0) { |
| SSLError("EVP_DigestFinal_ex failed using '%s' in hash for session id context", hash_buf); |
| goto fail; |
| } |
| |
| if (SSL_CTX_set_session_id_context(ctx, hash_buf, hash_len) == 0) { |
| SSLError("SSL_CTX_set_session_id_context failed using '%s' in hash for session id context", hash_buf); |
| goto fail; |
| } |
| |
| result = true; |
| |
| fail: |
| EVP_MD_CTX_free(digest); |
| if (ca_list != nullptr) { |
| sk_X509_NAME_pop_free(ca_list, X509_NAME_free); |
| } |
| |
| return result; |
| } |
| |
| const char * |
| SSLMultiCertConfigLoader::_debug_tag() const |
| { |
| return "ssl"; |
| } |
| |
| const DbgCtl & |
| SSLMultiCertConfigLoader::_dbg_ctl() const |
| { |
| static DbgCtl dc{_debug_tag()}; |
| return dc; |
| } |
| |
| /** |
| Clear password in SSL_CTX |
| @static |
| */ |
| void |
| SSLMultiCertConfigLoader::clear_pw_references(SSL_CTX *ssl_ctx) |
| { |
| Dbg(dbg_ctl_ssl_load, "clearing pw preferences"); |
| SSL_CTX_set_default_passwd_cb(ssl_ctx, nullptr); |
| SSL_CTX_set_default_passwd_cb_userdata(ssl_ctx, nullptr); |
| } |
| |
| ssl_curve_id |
| SSLGetCurveNID(SSL *ssl) |
| { |
| #if HAVE_SSL_GET_SHARED_CURVE |
| return SSL_get_shared_curve(ssl, 0); |
| #else |
| return SSL_get_curve_id(ssl); |
| #endif |
| } |
| |
| std::string_view |
| SSLGetGroupName([[maybe_unused]] SSL *ssl) |
| { |
| #if HAVE_SSL_GET0_GROUP_NAME // OpenSSL 3.2+ |
| char const *group_name = SSL_get0_group_name(ssl); |
| return group_name != nullptr ? std::string_view(group_name) : ""; |
| #elif HAVE_SSL_GET_NEGOTIATED_GROUP && HAVE_SSL_GROUP_TO_NAME // OpenSSL 3.0/3.1 |
| int const group_id = SSL_get_negotiated_group(ssl); |
| if (group_id != NID_undef) { |
| char const *group_name = SSL_group_to_name(ssl, group_id); |
| return group_name != nullptr ? std::string_view(group_name) : ""; |
| } |
| return ""; |
| #elif HAVE_SSL_GET_GROUP_ID && HAVE_SSL_GET_GROUP_NAME // BoringSSL |
| uint16_t const group_id = SSL_get_group_id(ssl); |
| if (group_id == 0) { |
| return ""; |
| } |
| char const *group_name = SSL_get_group_name(group_id); |
| return group_name != nullptr ? std::string_view(group_name) : ""; |
| #else |
| return ""; |
| #endif // HAVE_SSL_GET0_GROUP_NAME |
| } |
| |
| SSL_SESSION * |
| SSLSessionDup(SSL_SESSION *sess) |
| { |
| #ifdef HAVE_SSL_SESSION_DUP |
| return SSL_SESSION_dup(sess); |
| #else |
| SSL_SESSION *duplicated = nullptr; |
| int len = i2d_SSL_SESSION(sess, nullptr); |
| if (len < 0) { |
| return nullptr; |
| } |
| uint8_t *buf = static_cast<uint8_t *>(alloca(len)); |
| uint8_t *tmp = buf; |
| |
| i2d_SSL_SESSION(sess, &tmp); |
| tmp = buf; |
| if (d2i_SSL_SESSION(&duplicated, const_cast<const uint8_t **>(&tmp), len) == nullptr) { |
| return nullptr; |
| } |
| |
| return duplicated; |
| #endif |
| } |