diff --git a/configure.ac b/configure.ac index 137bd2bb389..83239cd25bd 100644 --- a/configure.ac +++ b/configure.ac @@ -837,7 +837,7 @@ case $host_os_def in # #279 is "controlling expression is constant" (which is e.g. TSReleaseAssert(!"Unexpected Event"); common_opt="-pipe -Wall -wd873 -wd279" debug_opt="-g $common_opt" - release_opt="-g $common_opt $optimization_flags -axsse4.2 -fno-strict-aliasing" + release_opt="-g $common_opt $optimizing_flags -axsse4.2 -fno-strict-aliasing" cxx_opt="-Wno-invalid-offsetof" ]) @@ -1302,6 +1302,7 @@ AC_CHECK_FUNCS([ \ HMAC_CTX_new \ X509_get0_signature \ ERR_get_error_all \ + SHA1 \ ]) AC_CHECK_FUNC([ASN1_STRING_get0_data], [], diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index 709653ccdbe..678fb55a191 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -418,7 +418,7 @@ values, such as year, month etc. %{NOW:MONTH} Current month (0-11, 0 == January) %{NOW:DAY} Current day of the month (1-31) %{NOW:HOUR} Current hour (0-23, in the 24h system) - %{NOW:MIN} Current minute (0-59} + %{NOW:MINUTE} Current minute (0-59} %{NOW:WEEKDAY} Current weekday (0-6, 0 == Sunday) %{NOW:YEARDAY} Current day of the year (0-365, 0 == Jan 1st) diff --git a/doc/developer-guide/api/functions/TSLifecycleHookAdd.en.rst b/doc/developer-guide/api/functions/TSLifecycleHookAdd.en.rst index 49e4b33d3d1..8d92190bf45 100644 --- a/doc/developer-guide/api/functions/TSLifecycleHookAdd.en.rst +++ b/doc/developer-guide/api/functions/TSLifecycleHookAdd.en.rst @@ -106,6 +106,14 @@ Types Invoked with the event :c:data:`TS_EVENT_LIFECYCLE_TASK_THREADS_READY` and ``NULL`` data. + .. cpp:enumerator:: TS_LIFECYCLE_SSL_SECRET_HOOK + + Called before the data for the certificate or key is loaded. The data argument to the callback is a pointer to a :type:`TSSecretID` which + contains a pointer to the name of the certificate or key and the relevant version if applicable. + + This hook gives the plugin a chance to load the certificate or key from an alternative source and set via the :c:func:`TSSslSecretSet` API. + If there is no plugin override, the certificate or key will be loaded from disk and the secret name will be interpreted as a file path. + .. cpp:enumerator:: TS_LIFECYCLE_SHUTDOWN_HOOK Called after |TS| receiving a shutdown signal, such as SIGTERM. diff --git a/doc/developer-guide/api/functions/TSSslSecret.en.rst b/doc/developer-guide/api/functions/TSSslSecret.en.rst new file mode 100644 index 00000000000..2de94916ec5 --- /dev/null +++ b/doc/developer-guide/api/functions/TSSslSecret.en.rst @@ -0,0 +1,77 @@ +.. 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:: /common.defs + +.. default-domain:: c + +TSSslSecretSet +************** + +Set the data associated with a secret name specified in the config. + +Synopsis +======== + +.. code-block:: cpp + + #include + +.. function:: TSReturnCode TSSslSecretSet(const char * secret_name, int secret_name_length, const char * secret_data, int secret_data_len) + +Description +=========== + +:func:`TSSslSecretSet` updates the current secret map. Generally the secret name corresponds to the name of a certificate or a key. +Future creation of SSL_CTX objects that use the secret will use the newly specified data. It can be useful to call this function +from the :data:`TS_LIFECYCLE_SSL_SECRET_HOOK`. + +TSSslSecretGet +************** + +Get the data associated with a secret name specified in the config. + +Synopsis +======== + +.. code-block:: cpp + + #include + +.. function:: TSReturnCode TSSslSecretGet(const char * secret_name, int secret_name_length, const char ** secret_data_return, int * secret_data_len) + +Description +=========== + +:func:`TSSslSecretGet` fetches the named secret from the current secret map. TS_ERROR is returned if there is no entry for the secret. + +TSSslSecretUpdate +***************** + +Tell |TS| to update the SSL objects dependent on the secret. + +Synopsis +======== + +.. code-block:: cpp + + #include + +.. function:: TSReturnCode TSSslSecretGet(const char * secret_name, int secret_name_length) + +Description +=========== + +:func:`TSSslSecretUpdate` causes |TS| to update the SSL objects that depend on the specified secret. This enables a plugin to look for +multiple secret updates and make calls to :func:`TSSslSecretSet` to update the secret table. Then once everything is updated call +:func:`TSSslSecretUpdate` to update the SSL objects with a consistent updated set of secrets. diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 3d00933246d..fd745795083 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -437,6 +437,7 @@ typedef enum { TS_LIFECYCLE_CLIENT_SSL_CTX_INITIALIZED_HOOK, TS_LIFECYCLE_MSG_HOOK, TS_LIFECYCLE_TASK_THREADS_READY_HOOK, + TS_LIFECYCLE_SSL_SECRET_HOOK, TS_LIFECYCLE_SHUTDOWN_HOOK, TS_LIFECYCLE_LAST_HOOK } TSLifecycleHookID; @@ -551,6 +552,7 @@ typedef enum { TS_EVENT_SSL_VERIFY_SERVER = 60205, TS_EVENT_SSL_VERIFY_CLIENT = 60206, TS_EVENT_SSL_CLIENT_HELLO = 60207, + TS_EVENT_SSL_SECRET = 60208, TS_EVENT_MGMT_UPDATE = 60300 } TSEvent; @@ -1031,6 +1033,13 @@ typedef struct TSSslSessionID_s { char bytes[TS_SSL_MAX_SSL_SESSION_ID_LENGTH]; } TSSslSessionID; +typedef struct TSSecretID_s { + const char *cert_name; + size_t cert_name_len; + const char *key_name; + size_t key_name_len; +} TSSecretID; + /* -------------------------------------------------------------------------- Init */ diff --git a/include/ts/ts.h b/include/ts/ts.h index 61e7d45e251..9ffc19b835e 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1300,6 +1300,13 @@ tsapi TSSslContext TSSslClientContextFindByName(const char *ca_paths, const char tsapi TSReturnCode TSSslClientCertUpdate(const char *cert_path, const char *key_path); tsapi TSReturnCode TSSslServerCertUpdate(const char *cert_path, const char *key_path); +/* Update the transient secret table for SSL_CTX loading */ +tsapi TSReturnCode TSSslSecretSet(const char *secret_name, int secret_name_length, const char *secret_data, int secret_data_len); +tsapi TSReturnCode TSSslSecretGet(const char *secret_name, int secret_name_length, const char **secret_data_return, + int *secret_data_len); + +tsapi TSReturnCode TSSslSecretUpdate(const char *secret_name, int secret_name_length); + /* Create a new SSL context based on the settings in records.config */ tsapi TSSslContext TSSslServerContextCreate(TSSslX509 cert, const char *certname, const char *rsp_file); tsapi void TSSslContextDestroy(TSSslContext ctx); diff --git a/iocore/net/Makefile.am b/iocore/net/Makefile.am index 85305f0dfb9..1b97bb87d6b 100644 --- a/iocore/net/Makefile.am +++ b/iocore/net/Makefile.am @@ -143,6 +143,7 @@ libinknet_a_SOURCES = \ P_Socks.h \ P_SSLCertLookup.h \ P_SSLConfig.h \ + P_SSLSecret.h \ P_SSLNetAccept.h \ P_SSLNetProcessor.h \ P_SSLNetVConnection.h \ @@ -171,6 +172,7 @@ libinknet_a_SOURCES = \ SSLClientCoordinator.cc \ SSLClientUtils.cc \ SSLConfig.cc \ + SSLSecret.cc \ SSLDiags.cc \ SSLInternal.cc \ SSLNetAccept.cc \ diff --git a/iocore/net/P_QUICNetVConnection.h b/iocore/net/P_QUICNetVConnection.h index eb9f4f59e8d..2b2a7e23830 100644 --- a/iocore/net/P_QUICNetVConnection.h +++ b/iocore/net/P_QUICNetVConnection.h @@ -38,6 +38,7 @@ #include "P_UnixNet.h" #include "P_UDPNet.h" #include "P_ALPNSupport.h" +#include "TLSBasicSupport.h" #include "TLSSessionResumptionSupport.h" #include "tscore/ink_apidefs.h" #include "tscore/List.h" @@ -140,6 +141,7 @@ class QUICNetVConnection : public UnixNetVConnection, public QUICConnection, public RefCountObj, public ALPNSupport, + public TLSBasicSupport, public TLSSessionResumptionSupport { using super = UnixNetVConnection; ///< Parent type. @@ -223,6 +225,11 @@ class QUICNetVConnection : public UnixNetVConnection, SLINK(QUICNetVConnection, closed_alink); protected: + // TLSBasicSupport + SSL *_get_ssl_object() const override; + ssl_curve_id _get_tls_curve() const override; + + // TLSSessionResumptionSupport const IpEndpoint &_getLocalEndpoint() override; private: diff --git a/iocore/net/P_SSLCertLookup.h b/iocore/net/P_SSLCertLookup.h index 7ee0f2ab54c..6d03f70134f 100644 --- a/iocore/net/P_SSLCertLookup.h +++ b/iocore/net/P_SSLCertLookup.h @@ -23,6 +23,7 @@ #pragma once +#include #include #include "ProxyConfig.h" @@ -140,7 +141,7 @@ struct SSLCertLookup : public ConfigInfo { Exact matches have priority, then wildcards. Only destination based matches are checked. @return @c A pointer to the matched context, @c nullptr if no match is found. */ - SSLCertContext *find(const char *name) const; + SSLCertContext *find(const std::string &name) const; // Return the last-resort default TLS context if there is no name or address match. SSL_CTX * @@ -152,8 +153,15 @@ struct SSLCertLookup : public ConfigInfo { unsigned count() const; SSLCertContext *get(unsigned i) const; + void register_cert_secrets(std::vector const &cert_secrets, std::set &lookup_names); + void getPolicies(const std::string &secret_name, std::set &policies) const; + SSLCertLookup(); ~SSLCertLookup() override; + +private: + // Map cert_secret name to lookup keys + std::unordered_map> cert_secret_registry; }; void ticket_block_free(void *ptr); diff --git a/iocore/net/P_SSLConfig.h b/iocore/net/P_SSLConfig.h index 0ecab2de7dc..262df7c9c58 100644 --- a/iocore/net/P_SSLConfig.h +++ b/iocore/net/P_SSLConfig.h @@ -41,6 +41,7 @@ #include "YamlSNIConfig.h" #include "P_SSLUtils.h" +#include "P_SSLSecret.h" struct SSLCertLookup; struct ssl_ticket_key_block; @@ -144,9 +145,16 @@ struct SSLConfigParams : public ConfigInfo { mutable std::unordered_map top_level_ctx_map; mutable ink_mutex ctxMapLock; + mutable SSLSecret secrets; + shared_SSL_CTX getClientSSL_CTX() const; + shared_SSL_CTX getCTX(const std::string &client_cert, const std::string &key_file, const char *ca_bundle_file, + const char *ca_bundle_path) const; shared_SSL_CTX getCTX(const char *client_cert, const char *key_file, const char *ca_bundle_file, const char *ca_bundle_path) const; + void updateCTX(const std::string &secret_string_name) const; + + void clearCTX(const std::string &client_cert) const; void cleanupCTXTable(); @@ -166,11 +174,22 @@ struct SSLConfig { static void startup(); static void reconfigure(); static SSLConfigParams *acquire(); + static SSLConfigParams *load_acquire(); static void release(SSLConfigParams *params); + static void load_release(SSLConfigParams *params); + + // These methods manipulate the double buffering of the configs + // The "loading" version is only active during loading. Once + // it is fliped to the active by comit_config_id, it/ becomes the + // version accessble to the rest of the system. + static int get_config_index(); + static int get_loading_config_index(); + static void commit_config_id(); typedef ConfigProcessor::scoped_config scoped_config; private: - static int configid; + static int config_index; + static int configids[2]; }; struct SSLCertificateConfig { diff --git a/iocore/net/P_SSLSecret.h b/iocore/net/P_SSLSecret.h new file mode 100644 index 00000000000..d873efb1faa --- /dev/null +++ b/iocore/net/P_SSLSecret.h @@ -0,0 +1,36 @@ +/** @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. + */ + +class SSLSecret +{ +public: + SSLSecret() {} + bool getSecret(const std::string &name, std::string_view &data) const; + bool setSecret(const std::string &name, const char *data, int data_len); + bool getOrLoadSecret(const std::string &name, const std::string &name2, std::string_view &data, std::string_view &data2); + +private: + const std::string *getSecretItem(const std::string &name) const; + bool loadSecret(const std::string &name, const std::string &name2, std::string &data_item, std::string &data_item2); + bool loadFile(const std::string &name, std::string &data_item); + + std::unordered_map secret_map; +}; diff --git a/iocore/net/P_SSLUtils.h b/iocore/net/P_SSLUtils.h index 1d876946441..21ec6b6ae11 100644 --- a/iocore/net/P_SSLUtils.h +++ b/iocore/net/P_SSLUtils.h @@ -78,10 +78,11 @@ class SSLMultiCertConfigLoader static bool set_session_id_context(SSL_CTX *ctx, const SSLConfigParams *params, const SSLMultiCertConfigParams *sslMultCertSettings); - static bool index_certificate(SSLCertLookup *lookup, SSLCertContext const &cc, const char *sni_name); static int check_server_cert_now(X509 *cert, const char *certname); static void clear_pw_references(SSL_CTX *ssl_ctx); + bool update_ssl_ctx(const std::string &secret_name); + protected: const SSLConfigParams *_params; @@ -90,7 +91,9 @@ class SSLMultiCertConfigLoader private: virtual const char *_debug_tag() const; - bool _store_ssl_ctx(SSLCertLookup *lookup, const shared_SSLMultiCertConfigParams &ssl_multi_cert_params); + virtual bool _store_ssl_ctx(SSLCertLookup *lookup, shared_SSLMultiCertConfigParams ssl_multi_cert_params); + bool _prep_ssl_ctx(const shared_SSLMultiCertConfigParams sslMultCertSettings, SSLMultiCertConfigLoader::CertLoadData &data, + std::set &common_names, std::unordered_map> &unique_names); virtual void _set_handshake_callbacks(SSL_CTX *ctx); virtual bool _setup_session_cache(SSL_CTX *ctx); virtual bool _setup_dialog(SSL_CTX *ctx, const SSLMultiCertConfigParams *sslMultCertSettings); diff --git a/iocore/net/QUICNetVConnection.cc b/iocore/net/QUICNetVConnection.cc index 0264fd39a79..34b90f7ce6c 100644 --- a/iocore/net/QUICNetVConnection.cc +++ b/iocore/net/QUICNetVConnection.cc @@ -417,6 +417,7 @@ QUICNetVConnection::start() this->_ack_frame_manager.set_ack_delay_exponent(this->_quic_config->ack_delay_exponent_out()); this->_hs_protocol = this->_setup_handshake_protocol(this->_quic_config->client_ssl_ctx()); this->_handshake_handler = new QUICHandshake(this->_initial_version, this, this->_hs_protocol); + this->_record_tls_handshake_begin_time(); this->_handshake_handler->start(tp_config, &this->_packet_factory, this->_quic_config->vn_exercise_enabled()); this->_handshake_handler->do_handshake(); this->_ack_frame_manager.set_max_ack_delay(this->_quic_config->max_ack_delay_out()); @@ -495,6 +496,8 @@ QUICNetVConnection::free(EThread *t) */ this->_context->trigger(QUICContext::CallbackEvent::CONNECTION_CLOSE); ALPNSupport::clear(); + TLSSessionResumptionSupport::clear(); + TLSBasicSupport::clear(); this->_packet_handler->close_connection(this); } @@ -1222,6 +1225,7 @@ QUICNetVConnection::_state_handshake_process_initial_packet(const QUICInitialPac if (this->_quic_config->quantum_readiness_test_enabled_in()) { tp_config.add_tp(QUANTUM_TEST_ID, QUANTUM_TEST_VALUE, sizeof(QUANTUM_TEST_VALUE)); } + this->_record_tls_handshake_begin_time(); error = this->_handshake_handler->start(tp_config, packet, &this->_packet_factory, this->_alt_con_manager->preferred_address()); // If version negotiation was failed and VERSION NEGOTIATION packet was sent, nothing to do. @@ -2118,6 +2122,7 @@ QUICNetVConnection::_switch_to_established_state() if (this->_complete_handshake_if_possible() == 0) { QUICConDebug("Enter state_connection_established"); QUICConDebug("Negotiated cipher suite: %s", this->_handshake_handler->negotiated_cipher_suite()); + this->_record_tls_handshake_end_time(); SET_HANDLER((NetVConnHandler)&QUICNetVConnection::state_connection_established); @@ -2296,6 +2301,7 @@ QUICNetVConnection::_setup_handshake_protocol(const shared_SSL_CTX &ctx) QUICTLS *tls = new QUICTLS(this->_pp_key_info, ctx.get(), this->direction(), this->options, this->_quic_config->client_session_file(), this->_quic_config->client_keylog_file()); SSL_set_ex_data(tls->ssl_handle(), QUIC::ssl_quic_qc_index, static_cast(this)); + TLSBasicSupport::bind(tls->ssl_handle(), this); TLSSessionResumptionSupport::bind(tls->ssl_handle(), this); ALPNSupport::bind(tls->ssl_handle(), this); @@ -2412,6 +2418,22 @@ QUICNetVConnection::_handle_periodic_ack_event() } } +SSL * +QUICNetVConnection::_get_ssl_object() const +{ + return static_cast(this->_hs_protocol)->ssl_handle(); +} + +ssl_curve_id +QUICNetVConnection::_get_tls_curve() const +{ + if (this->getSSLSessionCacheHit()) { + return this->getSSLCurveNID(); + } else { + return SSLGetCurveNID(this->_get_ssl_object()); + } +} + const IpEndpoint & QUICNetVConnection::_getLocalEndpoint() { diff --git a/iocore/net/SSLCertLookup.cc b/iocore/net/SSLCertLookup.cc index 9b887e51f4c..69c668005e4 100644 --- a/iocore/net/SSLCertLookup.cc +++ b/iocore/net/SSLCertLookup.cc @@ -96,7 +96,7 @@ struct SSLContextStorage { /// cert context. /// @return @a idx int insert(const char *name, int idx); - SSLCertContext *lookup(const char *name); + SSLCertContext *lookup(const std::string &name); void printWildDomains() const; unsigned count() const @@ -292,7 +292,7 @@ SSLCertLookup::~SSLCertLookup() } SSLCertContext * -SSLCertLookup::find(const char *address) const +SSLCertLookup::find(const std::string &address) const { return this->ssl_storage->lookup(address); } @@ -342,6 +342,34 @@ SSLCertLookup::get(unsigned i) const return ssl_storage->get(i); } +void +SSLCertLookup::register_cert_secrets(std::vector const &cert_secrets, std::set &lookup_names) +{ + for (auto &secret : cert_secrets) { + auto iter = cert_secret_registry.find(secret); + if (iter == cert_secret_registry.end()) { + std::vector add_names; + cert_secret_registry.insert(std::make_pair(secret, add_names)); + iter = cert_secret_registry.find(secret); + } + iter->second.insert(iter->second.end(), lookup_names.begin(), lookup_names.end()); + } +} + +void +SSLCertLookup::getPolicies(const std::string &secret_name, std::set &policies) const +{ + auto iter = cert_secret_registry.find(secret_name); + if (iter != cert_secret_registry.end()) { + for (auto name : iter->second) { + SSLCertContext *cc = this->find(name); + if (cc) { + policies.insert(cc->userconfig); + } + } + } +} + SSLContextStorage::SSLContextStorage() {} SSLContextStorage::~SSLContextStorage() {} @@ -412,7 +440,7 @@ SSLContextStorage::printWildDomains() const } SSLCertContext * -SSLContextStorage::lookup(const char *name) +SSLContextStorage::lookup(const std::string &name) { // First look for an exact name match if (auto it = this->hostnames.find(name); it != this->hostnames.end()) { diff --git a/iocore/net/SSLConfig.cc b/iocore/net/SSLConfig.cc index 2af43bcdfb4..0098e5132f4 100644 --- a/iocore/net/SSLConfig.cc +++ b/iocore/net/SSLConfig.cc @@ -35,6 +35,10 @@ #include #include "tscore/ink_config.h" +#include + +#include "InkAPIInternal.h" // Added to include the ssl_hook and lifestyle_hook definitions + #include "tscore/ink_platform.h" #include "tscore/I_Layout.h" #include "records/I_RecHttp.h" @@ -43,6 +47,7 @@ #include "P_Net.h" #include "P_SSLClientUtils.h" +#include "P_SSLSNI.h" #include "P_SSLCertLookup.h" #include "P_SSLSNI.h" #include "SSLDiags.h" @@ -50,7 +55,8 @@ #include "SSLSessionTicket.h" #include "YamlSNIConfig.h" -int SSLConfig::configid = 0; +int SSLConfig::config_index = 0; +int SSLConfig::configids[] = {0, 0}; int SSLCertificateConfig::configid = 0; int SSLTicketKeyConfig::configid = 0; int SSLConfigParams::ssl_maxrecord = 0; @@ -437,6 +443,30 @@ SSLConfigParams::getClientSSL_CTX() const return client_ctx; } +int +SSLConfig::get_config_index() +{ + return config_index; +} + +int +SSLConfig::get_loading_config_index() +{ + return config_index == 0 ? 1 : 0; +} + +void +SSLConfig::commit_config_id() +{ + // Update the active config index + config_index = get_loading_config_index(); + + if (configids[get_loading_config_index()] != 0) { + // Start draining to free the old config + configProcessor.set(configids[get_loading_config_index()], nullptr); + } +} + void SSLConfig::startup() { @@ -449,20 +479,36 @@ SSLConfig::reconfigure() Debug("ssl", "Reload SSLConfig"); SSLConfigParams *params; params = new SSLConfigParams; + // start loading the next config + int loading_config_index = get_loading_config_index(); + configids[loading_config_index] = configProcessor.set(configids[loading_config_index], params); params->initialize(); // re-read configuration - configid = configProcessor.set(configid, params); + // Make the new config avaiable for use. + commit_config_id(); } SSLConfigParams * SSLConfig::acquire() { - return static_cast(configProcessor.get(configid)); + return static_cast(configProcessor.get(configids[get_config_index()])); +} + +SSLConfigParams * +SSLConfig::load_acquire() +{ + return static_cast(configProcessor.get(configids[get_loading_config_index()])); } void SSLConfig::release(SSLConfigParams *params) { - configProcessor.release(configid, params); + configProcessor.release(configids[get_config_index()], params); +} + +void +SSLConfig::load_release(SSLConfigParams *params) +{ + configProcessor.release(configids[get_loading_config_index()], params); } bool @@ -657,16 +703,62 @@ SSLTicketParams::cleanup() ticket_key_filename = static_cast(ats_free_null(ticket_key_filename)); } +void +cleanup_bio(BIO *&biop) +{ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-value" + BIO_set_close(biop, BIO_NOCLOSE); +#pragma GCC diagnostic pop + BIO_free(biop); + biop = nullptr; +} + +void +SSLConfigParams::updateCTX(const std::string &cert_secret_name) const +{ + // Clear the corresponding client CTX's. They will be lazy loaded later + Debug("ssl", "Update cert %s", cert_secret_name.c_str()); + this->clearCTX(cert_secret_name); + + // Update the server cert + SSLMultiCertConfigLoader loader(this); + loader.update_ssl_ctx(cert_secret_name); +} + +void +SSLConfigParams::clearCTX(const std::string &client_cert) const +{ + ink_mutex_acquire(&ctxMapLock); + for (auto ctx_map_iter = top_level_ctx_map.begin(); ctx_map_iter != top_level_ctx_map.end(); ++ctx_map_iter) { + auto ctx_iter = ctx_map_iter->second.find(client_cert); + if (ctx_iter != ctx_map_iter->second.end()) { + ctx_iter->second = nullptr; + Debug("ssl", "Clear client cert %s %s", ctx_map_iter->first.c_str(), ctx_iter->first.c_str()); + } + } + ink_mutex_release(&ctxMapLock); +} + shared_SSL_CTX SSLConfigParams::getCTX(const char *client_cert, const char *key_file, const char *ca_bundle_file, const char *ca_bundle_path) const +{ + return this->getCTX(std::string(client_cert ? client_cert : ""), std::string(key_file ? key_file : ""), ca_bundle_file, + ca_bundle_path); +} + +shared_SSL_CTX +SSLConfigParams::getCTX(const std::string &client_cert, const std::string &key_file, const char *ca_bundle_file, + const char *ca_bundle_path) const { shared_SSL_CTX client_ctx = nullptr; std::string top_level_key, ctx_key; + ctx_key = client_cert; ts::bwprint(top_level_key, "{}:{}", ca_bundle_file, ca_bundle_path); - ts::bwprint(ctx_key, "{}:{}", client_cert, key_file); - Debug("ssl", "Load client cert %s %s", client_cert, key_file); + Debug("ssl", "Look for client cert %s %s", top_level_key.c_str(), ctx_key.c_str()); + ink_mutex_acquire(&ctxMapLock); auto ctx_map_iter = top_level_ctx_map.find(top_level_key); if (ctx_map_iter != top_level_ctx_map.end()) { auto ctx_iter = ctx_map_iter->second.find(ctx_key); @@ -674,29 +766,84 @@ SSLConfigParams::getCTX(const char *client_cert, const char *key_file, const cha client_ctx = ctx_iter->second; } } + ink_mutex_release(&ctxMapLock); + BIO *biop = nullptr; + X509 *cert = nullptr; + EVP_PKEY *key = nullptr; // Create context if doesn't exists if (!client_ctx) { + Debug("ssl", "Load new cert for %s %s", top_level_key.c_str(), ctx_key.c_str()); client_ctx = shared_SSL_CTX(SSLInitClientContext(this), SSLReleaseContext); - if (client_cert) { - // Set public and private keys - if (!SSL_CTX_use_certificate_chain_file(client_ctx.get(), client_cert)) { - SSLError("failed to load client certificate from %s", client_cert); + // Set public and private keys + if (!client_cert.empty()) { + std::string_view secret_data; + std::string_view secret_key_data; + + // Fetch the client_cert data + std::string completeSecretPath{Layout::get()->relative_to(this->clientCertPathOnly, client_cert)}; + std::string completeKeySecretPath{!key_file.empty() ? Layout::get()->relative_to(this->clientKeyPathOnly, key_file) : ""}; + secrets.getOrLoadSecret(completeSecretPath, completeKeySecretPath, secret_data, secret_key_data); + if (secret_data.empty()) { + SSLError("failed to access cert %s", client_cert.c_str()); goto fail; } - if (!key_file || key_file[0] == '\0') { - key_file = client_cert; + + biop = BIO_new_mem_buf(secret_data.data(), secret_data.size()); + + cert = PEM_read_bio_X509(biop, NULL, 0, NULL); + if (!cert) { + SSLError("failed to load cert %s", client_cert.c_str()); + goto fail; } - if (!SSL_CTX_use_PrivateKey_file(client_ctx.get(), key_file, SSL_FILETYPE_PEM)) { - SSLError("failed to load client private key file from %s", key_file); + if (!SSL_CTX_use_certificate(client_ctx.get(), cert)) { + SSLError("failed to attach client certificate from %s", client_cert.c_str()); goto fail; } + X509_free(cert); + + // Continue to fetch certs to associate intermediate certificates + cert = PEM_read_bio_X509(biop, NULL, 0, NULL); + while (cert) { + if (!SSL_CTX_use_certificate(client_ctx.get(), cert)) { + SSLError("failed to attach client chain certificate from %s", client_cert.c_str()); + goto fail; + } + X509_free(cert); + cert = PEM_read_bio_X509(biop, NULL, 0, NULL); + } + + cleanup_bio(biop); + + const std::string &key_file_name = (secret_key_data.empty()) ? client_cert : key_file; + + // If there is a separate key file, fetch the new content + // otherwise, continue on with the cert data and hope for the best + if (!secret_key_data.empty()) { + biop = BIO_new_mem_buf(secret_key_data.data(), secret_key_data.size()); + } else { + biop = BIO_new_mem_buf(secret_data.data(), secret_data.size()); + } + + key = PEM_read_bio_PrivateKey(biop, NULL, 0, NULL); + if (!key) { + SSLError("failed to load client private key file from %s", key_file_name.c_str()); + goto fail; + } + if (!SSL_CTX_use_PrivateKey(client_ctx.get(), key)) { + SSLError("failed to use client private key file from %s", key_file_name.c_str()); + goto fail; + } + EVP_PKEY_free(key); + key = nullptr; if (!SSL_CTX_check_private_key(client_ctx.get())) { - SSLError("client private key (%s) does not match the certificate public key (%s)", key_file, client_cert); + SSLError("client private key (%s) does not match the certificate public key (%s)", key_file_name.c_str(), + client_cert.c_str()); goto fail; } + cleanup_bio(biop); } // Set CA information for verifying peer cert @@ -723,6 +870,15 @@ SSLConfigParams::getCTX(const char *client_cert, const char *key_file, const cha return client_ctx; fail: + if (biop) { + cleanup_bio(biop); + } + if (cert) { + X509_free(cert); + } + if (key) { + EVP_PKEY_free(key); + } return nullptr; } diff --git a/iocore/net/SSLNetVConnection.cc b/iocore/net/SSLNetVConnection.cc index 02339f24cf3..d20c4aab6ab 100644 --- a/iocore/net/SSLNetVConnection.cc +++ b/iocore/net/SSLNetVConnection.cc @@ -1099,16 +1099,15 @@ SSLNetVConnection::sslStartHandShake(int event, int &err) caCertFilePath = Layout::get()->relative_to(params->clientCACertPath, options.ssl_client_ca_cert_name); } sharedCTX = - params->getCTX(certFilePath.c_str(), keyFilePath.empty() ? nullptr : keyFilePath.c_str(), - caCertFilePath.empty() ? params->clientCACertFilename : caCertFilePath.c_str(), params->clientCACertPath); + params->getCTX(certFilePath, keyFilePath, caCertFilePath.empty() ? params->clientCACertFilename : caCertFilePath.c_str(), + params->clientCACertPath); } else if (options.ssl_client_ca_cert_name) { std::string caCertFilePath = Layout::get()->relative_to(params->clientCACertPath, options.ssl_client_ca_cert_name); sharedCTX = params->getCTX(params->clientCertPath, params->clientKeyPath, caCertFilePath.c_str(), params->clientCACertPath); } else if (nps && !nps->client_cert_file.empty()) { // If no overrides available, try the available nextHopProperty by reading from context mappings sharedCTX = - params->getCTX(nps->client_cert_file.c_str(), nps->client_key_file.empty() ? nullptr : nps->client_key_file.c_str(), - params->clientCACertFilename, params->clientCACertPath); + params->getCTX(nps->client_cert_file, nps->client_key_file, params->clientCACertFilename, params->clientCACertPath); } else { // Just stay with the values passed down from the SM for verify clientCTX = params->client_ctx.get(); } @@ -1577,6 +1576,11 @@ SSLNetVConnection::reenable(NetHandler *nh, int event) { Debug("ssl", "Handshake reenable from state=%d", sslHandshakeHookState); + // Mark as error to stop the Handshake + if (event == TS_EVENT_ERROR) { + sslHandshakeStatus = SSL_HANDSHAKE_ERROR; + } + switch (sslHandshakeHookState) { case HANDSHAKE_HOOKS_PRE_INVOKE: sslHandshakeHookState = HANDSHAKE_HOOKS_PRE; @@ -1592,9 +1596,6 @@ SSLNetVConnection::reenable(NetHandler *nh, int event) break; case HANDSHAKE_HOOKS_VERIFY_SERVER: case HANDSHAKE_HOOKS_CLIENT_CERT: - if (event == TS_EVENT_ERROR) { - sslHandshakeStatus = SSL_HANDSHAKE_ERROR; - } break; default: break; diff --git a/iocore/net/SSLSNIConfig.cc b/iocore/net/SSLSNIConfig.cc index ee92eb04635..f6c10ca3135 100644 --- a/iocore/net/SSLSNIConfig.cc +++ b/iocore/net/SSLSNIConfig.cc @@ -96,9 +96,7 @@ SNIConfigParams::loadSNIConfig() nps->prop.client_key_file = Layout::get()->relative_to(params->clientKeyPathOnly, item.client_key.data()); } - params->getCTX(nps->prop.client_cert_file.c_str(), - nps->prop.client_key_file.empty() ? nullptr : nps->prop.client_key_file.c_str(), params->clientCACertFilename, - params->clientCACertPath); + params->getCTX(nps->prop.client_cert_file, nps->prop.client_key_file, params->clientCACertFilename, params->clientCACertPath); } nps->setGlobName(item.fqdn); diff --git a/iocore/net/SSLSecret.cc b/iocore/net/SSLSecret.cc new file mode 100644 index 00000000000..ebcd355a900 --- /dev/null +++ b/iocore/net/SSLSecret.cc @@ -0,0 +1,139 @@ +/** @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 +#include +#include "InkAPIInternal.h" // Added to include the ssl_hook and lifestyle_hook definitions +#include "tscore/ts_file.h" +#include "P_SSLConfig.h" + +bool +SSLSecret::loadSecret(const std::string &name1, const std::string &name2, std::string &data_item1, std::string &data_item2) +{ + // Call the load secret hooks + // + class APIHook *curHook = lifecycle_hooks->get(TS_LIFECYCLE_SSL_SECRET_HOOK); + TSSecretID secret_name; + secret_name.cert_name = name1.data(); + secret_name.cert_name_len = name1.size(); + secret_name.key_name = name2.data(); + secret_name.key_name_len = name2.size(); + while (curHook) { + curHook->invoke(TS_EVENT_SSL_SECRET, &secret_name); + curHook = curHook->next(); + } + + const std::string *data1 = this->getSecretItem(name1); + const std::string *data2 = this->getSecretItem(name2); + if ((nullptr == data1 || data1->length() == 0) || (!name2.empty() && (nullptr == data2 || data2->length() == 0))) { + // If none of them loaded it, assume it is a file + return loadFile(name1, data_item1) && (name2.empty() || loadFile(name2, data_item2)); + } + return true; +} + +bool +SSLSecret::loadFile(const std::string &name, std::string &data_item) +{ + struct stat statdata; + // Load the secret and add it to the map + if (stat(name.c_str(), &statdata) < 0) { + return false; + } + std::error_code error; + data_item = ts::file::load(ts::file::path(name), error); + if (error) { + // Loading file failed + return false; + } + if (SSLConfigParams::load_ssl_file_cb) { + SSLConfigParams::load_ssl_file_cb(name.c_str()); + } + return true; +} + +bool +SSLSecret::setSecret(const std::string &name, const char *data, int data_len) +{ + auto iter = secret_map.find(name); + if (iter == secret_map.end()) { + secret_map[name] = ""; + iter = secret_map.find(name); + } + if (iter == secret_map.end()) { + return false; + } + iter->second.assign(data, data_len); + Debug("ssl_secret", "Set secret=%10.s... to %*.s", name.c_str(), static_cast(iter->second.size()), iter->second.data()); + return true; +} + +const std::string * +SSLSecret::getSecretItem(const std::string &name) const +{ + auto iter = secret_map.find(name); + if (iter == secret_map.end()) { + return nullptr; + } + return &iter->second; +} + +bool +SSLSecret::getSecret(const std::string &name, std::string_view &data) const +{ + const std::string *data_item = this->getSecretItem(name); + if (data_item) { + Debug("ssl_secret", "Get secret=%10.s... %s(%zd)", name.c_str(), data_item->data(), data_item->length()); + data = *data_item; + } else { + data = std::string_view{}; + } + return data_item != nullptr; +} + +bool +SSLSecret::getOrLoadSecret(const std::string &name1, const std::string &name2, std::string_view &data1, std::string_view &data2) +{ + bool found_secret1 = this->getSecret(name1, data1); + bool found_secret2 = name2.empty() || this->getSecret(name2, data2); + + // If we can't find either secret, load the both again + if (!found_secret1 || !found_secret2) { + // Make sure each name has an entry + if (!found_secret1) { + secret_map[name1] = ""; + } + if (!found_secret2) { + secret_map[name2] = ""; + } + auto iter1 = secret_map.find(name1); + auto iter2 = name2.empty() ? iter1 : secret_map.find(name2); + if (this->loadSecret(name1, name2, iter1->second, iter2->second)) { + data1 = iter1->second; + if (!name2.empty()) { + data2 = iter2->second; + } + return true; + } + } else { + return true; + } + return false; +} diff --git a/iocore/net/SSLUtils.cc b/iocore/net/SSLUtils.cc index 4a190d00b9e..054432f12a1 100644 --- a/iocore/net/SSLUtils.cc +++ b/iocore/net/SSLUtils.cc @@ -935,48 +935,35 @@ SSLMultiCertConfigLoader::default_server_ssl_ctx() } static bool -SSLPrivateKeyHandler(SSL_CTX *ctx, const SSLConfigParams *params, const std::string &completeServerCertPath, const char *keyPath) +SSLPrivateKeyHandler(SSL_CTX *ctx, const SSLConfigParams *params, const char *keyPath, const char *secret_data, int secret_data_len) { EVP_PKEY *pkey = nullptr; #ifndef OPENSSL_IS_BORINGSSL ENGINE *e = ENGINE_get_default_RSA(); if (e != nullptr) { - ats_scoped_str argkey; - if (keyPath == nullptr || keyPath[0] == '\0') { - argkey = completeServerCertPath.c_str(); - } else { - argkey = Layout::get()->relative_to(params->serverKeyPathOnly, keyPath); - } - pkey = ENGINE_load_private_key(e, argkey.get(), nullptr, nullptr); + pkey = ENGINE_load_private_key(e, keyPath, nullptr, nullptr); if (pkey) { if (!SSL_CTX_use_PrivateKey(ctx, pkey)) { SSLError("failed to load server private key from engine"); + EVP_PKEY_free(pkey); return false; } } } #endif if (pkey == nullptr) { - if (!keyPath || keyPath[0] == '\0') { - // assume private key is contained in cert obtained from multicert file. - if (!SSL_CTX_use_PrivateKey_file(ctx, completeServerCertPath.c_str(), SSL_FILETYPE_PEM)) { - SSLError("failed to load server private key from %s", completeServerCertPath.c_str()); - return false; - } - } else if (params->serverKeyPathOnly != nullptr) { - ats_scoped_str completeServerKeyPath(Layout::get()->relative_to(params->serverKeyPathOnly, keyPath)); - if (!SSL_CTX_use_PrivateKey_file(ctx, completeServerKeyPath, SSL_FILETYPE_PEM)) { - SSLError("failed to load server private key from %s", (const char *)completeServerKeyPath); - return false; - } - if (SSLConfigParams::load_ssl_file_cb) { - SSLConfigParams::load_ssl_file_cb(completeServerKeyPath); - } - } else { - SSLError("empty SSL private key path in %s", ts::filename::RECORDS); + scoped_BIO bio(BIO_new_mem_buf(secret_data, secret_data_len)); + pkey = PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, nullptr); + if (nullptr == pkey) { + SSLError("failed to load server private key from %s", keyPath); return false; } - if (!SSL_CTX_check_private_key(ctx)) { + if (!SSL_CTX_use_PrivateKey(ctx, pkey)) { + SSLError("failed to attache server private key loaded from %s", keyPath); + EVP_PKEY_free(pkey); + return false; + } + if (e == nullptr && !SSL_CTX_check_private_key(ctx)) { SSLError("server private key does not match the certificate public key"); return false; } @@ -1046,25 +1033,6 @@ asn1_strdup(ASN1_STRING *s) return ats_strndup((const char *)ASN1_STRING_get0_data(s), ASN1_STRING_length(s)); } -/** - Given a certificate and it's corresponding SSL_CTX context, insert hash - table aliases for subject CN and subjectAltNames DNS without wildcard, - insert trie aliases for those with wildcard. - @static -*/ -bool -SSLMultiCertConfigLoader::index_certificate(SSLCertLookup *lookup, SSLCertContext const &cc, const char *sni_name) -{ - bool inserted = false; - - Debug("ssl", "mapping '%s'", sni_name); - if (lookup->insert(sni_name, cc) >= 0) { - inserted = true; - } - - return inserted; -} - // 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 @@ -1531,44 +1499,68 @@ SSLCreateServerContext(const SSLConfigParams *params, const SSLMultiCertConfigPa } /** - Insert SSLCertContext (SSL_CTX ans 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(). + * Common name resolution and cert validation */ bool -SSLMultiCertConfigLoader::_store_ssl_ctx(SSLCertLookup *lookup, const shared_SSLMultiCertConfigParams &sslMultCertSettings) +SSLMultiCertConfigLoader::_prep_ssl_ctx(const shared_SSLMultiCertConfigParams sslMultCertSettings, + SSLMultiCertConfigLoader::CertLoadData &data, std::set &common_names, + std::unordered_map> &unique_names) { - bool retval = true; std::vector cert_list; - std::set common_names; - std::unordered_map> unique_names; - SSLMultiCertConfigLoader::CertLoadData data; - const SSLConfigParams *params = this->_params; this->load_certs_and_cross_reference_names(cert_list, data, params, sslMultCertSettings.get(), common_names, unique_names); - int i = 0; + int i = 0; + bool good_certs = true; for (auto 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 */ Debug(this->_debug_tag(), "Marking certificate as NOT VALID: %s", current_cert_name); - lookup->is_valid = false; + good_certs = false; } i++; } - shared_SSL_CTX ctx(this->init_server_ssl_ctx(data, sslMultCertSettings.get(), common_names), SSL_CTX_free); + for (auto &cert : cert_list) { + X509_free(cert); + } + return good_certs; +} - if (!ctx || !sslMultCertSettings || !this->_store_single_ssl_ctx(lookup, sslMultCertSettings, ctx, common_names)) { - retval = false; - std::string names; - for (auto name : data.cert_names_list) { - names.append(name); - names.append(" "); +/** + Insert SSLCertContext (SSL_CTX ans 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 common_names; + std::unordered_map> unique_names; + SSLMultiCertConfigLoader::CertLoadData data; + + if (!this->_prep_ssl_ctx(sslMultCertSettings, data, common_names, unique_names)) { + lookup->is_valid = false; + return false; + } + + if (!common_names.empty()) { + shared_SSL_CTX ctx(this->init_server_ssl_ctx(data, sslMultCertSettings.get(), common_names), SSL_CTX_free); + + if (!ctx || !sslMultCertSettings || !this->_store_single_ssl_ctx(lookup, sslMultCertSettings, ctx, common_names)) { + retval = false; + std::string names; + for (auto 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 { + lookup->register_cert_secrets(data.cert_names_list, common_names); } - Warning("(%s) Failed to insert SSL_CTX for certificate %s entries for names already made", this->_debug_tag(), names.c_str()); } for (auto iter = unique_names.begin(); retval && iter != unique_names.end(); ++iter) { @@ -1585,13 +1577,74 @@ SSLMultiCertConfigLoader::_store_ssl_ctx(SSLCertLookup *lookup, const shared_SSL shared_SSL_CTX unique_ctx(this->init_server_ssl_ctx(single_data, sslMultCertSettings.get(), iter->second), SSL_CTX_free); if (!unique_ctx || !this->_store_single_ssl_ctx(lookup, sslMultCertSettings, unique_ctx, iter->second)) { retval = false; + } else { + lookup->register_cert_secrets(data.cert_names_list, iter->second); } } + return retval; +} - for (auto &i : cert_list) { - X509_free(i); - } +/** + * 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; + std::set policies; + lookup->getPolicies(secret_name, policies); + + for (auto policy_iter = policies.begin(); policy_iter != policies.end() && retval; ++policy_iter) { + std::set common_names; + std::unordered_map> unique_names; + SSLMultiCertConfigLoader::CertLoadData data; + if (!this->_prep_ssl_ctx(*policy_iter, data, common_names, unique_names)) { + retval = false; + break; + } + + if (!common_names.empty()) { + shared_SSL_CTX ctx(this->init_server_ssl_ctx(data, policy_iter->get(), common_names), SSL_CTX_free); + + if (!ctx) { + retval = false; + } else { + for (auto name : common_names) { + SSLCertContext *cc = lookup->find(name); + 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] : ""); + shared_SSL_CTX unique_ctx(this->init_server_ssl_ctx(single_data, policy_iter->get(), iter->second), SSL_CTX_free); + + if (!unique_ctx) { + retval = false; + } else { + for (auto name : iter->second) { + SSLCertContext *cc = lookup->find(name); + if (cc && cc->userconfig.get() == policy_iter->get()) { + cc->setCtx(unique_ctx); + } + } + } + } + } return retval; } @@ -1632,7 +1685,7 @@ SSLMultiCertConfigLoader::_store_single_ssl_ctx(SSLCertLookup *lookup, const sha // 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 sni_name : names) { - if (SSLMultiCertConfigLoader::index_certificate(lookup, SSLCertContext(ctx, sslMultCertSettings), sni_name.c_str())) { + if (lookup->insert(sni_name.c_str(), SSLCertContext(ctx, sslMultCertSettings, keyblock)) >= 0) { inserted = true; } } @@ -2213,7 +2266,8 @@ SSLMultiCertConfigLoader::load_certs_and_cross_reference_names(std::vectorrelative_to(params->serverKeyPathOnly, keyname); + data.key_list.push_back(completeServerKeyPath); } for (const char *caname = ca_tok.getNext(); caname; caname = ca_tok.getNext()) { @@ -2227,22 +2281,30 @@ SSLMultiCertConfigLoader::load_certs_and_cross_reference_names(std::vectorserverCertPathOnly, certname); - scoped_BIO bio(BIO_new_file(completeServerCertPath.c_str(), "r")); + data.cert_names_list.push_back(completeServerCertPath); + } + + for (size_t i = 0; i < data.cert_names_list.size(); i++) { + std::string_view secret_data; + std::string_view 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", data.cert_names_list[i].c_str()); + 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); } if (!bio || !cert) { - SSLError("failed to load certificate chain from %s", completeServerCertPath.c_str()); + SSLError("failed to load certificate chain from %s", data.cert_names_list[i].c_str()); return false; } cert_list.push_back(cert); - if (SSLConfigParams::load_ssl_file_cb) { - SSLConfigParams::load_ssl_file_cb(completeServerCertPath.c_str()); - } std::set name_set; // Grub through the names in the certs @@ -2263,7 +2325,7 @@ SSLMultiCertConfigLoader::load_certs_and_cross_reference_names(std::vectorserverCertPathOnly, data.cert_names_list[i]); - scoped_BIO bio(BIO_new_file(completeServerCertPath.c_str(), "r")); + std::string keyPath = (i < data.key_list.size()) ? data.key_list[i] : ""; + std::string_view secret_data; + std::string_view secret_key_data; + params->secrets.getOrLoadSecret(data.cert_names_list[i], keyPath, secret_data, secret_key_data); + if (secret_data.empty()) { + SSLError("failed to load certificate secret for %s", data.cert_names_list[i].c_str()); + 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); } if (!bio || !cert) { - SSLError("failed to load certificate chain from %s", completeServerCertPath.c_str()); + SSLError("failed to load certificate chain from %s", data.cert_names_list[i].c_str()); return false; } if (!SSL_CTX_use_certificate(ctx, cert)) { - SSLError("Failed to assign cert from %s to SSL_CTX", completeServerCertPath.c_str()); + SSLError("Failed to assign cert from %s to SSL_CTX", data.cert_names_list[i].c_str()); X509_free(cert); return false; } @@ -2369,28 +2438,25 @@ SSLMultiCertConfigLoader::load_certs(SSL_CTX *ctx, SSLMultiCertConfigLoader::Cer // Load up any additional chain certificates SSL_CTX_add_extra_chain_cert_bio(ctx, bio); - const char *keyPath = i < data.key_list.size() ? data.key_list[i].c_str() : nullptr; - if (!SSLPrivateKeyHandler(ctx, params, completeServerCertPath, keyPath)) { - return false; + if (secret_key_data.empty()) { + secret_key_data = secret_data; } - - if (SSLConfigParams::load_ssl_file_cb) { - SSLConfigParams::load_ssl_file_cb(completeServerCertPath.c_str()); + if (!SSLPrivateKeyHandler(ctx, params, keyPath.c_str(), secret_key_data.data(), secret_key_data.size())) { + 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) { - ats_scoped_str completeServerCertChainPath(Layout::relative_to(params->serverCertPathOnly, params->serverCertChainFilename)); - if (!SSL_CTX_add_extra_chain_cert_file(ctx, completeServerCertChainPath)) { - SSLError("failed to load global certificate chain from %s", (const char *)completeServerCertChainPath); + 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); + SSLConfigParams::load_ssl_file_cb(completeServerCertChainPath.c_str()); } } @@ -2398,13 +2464,13 @@ SSLMultiCertConfigLoader::load_certs(SSL_CTX *ctx, SSLMultiCertConfigLoader::Cer if (sslMultCertSettings->ca) { const char *ca_name = data.ca_list[i].c_str(); if (ca_name != nullptr) { - ats_scoped_str completeServerCertChainPath(Layout::relative_to(params->serverCertPathOnly, ca_name)); - if (!SSL_CTX_add_extra_chain_cert_file(ctx, completeServerCertChainPath)) { - SSLError("failed to load certificate chain from %s", (const char *)completeServerCertChainPath); + 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); + SSLConfigParams::load_ssl_file_cb(completeServerCertChainPath.c_str()); } } } @@ -2412,8 +2478,8 @@ SSLMultiCertConfigLoader::load_certs(SSL_CTX *ctx, SSLMultiCertConfigLoader::Cer if (SSLConfigParams::ssl_ocsp_enabled) { if (sslMultCertSettings->ocsp_response) { const char *ocsp_response_name = data.ocsp_list[i].c_str(); - ats_scoped_str completeOCSPResponsePath(Layout::relative_to(params->ssl_ocsp_response_path_only, ocsp_response_name)); - if (!ssl_stapling_init_cert(ctx, cert, data.cert_names_list[i].c_str(), (const char *)completeOCSPResponsePath)) { + std::string completeOCSPResponsePath(Layout::relative_to(params->ssl_ocsp_response_path_only, ocsp_response_name)); + if (!ssl_stapling_init_cert(ctx, cert, data.cert_names_list[i].c_str(), completeOCSPResponsePath.c_str())) { Warning("failed to configure SSL_CTX for OCSP Stapling info for certificate at %s", data.cert_names_list[i].c_str()); } } else { @@ -2425,7 +2491,6 @@ SSLMultiCertConfigLoader::load_certs(SSL_CTX *ctx, SSLMultiCertConfigLoader::Cer #endif /* TS_USE_TLS_OCSP */ X509_free(cert); } - return true; } diff --git a/iocore/net/libinknet_stub.cc b/iocore/net/libinknet_stub.cc index 4722daaab87..249e12a04dd 100644 --- a/iocore/net/libinknet_stub.cc +++ b/iocore/net/libinknet_stub.cc @@ -148,7 +148,8 @@ HttpRequestData::get_client_ip() return nullptr; } -SslAPIHooks *ssl_hooks = nullptr; +SslAPIHooks *ssl_hooks = nullptr; +LifecycleAPIHooks *lifecycle_hooks = nullptr; StatPagesManager statPagesManager; #include "ProcessManager.h" diff --git a/mgmt/ProxyConfig.cc b/mgmt/ProxyConfig.cc index d30d06544b5..e7b36df6ded 100644 --- a/mgmt/ProxyConfig.cc +++ b/mgmt/ProxyConfig.cc @@ -65,10 +65,12 @@ ConfigProcessor::set(unsigned int id, ConfigInfo *info, unsigned timeout_secs) // Don't be an idiot and use a zero timeout ... ink_assert(timeout_secs > 0); - // New objects *must* start with a zero refcount. The config - // processor holds it's own refcount. We should be the only - // refcount holder at this point. - ink_release_assert(info->refcount_inc() == 1); + if (info) { + // New objects *must* start with a zero refcount. The config + // processor holds it's own refcount. We should be the only + // refcount holder at this point. + ink_release_assert(info->refcount_inc() == 1); + } if (id > MAX_CONFIGS) { // invalid index @@ -108,9 +110,11 @@ ConfigProcessor::get(unsigned int id) idx = id - 1; info = infos[idx]; - // Hand out a refcount to the caller. We should still have out - // own refcount, so it should be at least 2. - ink_release_assert(info->refcount_inc() > 1); + if (info) { + // Hand out a refcount to the caller. We should still have out + // own refcount, so it should be at least 2. + ink_release_assert(info->refcount_inc() > 1); + } return info; } diff --git a/plugins/cache_promote/lru_policy.cc b/plugins/cache_promote/lru_policy.cc index fc0af72a60c..bf7240df7e7 100644 --- a/plugins/cache_promote/lru_policy.cc +++ b/plugins/cache_promote/lru_policy.cc @@ -42,12 +42,20 @@ LRUHash::initFromUrl(TSHttpTxn txnp) char *url = TSUrlStringGet(reqp, c_url, &url_len); if (url && url_len > 0) { + // SHA1() is deprecated on OpenSSL 3, but it's faster than its replacement. +#ifdef HAVE_SHA1 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" SHA_CTX sha; SHA1_Init(&sha); - TSDebug(PLUGIN_NAME, "LRUHash::initFromUrl(%.*s%s)", url_len > 100 ? 100 : url_len, url, url_len > 100 ? "..." : ""); SHA1_Update(&sha, url, url_len); SHA1_Final(_hash, &sha); +#pragma GCC diagnostic pop +#else + EVP_Digest(url, url_len, _hash, nullptr, EVP_sha1(), nullptr); +#endif + TSDebug(PLUGIN_NAME, "LRUHash::initFromUrl(%.*s%s)", url_len > 100 ? 100 : url_len, url, url_len > 100 ? "..." : ""); TSfree(url); ret = true; } diff --git a/plugins/cache_promote/lru_policy.h b/plugins/cache_promote/lru_policy.h index 99151206e2e..3cf62e7081e 100644 --- a/plugins/cache_promote/lru_policy.h +++ b/plugins/cache_promote/lru_policy.h @@ -17,7 +17,12 @@ */ #pragma once +#include "tscore/ink_defs.h" + #include +#ifndef HAVE_SHA1 +#include +#endif #include #include #include diff --git a/proxy/Transform.cc b/proxy/Transform.cc index dd8e4766e16..664e310df6b 100644 --- a/proxy/Transform.cc +++ b/proxy/Transform.cc @@ -850,6 +850,10 @@ RangeTransform::transform_to_range() int64_t prev_end = 0; int64_t *done_byte; + if (m_current_range >= m_num_range_fields) { + return; + } + end = &m_ranges[m_current_range]._end; done_byte = &m_ranges[m_current_range]._done_byte; start = &m_ranges[m_current_range]._start; diff --git a/proxy/http/HttpSessionManager.cc b/proxy/http/HttpSessionManager.cc index 0874f100c0b..97e881031f1 100644 --- a/proxy/http/HttpSessionManager.cc +++ b/proxy/http/HttpSessionManager.cc @@ -212,8 +212,10 @@ ServerSessionPool::releaseSession(PoolableSession *ss) ss->do_io_write(this, 0, nullptr); // we probably don't need the active timeout set, but will leave it for now - ss->set_inactivity_timeout(ss->get_netvc()->get_inactivity_timeout()); - ss->set_active_timeout(ss->get_netvc()->get_active_timeout()); + HttpConfigParams *http_config_params = HttpConfig::acquire(); + ss->set_inactivity_timeout(HRTIME_SECONDS(http_config_params->oride.keep_alive_no_activity_timeout_out)); + ss->set_active_timeout(HRTIME_SECONDS(http_config_params->oride.keep_alive_no_activity_timeout_out)); + HttpConfig::release(http_config_params); // put it in the pools. this->addSession(ss); @@ -269,8 +271,8 @@ ServerSessionPool::eventHandler(int event, void *data) "[%" PRId64 "] [session_bucket] session received io notice [%s], " "resetting timeout to maintain minimum number of connections", s->connection_id(), HttpDebugNames::get_event_name(event)); - s->get_netvc()->set_inactivity_timeout(s->get_netvc()->get_inactivity_timeout()); - s->get_netvc()->set_active_timeout(s->get_netvc()->get_active_timeout()); + s->get_netvc()->set_inactivity_timeout(HRTIME_SECONDS(http_config_params->oride.keep_alive_no_activity_timeout_out)); + s->get_netvc()->set_active_timeout(HRTIME_SECONDS(http_config_params->oride.keep_alive_no_activity_timeout_out)); found = true; break; } diff --git a/proxy/http2/Http2ClientSession.cc b/proxy/http2/Http2ClientSession.cc index 9f398a371f5..1834e1c8b4e 100644 --- a/proxy/http2/Http2ClientSession.cc +++ b/proxy/http2/Http2ClientSession.cc @@ -177,8 +177,8 @@ Http2ClientSession::start() VIO *read_vio = this->do_io_read(this, INT64_MAX, this->read_buffer); write_vio = this->do_io_write(this, INT64_MAX, this->_write_buffer_reader); - this->connection_state.init(); - send_connection_event(&this->connection_state, HTTP2_SESSION_EVENT_INIT, this); + this->connection_state.init(this); + this->connection_state.send_connection_preface(); if (this->_read_buffer_reader->is_read_avail_more_than(0)) { this->handleEvent(VC_EVENT_READ_READY, read_vio); @@ -523,7 +523,8 @@ Http2ClientSession::do_complete_frame_read() ink_release_assert(this->_read_buffer_reader->read_avail() >= this->current_hdr.length); Http2Frame frame(this->current_hdr, this->_read_buffer_reader, this->cur_frame_from_early_data); - send_connection_event(&this->connection_state, HTTP2_SESSION_EVENT_RECV, &frame); + connection_state.rcv_frame(&frame); + // Check whether data is read from early data if (this->read_from_early_data > 0) { this->read_from_early_data -= diff --git a/proxy/http2/Http2ConnectionState.cc b/proxy/http2/Http2ConnectionState.cc index 323790ec1a3..7d424fb67f9 100644 --- a/proxy/http2/Http2ConnectionState.cc +++ b/proxy/http2/Http2ConnectionState.cc @@ -1040,8 +1040,9 @@ Http2ConnectionState::Http2ConnectionState() : stream_list() } void -Http2ConnectionState::init() +Http2ConnectionState::init(Http2ClientSession *ssn) { + ua_session = ssn; this->_server_rwnd = Http2::initial_window_size; local_hpack_handle = new HpackHandle(HTTP2_HEADER_TABLE_SIZE); @@ -1054,6 +1055,32 @@ Http2ConnectionState::init() _cop.start(); } +/** + Send connection preface + + The client connection preface is HTTP2_CONNECTION_PREFACE. + The server connection preface consists of a potentially emptry SETTINGS frame. + + Details in [RFC 7540] 3.5. HTTP/2 Connection Preface + + TODO: send client connection preface if the connection is outbound + */ +void +Http2ConnectionState::send_connection_preface() +{ + REMEMBER(NO_EVENT, this->recursion) + + Http2ConnectionSettings configured_settings; + configured_settings.settings_from_configs(); + configured_settings.set(HTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, _adjust_concurrent_stream()); + + send_settings_frame(configured_settings); + + if (server_settings.get(HTTP2_SETTINGS_INITIAL_WINDOW_SIZE) > HTTP2_INITIAL_WINDOW_SIZE) { + send_window_update_frame(0, server_settings.get(HTTP2_SETTINGS_INITIAL_WINDOW_SIZE) - HTTP2_INITIAL_WINDOW_SIZE); + } +} + void Http2ConnectionState::destroy() { @@ -1089,6 +1116,71 @@ Http2ConnectionState::destroy() mutex = nullptr; // magic happens - assigning to nullptr frees the ProxyMutex } +void +Http2ConnectionState::rcv_frame(const Http2Frame *frame) +{ + REMEMBER(NO_EVENT, this->recursion); + const Http2StreamId stream_id = frame->header().streamid; + Http2Error error; + + // [RFC 7540] 5.5. Extending HTTP/2 + // Implementations MUST discard frames that have unknown or unsupported types. + if (frame->header().type >= HTTP2_FRAME_TYPE_MAX) { + Http2StreamDebug(ua_session, stream_id, "Discard a frame which has unknown type, type=%x", frame->header().type); + return; + } + + // We need to be careful here, certain frame types are not safe over 0-rtt, tentative for now. + // DATA: NO + // HEADERS: YES (safe http methods only, can only be checked after parsing the payload). + // PRIORITY: YES + // RST_STREAM: NO + // SETTINGS: YES + // PUSH_PROMISE: NO + // PING: YES + // GOAWAY: NO + // WINDOW_UPDATE: YES + // CONTINUATION: YES (safe http methods only, same as HEADERS frame). + if (frame->is_from_early_data() && + (frame->header().type == HTTP2_FRAME_TYPE_DATA || frame->header().type == HTTP2_FRAME_TYPE_RST_STREAM || + frame->header().type == HTTP2_FRAME_TYPE_PUSH_PROMISE || frame->header().type == HTTP2_FRAME_TYPE_GOAWAY)) { + Http2StreamDebug(ua_session, stream_id, "Discard a frame which is received from early data and has type=%x", + frame->header().type); + return; + } + + if (frame_handlers[frame->header().type]) { + error = frame_handlers[frame->header().type](*this, *frame); + } else { + error = Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, Http2ErrorCode::HTTP2_ERROR_INTERNAL_ERROR, "no handler"); + } + + if (error.cls != Http2ErrorClass::HTTP2_ERROR_CLASS_NONE) { + ip_port_text_buffer ipb; + const char *client_ip = ats_ip_ntop(ua_session->get_remote_addr(), ipb, sizeof(ipb)); + if (error.cls == Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION) { + if (error.msg) { + Error("HTTP/2 connection error code=0x%02x client_ip=%s session_id=%" PRId64 " stream_id=%u %s", + static_cast(error.code), client_ip, ua_session->connection_id(), stream_id, error.msg); + } + this->send_goaway_frame(this->latest_streamid_in, error.code); + this->ua_session->set_half_close_local_flag(true); + if (fini_event == nullptr) { + fini_event = this_ethread()->schedule_imm_local((Continuation *)this, HTTP2_SESSION_EVENT_FINI); + } + + // The streams will be cleaned up by the HTTP2_SESSION_EVENT_FINI event + // The Http2ClientSession will shutdown because connection_state.is_state_closed() will be true + } else if (error.cls == Http2ErrorClass::HTTP2_ERROR_CLASS_STREAM) { + if (error.msg) { + Error("HTTP/2 stream error code=0x%02x client_ip=%s session_id=%" PRId64 " stream_id=%u %s", static_cast(error.code), + client_ip, ua_session->connection_id(), stream_id, error.msg); + } + this->send_rst_stream_frame(stream_id, error.code); + } + } +} + int Http2ConnectionState::main_event_handler(int event, void *edata) { @@ -1100,33 +1192,6 @@ Http2ConnectionState::main_event_handler(int event, void *edata) } ++recursion; switch (event) { - // Initialize HTTP/2 Connection - case HTTP2_SESSION_EVENT_INIT: { - ink_assert(this->ua_session == nullptr); - this->ua_session = static_cast(edata); - REMEMBER(event, this->recursion); - - // [RFC 7540] 3.5. HTTP/2 Connection Preface. Upon establishment of a TCP connection and - // determination that HTTP/2 will be used by both peers, each endpoint MUST - // send a connection preface as a final confirmation ... The server - // connection - // preface consists of a potentially empty SETTINGS frame. - - // Load the server settings from the records.config / RecordsConfig.cc - // settings. - Http2ConnectionSettings configured_settings; - configured_settings.settings_from_configs(); - configured_settings.set(HTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, _adjust_concurrent_stream()); - - send_settings_frame(configured_settings); - - if (server_settings.get(HTTP2_SETTINGS_INITIAL_WINDOW_SIZE) > HTTP2_INITIAL_WINDOW_SIZE) { - send_window_update_frame(0, server_settings.get(HTTP2_SETTINGS_INITIAL_WINDOW_SIZE) - HTTP2_INITIAL_WINDOW_SIZE); - } - - break; - } - // Finalize HTTP/2 Connection case HTTP2_SESSION_EVENT_FINI: { SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread()); @@ -1146,72 +1211,6 @@ Http2ConnectionState::main_event_handler(int event, void *edata) _scheduled = false; } break; - // Parse received HTTP/2 frames - case HTTP2_SESSION_EVENT_RECV: { - REMEMBER(event, this->recursion); - const Http2Frame *frame = static_cast(edata); - const Http2StreamId stream_id = frame->header().streamid; - Http2Error error; - - // [RFC 7540] 5.5. Extending HTTP/2 - // Implementations MUST discard frames that have unknown or unsupported types. - if (frame->header().type >= HTTP2_FRAME_TYPE_MAX) { - Http2StreamDebug(ua_session, stream_id, "Discard a frame which has unknown type, type=%x", frame->header().type); - break; - } - - // We need to be careful here, certain frame types are not safe over 0-rtt, tentative for now. - // DATA: NO - // HEADERS: YES (safe http methods only, can only be checked after parsing the payload). - // PRIORITY: YES - // RST_STREAM: NO - // SETTINGS: YES - // PUSH_PROMISE: NO - // PING: YES - // GOAWAY: NO - // WINDOW_UPDATE: YES - // CONTINUATION: YES (safe http methods only, same as HEADERS frame). - if (frame->is_from_early_data() && - (frame->header().type == HTTP2_FRAME_TYPE_DATA || frame->header().type == HTTP2_FRAME_TYPE_RST_STREAM || - frame->header().type == HTTP2_FRAME_TYPE_PUSH_PROMISE || frame->header().type == HTTP2_FRAME_TYPE_GOAWAY)) { - Http2StreamDebug(ua_session, stream_id, "Discard a frame which is received from early data and has type=%x", - frame->header().type); - break; - } - - if (frame_handlers[frame->header().type]) { - error = frame_handlers[frame->header().type](*this, *frame); - } else { - error = Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, Http2ErrorCode::HTTP2_ERROR_INTERNAL_ERROR, "no handler"); - } - - if (error.cls != Http2ErrorClass::HTTP2_ERROR_CLASS_NONE) { - ip_port_text_buffer ipb; - const char *client_ip = ats_ip_ntop(ua_session->get_remote_addr(), ipb, sizeof(ipb)); - if (error.cls == Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION) { - if (error.msg) { - Error("HTTP/2 connection error code=0x%02x client_ip=%s session_id=%" PRId64 " stream_id=%u %s", - static_cast(error.code), client_ip, ua_session->connection_id(), stream_id, error.msg); - } - this->send_goaway_frame(this->latest_streamid_in, error.code); - this->ua_session->set_half_close_local_flag(true); - if (fini_event == nullptr) { - fini_event = this_ethread()->schedule_imm_local((Continuation *)this, HTTP2_SESSION_EVENT_FINI); - } - - // The streams will be cleaned up by the HTTP2_SESSION_EVENT_FINI event - // The Http2ClientSession will shutdown because connection_state.is_state_closed() will be true - } else if (error.cls == Http2ErrorClass::HTTP2_ERROR_CLASS_STREAM) { - if (error.msg) { - Error("HTTP/2 stream error code=0x%02x client_ip=%s session_id=%" PRId64 " stream_id=%u %s", static_cast(error.code), - client_ip, ua_session->connection_id(), stream_id, error.msg); - } - this->send_rst_stream_frame(stream_id, error.code); - } - } - - } break; - // Initiate a gracefull shutdown case HTTP2_SESSION_EVENT_SHUTDOWN_INIT: { REMEMBER(event, this->recursion); diff --git a/proxy/http2/Http2ConnectionState.h b/proxy/http2/Http2ConnectionState.h index b79464ddbbd..3c55bdfaa3b 100644 --- a/proxy/http2/Http2ConnectionState.h +++ b/proxy/http2/Http2ConnectionState.h @@ -34,6 +34,7 @@ #include "Http2FrequencyCounter.h" class Http2ClientSession; +class Http2Frame; enum class Http2SendDataFrameResult { NO_ERROR = 0, @@ -89,8 +90,10 @@ class Http2ConnectionState : public Continuation Http2ConnectionSettings server_settings; Http2ConnectionSettings client_settings; - void init(); + void init(Http2ClientSession *ssn); + void send_connection_preface(); void destroy(); + void rcv_frame(const Http2Frame *frame); // Event handlers int main_event_handler(int, void *); diff --git a/src/traffic_quic/traffic_quic.cc b/src/traffic_quic/traffic_quic.cc index b3c37c1b6a0..4ee2f5fb0ac 100644 --- a/src/traffic_quic/traffic_quic.cc +++ b/src/traffic_quic/traffic_quic.cc @@ -267,7 +267,8 @@ HttpRequestData::get_client_ip() return nullptr; } -SslAPIHooks *ssl_hooks = nullptr; +SslAPIHooks *ssl_hooks = nullptr; +LifecycleAPIHooks *lifecycle_hooks = nullptr; StatPagesManager statPagesManager; #include "HttpDebugNames.h" diff --git a/src/traffic_server/InkAPI.cc b/src/traffic_server/InkAPI.cc index 47cb4df72fc..6d63bd1ac90 100644 --- a/src/traffic_server/InkAPI.cc +++ b/src/traffic_server/InkAPI.cc @@ -9358,6 +9358,71 @@ TSSslContextFindByAddr(struct sockaddr const *addr) return ret; } +/** + * This function sets the secret cache value for a given secret name. This allows + * plugins to load cert/key PEM information on for use by the TLS core + */ +tsapi TSReturnCode +TSSslSecretSet(const char *secret_name, int secret_name_length, const char *secret_data, int secret_data_len) +{ + TSReturnCode retval = TS_SUCCESS; + SSLConfigParams *load_params = SSLConfig::load_acquire(); + SSLConfigParams *params = SSLConfig::acquire(); + if (load_params != nullptr) { // Update the current data structure + if (!load_params->secrets.setSecret(std::string(secret_name, secret_name_length), secret_data, secret_data_len)) { + retval = TS_ERROR; + } + SSLConfig::load_release(params); + } + if (params != nullptr) { + if (!params->secrets.setSecret(std::string(secret_name, secret_name_length), secret_data, secret_data_len)) { + retval = TS_ERROR; + } + SSLConfig::release(params); + } + return retval; +} + +tsapi TSReturnCode +TSSslSecretUpdate(const char *secret_name, int secret_name_length) +{ + TSReturnCode retval = TS_SUCCESS; + SSLConfigParams *params = SSLConfig::acquire(); + if (params != nullptr) { + params->updateCTX(std::string(secret_name, secret_name_length)); + } + SSLConfig::release(params); + return retval; +} + +tsapi TSReturnCode +TSSslSecretGet(const char *secret_name, int secret_name_length, const char **secret_data_return, int *secret_data_len) +{ + bool loading = true; + TSReturnCode retval = TS_SUCCESS; + SSLConfigParams *params = SSLConfig::load_acquire(); + if (params == nullptr) { + params = SSLConfig::acquire(); + loading = false; + } + std::string_view secret_data; + if (!params->secrets.getSecret(std::string(secret_name, secret_name_length), secret_data)) { + retval = TS_ERROR; + } + if (secret_data_return) { + *secret_data_return = secret_data.data(); + } + if (secret_data_len) { + *secret_data_len = secret_data.size(); + } + if (loading) { + SSLConfig::load_release(params); + } else { + SSLConfig::release(params); + } + return retval; +} + /** * This function retrieves an array of lookup keys for client contexts loaded in * traffic server. Given a 2-level mapping for client contexts, every 2 lookup keys diff --git a/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_state.test.py b/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_state.test.py index d733cddda48..04433cc5f71 100644 --- a/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_state.test.py +++ b/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_state.test.py @@ -61,7 +61,7 @@ server.addResponse("sessionlog.json", request_header_0, response_header_0) reval_conf_path = os.path.join(ts.Variables.CONFIGDIR, 'reval.conf') -reval_state_path = os.path.join(ts.Variables.RUNTIMEDIR, 'reval.state') +reval_state_path = os.path.join(Test.Variables.RUNTIMEDIR, 'reval.state') # Configure ATS server ts.Disk.plugin_config.AddLine( @@ -109,6 +109,8 @@ path0_rule, path1_rule, ]) +ts.chownForATSProcess(reval_state_path) + ts.Disk.remap_config.AddLine( f"map http://ats/ http://127.0.0.1:{server.Variables.Port}" ) @@ -120,11 +122,21 @@ 'proxy.config.http.wait_for_cache': 1, }) + +# This TestRun creates the state file so it exists when the ts process's Setup +# logic is run so that it can be chowned at that point. +tr = Test.AddTestRun("Populate the regex_revalidate state file") +tr.Processes.Default.Command = f'touch {reval_state_path}' + # Start ATS and evaluate the new state file tr = Test.AddTestRun("Initial load, state merged") ps = tr.Processes.Default ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) + +# Note the ready condition: wait for ATS to modify the contents +# of the file from dummy to path1. +ps.StartBefore(Test.Processes.ts, ready=When.FileContains(reval_state_path, "path0")) + ps.Command = 'cat ' + reval_state_path ps.ReturnCode = 0 ps.Streams.stdout.Content = Testers.GoldFile(gold_path_good) diff --git a/tests/gold_tests/tls/tls_check_cert_select_plugin.test.py b/tests/gold_tests/tls/tls_check_cert_select_plugin.test.py new file mode 100644 index 00000000000..b189060d1ac --- /dev/null +++ b/tests/gold_tests/tls/tls_check_cert_select_plugin.test.py @@ -0,0 +1,166 @@ +''' +''' +# 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. + +import os + +Test.Summary = ''' +Test ATS offering different certificates based on SNI. Load via plugin +''' + +# Define default ATS +ts = Test.MakeATSProcess("ts", command="traffic_manager", select_ports=True, enable_tls=True) +server = Test.MakeOriginServer("server", ssl=True) +dns = Test.MakeDNServer("dns") + +request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +server.addResponse("sessionlog.json", request_header, response_header) + +# add ssl materials like key, certificates for the server +ts.addSSLfile("ssl/signed-foo.pem") +ts.addSSLfile("ssl/signed-foo.key") +ts.addSSLfile("ssl/signed-bar.pem") +ts.addSSLfile("ssl/signed2-bar.pem") +ts.addSSLfile("ssl/signed-bar.key") +ts.addSSLfile("ssl/server.pem") +ts.addSSLfile("ssl/server.key") +ts.addSSLfile("ssl/signer.pem") +ts.addSSLfile("ssl/signer.key") + +ts.Disk.remap_config.AddLine( + 'map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + +ts.Disk.ssl_multicert_config.AddLines([ + 'dest_ip=127.0.0.1 ssl_cert_name=signed-foo.pem ssl_key_name=signed-foo.key', + 'ssl_cert_name=signed2-bar.pem ssl_key_name=signed-bar.key', + 'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key' +]) + +Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) + +# Case 1, global config policy=permissive properties=signature +# override for foo.com policy=enforced properties=all +ts.Disk.records_config.update({ + 'proxy.config.diags.debug.tags': 'ssl_secret_load_test|ssl', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.ssl.server.cert.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', +}) + +dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) +dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + +# Should receive a bar.com cert +tr = Test.AddTestRun("bar.com cert") +tr.Setup.Copy("ssl/signer.pem") +tr.Setup.Copy("ssl/signer2.pem") +tr.Processes.Default.Command = "curl -v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format( + ts.Variables.ssl_port) +tr.ReturnCode = 0 +tr.Processes.Default.StartBefore(server) +tr.Processes.Default.StartBefore(dns) +tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") + +# Should receive a foo.com cert +tr2 = Test.AddTestRun("foo.com cert") +tr2.Processes.Default.Command = "curl -v --cacert ./signer.pem --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format( + ts.Variables.ssl_port) +tr2.ReturnCode = 0 +tr2.StillRunningAfter = server +tr2.StillRunningAfter = ts +tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") +tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Cert should contain foo.com") +tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") + +# Should receive random.server.com +tr2 = Test.AddTestRun("random.server.com cert") +tr2.Processes.Default.Command = "curl -v -k --resolve 'random.server.com:{0}:127.0.0.1' https://random.server.com:{0}".format( + ts.Variables.ssl_port) +tr2.ReturnCode = 0 +tr2.StillRunningAfter = server +tr2.StillRunningAfter = ts +tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") +tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=random.server.com", "Cert should contain random.server.com") +tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") +tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") + +# No SNI match should match specific IP address, foo.com +# SNI name and returned cert name will not match, so must use -k to avoid cert verification +tr2 = Test.AddTestRun("Bad SNI") +tr2.Processes.Default.Command = "curl -v -k --cacert ./signer.pem --resolve 'bad.sni.com:{0}:127.0.0.1' https://bad.sni.com:{0}".format( + ts.Variables.ssl_port) +tr2.ReturnCode = 0 +tr2.StillRunningAfter = server +tr2.StillRunningAfter = ts +tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") +tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Cert should contain foo.com") +tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") + +# Copy in a new version of the bar.com cert. Replace it with the version +# signed by signer 1. Wait at least a second to sure the file update time +# differs +trupdate = Test.AddTestRun("Update server bar cert file in place") +trupdate.StillRunningAfter = ts +trupdate.StillRunningAfter = server +trupdate.Setup.CopyAs("ssl/signed-bar.pem", ".", "{0}/signed2-bar.pem".format(ts.Variables.SSLDir)) +# For some reason the Setup.CopyAs does not change the modification time, so we touch +trupdate.Processes.Default.Command = 'touch {0}/signed2-bar.pem'.format(ts.Variables.SSLDir) +# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket +trupdate.Processes.Default.Env = ts.Env +trupdate.Processes.Default.ReturnCode = 0 + +# The plugin will pull every 3 seconds. So wait 4 seconds and test again. +# Request with CA=signer.pem should work. Request with CA=signer2.pem +# should fail +tr = Test.AddTestRun("Test new version of bar cert with good CA") +tr.DelayStart = 4 +tr.Processes.Default.Command = "date; curl -v --cacert ./signer.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format( + ts.Variables.ssl_port) +tr.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") + +tr = Test.AddTestRun("Test new version of bar cert with bad CA") +tr.Processes.Default.Command = "curl -v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format( + ts.Variables.ssl_port) +tr.ReturnCode = 60 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All = Testers.ContainsExpression("unknown CA", "Failed handshake") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should contain bar.com") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("404", "Should make an exchange") diff --git a/tests/gold_tests/tls/tls_check_dual_cert_selection_plugin.test.py b/tests/gold_tests/tls/tls_check_dual_cert_selection_plugin.test.py new file mode 100644 index 00000000000..0532dbe7cc3 --- /dev/null +++ b/tests/gold_tests/tls/tls_check_dual_cert_selection_plugin.test.py @@ -0,0 +1,193 @@ +''' +''' +# 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. + +import os + +Test.Summary = ''' +Test ATS offering both RSA and EC certificates loaded via plugin +''' + +Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) + +# Define default ATS +ts = Test.MakeATSProcess("ts", select_ports=True, enable_tls=True) +server = Test.MakeOriginServer("server", ssl=True) +dns = Test.MakeDNServer("dns") + +request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +server.addResponse("sessionlog.json", request_header, response_header) + +# add ssl materials like key, certificates for the server +ts.addSSLfile("ssl/signed-foo.pem") +ts.addSSLfile("ssl/signed2-foo.pem") +ts.addSSLfile("ssl/signed-foo.key") +ts.addSSLfile("ssl/signed-foo-ec.pem") +ts.addSSLfile("ssl/signed-foo-ec.key") +ts.addSSLfile("ssl/signed-san.pem") +ts.addSSLfile("ssl/signed-san.key") +ts.addSSLfile("ssl/signed-san-ec.pem") +ts.addSSLfile("ssl/signed-san-ec.key") +ts.addSSLfile("ssl/signer.pem") +ts.addSSLfile("ssl/signer.key") +ts.addSSLfile("ssl/server.pem") +ts.addSSLfile("ssl/server.key") + +ts.Disk.remap_config.AddLine( + 'map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + +ts.Disk.ssl_multicert_config.AddLines([ + 'ssl_cert_name=signed-foo-ec.pem,signed-foo.pem ssl_key_name=signed-foo-ec.key,signed-foo.key', + 'ssl_cert_name=signed-san-ec.pem,signed-san.pem ssl_key_name=signed-san-ec.key,signed-san.key', + 'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key' +]) + +Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) + +# Case 1, global config policy=permissive properties=signature +# override for foo.com policy=enforced properties=all +ts.Disk.records_config.update({ + 'proxy.config.ssl.server.cert.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.tags': 'ssl_secret_load_test', + 'proxy.config.diags.debug.enabled': 1 +}) + +dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) +dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + +# Should receive a EC cert +tr = Test.AddTestRun("Default for foo should return EC cert") +tr.Setup.Copy("ssl/signer.pem") +tr.Setup.Copy("ssl/signer2.pem") +tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) +tr.ReturnCode = 0 +tr.Processes.Default.StartBefore(server) +tr.Processes.Default.StartBefore(dns) +tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: ECDSA", "Should select EC cert") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + +# Should receive a RSA cert +tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert") +tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) +tr.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: RSA-PSS", "Should select RSA cert") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + +# Should receive a EC cert +tr = Test.AddTestRun("Default for one.com should return EC cert") +tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername one.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) +tr.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: ECDSA", "Should select EC cert") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN = group.com", "Should select a group SAN") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + +# Should receive a RSA cert +tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert") +tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername one.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) +tr.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: RSA-PSS", "Should select RSA cert") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN = group.com", "Should select a group SAN") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + +# Should receive a RSA cert +tr = Test.AddTestRun("rsa.com only in rsa cert") +tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername rsa.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) +tr.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: RSA-PSS", "Should select RSA cert") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN = group.com", "Should select a group SAN") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + +# Should receive a EC cert +tr = Test.AddTestRun("ec.com only in ec cert") +tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername ec.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) +tr.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: ECDSA", "Should select EC cert") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN = group.com", "Should select a group SAN") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + +# Copy in a new version of the foo.com cert. Replace it with the version +# signed by signer 2. Wait at least a second to sure the file update time +# differs +trupdate = Test.AddTestRun("Update server bar cert file in place") +trupdate.StillRunningAfter = ts +trupdate.StillRunningAfter = server +trupdate.Setup.CopyAs("ssl/signed2-foo.pem", ".", "{0}/signed-foo.pem".format(ts.Variables.SSLDir)) +# For some reason the Setup.CopyAs does not change the modification time, so we touch +trupdate.Processes.Default.Command = 'touch {0}/signed-foo.pem'.format(ts.Variables.SSLDir) +# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket +trupdate.Processes.Default.Env = ts.Env +trupdate.Processes.Default.ReturnCode = 0 + +# The plugin will pull every 3 seconds. So wait 4 seconds and test again. Request with CA=signer2.pem should work. Request with CA=signer.pem should fail +# Should receive a RSA cert +tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert") +tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) +tr.DelayStart = 4 +tr.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: RSA-PSS", "Should select RSA cert") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN = foo.com", "Should select foo.com") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("unable to verify the first certificate", "Different signer") + +tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert with correct CA") +tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer2.pem -servername foo.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) +tr.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: RSA-PSS", "Should select RSA cert") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN = foo.com", "Should select foo.com") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + +# The EC case should be unchanged +tr = Test.AddTestRun("Offer any cipher") +tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) +tr.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = ts +tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: ECDSA", "Should select EC cert") +tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN = foo.com", "Should select foo.com") +tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") diff --git a/tests/gold_tests/tls/tls_client_cert2_plugin.test.py b/tests/gold_tests/tls/tls_client_cert2_plugin.test.py new file mode 100644 index 00000000000..4410e27c37f --- /dev/null +++ b/tests/gold_tests/tls/tls_client_cert2_plugin.test.py @@ -0,0 +1,191 @@ +''' +Test offering client cert to origin, but using plugin for cert loading +''' +# 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. + +import subprocess +import os + +Test.Summary = ''' +Test offering client cert to origin, but using plugin for cert loading +''' + +ts = Test.MakeATSProcess("ts", command="traffic_server", select_ports=True) +cafile = "{0}/signer.pem".format(Test.RunDirectory) +cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) +server = Test.MakeOriginServer("server", + ssl=True, + options={"--clientCA": cafile, + "--clientverify": ""}, + clientcert="{0}/signed-foo.pem".format(Test.RunDirectory), + clientkey="{0}/signed-foo.key".format(Test.RunDirectory)) +server2 = Test.MakeOriginServer("server2", + ssl=True, + options={"--clientCA": cafile2, + "--clientverify": ""}, + clientcert="{0}/signed2-bar.pem".format(Test.RunDirectory), + clientkey="{0}/signed-bar.key".format(Test.RunDirectory)) +server.Setup.Copy("ssl/signer.pem") +server.Setup.Copy("ssl/signer2.pem") +server.Setup.Copy("ssl/signed-foo.pem") +server.Setup.Copy("ssl/signed-foo.key") +server.Setup.Copy("ssl/signed2-foo.pem") +server.Setup.Copy("ssl/signed2-bar.pem") +server.Setup.Copy("ssl/signed-bar.key") +server2.Setup.Copy("ssl/signer.pem") +server2.Setup.Copy("ssl/signer2.pem") +server2.Setup.Copy("ssl/signed-foo.pem") +server2.Setup.Copy("ssl/signed-foo.key") +server2.Setup.Copy("ssl/signed2-foo.pem") +server2.Setup.Copy("ssl/signed2-bar.pem") +server2.Setup.Copy("ssl/signed-bar.key") + +request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +server.addResponse("sessionlog.json", request_header, response_header) +request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +server.addResponse("sessionlog.json", request_header, response_header) + +# +# Certs and keys loaded into the ts/ssl directory, but the paths in the +# configs omit the ssl subdirectory. The ssl_secret_load_test plugin adds this back in +ts.addSSLfile("ssl/server.pem") +ts.addSSLfile("ssl/server.key") +ts.addSSLfile("ssl/combo-signed-foo.pem") +ts.addSSLfile("ssl/signed-foo.pem") +ts.addSSLfile("ssl/signed-foo.key") +ts.addSSLfile("ssl/signed2-foo.pem") +ts.addSSLfile("ssl/signed-bar.pem") +ts.addSSLfile("ssl/signed2-bar.pem") +ts.addSSLfile("ssl/signed-bar.key") + +Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) + +ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_secret_load_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, +}) + +ts.Disk.ssl_multicert_config.AddLine( + 'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key' +) + +ts.Disk.remap_config.AddLine( + 'map /case1 https://127.0.0.1:{0}/'.format(server.Variables.SSL_Port) +) +ts.Disk.remap_config.AddLine( + 'map /case2 https://127.0.0.1:{0}/'.format(server2.Variables.SSL_Port) +) + +ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: bob.bar.com', + ' client_cert: {0}/../signed-bar.pem'.format(ts.Variables.SSLDir), + ' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir), + '- fqdn: bob.*.com', + ' client_cert: {0}/../combo-signed-foo.pem'.format(ts.Variables.SSLDir), + '- fqdn: "*bar.com"', + ' client_cert: {0}/../signed2-bar.pem'.format(ts.Variables.SSLDir), + ' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir), + '- fqdn: "foo.com"', + ' client_cert: {0}/../signed2-foo.pem'.format(ts.Variables.SSLDir), + ' client_key: {0}/../signed-foo.key'.format(ts.Variables.SSLDir), +]) + + +# Should succeed +tr = Test.AddTestRun("bob.bar.com to server 1") +tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.port)) +tr.Processes.Default.StartBefore(server) +tr.Processes.Default.StartBefore(server2) +tr.StillRunningAfter = ts +tr.StillRunningAfter = server +tr.StillRunningAfter = server2 +tr.Processes.Default.Command = "curl -H host:bob.bar.com http://127.0.0.1:{0}/case1".format(ts.Variables.port) +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Should fail +trfail = Test.AddTestRun("bob.bar.com to server 2") +trfail.StillRunningAfter = ts +trfail.StillRunningAfter = server +trfail.StillRunningAfter = server2 +trfail.Processes.Default.Command = 'curl -H host:bob.bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port) +trfail.Processes.Default.ReturnCode = 0 +trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + +# Should succeed +tr = Test.AddTestRun("bob.foo.com to server 1") +tr.StillRunningAfter = ts +tr.StillRunningAfter = server +tr.StillRunningAfter = server2 +tr.Processes.Default.Command = "curl -H host:bob.foo.com http://127.0.0.1:{0}/case1".format(ts.Variables.port) +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Should fail +trfail = Test.AddTestRun("bob.foo.com to server 2") +trfail.StillRunningAfter = ts +trfail.StillRunningAfter = server +trfail.StillRunningAfter = server2 +trfail.Processes.Default.Command = 'curl -H host:bob.foo.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port) +trfail.Processes.Default.ReturnCode = 0 +trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + +# Should succeed +tr = Test.AddTestRun("random.bar.com to server 2") +tr.StillRunningAfter = ts +tr.StillRunningAfter = server +tr.StillRunningAfter = server2 +tr.Processes.Default.Command = "curl -H host:random.bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port) +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Should fail +trfail = Test.AddTestRun("random.bar.com to server 1") +trfail.StillRunningAfter = ts +trfail.StillRunningAfter = server +trfail.StillRunningAfter = server2 +trfail.Processes.Default.Command = 'curl -H host:random.bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port) +trfail.Processes.Default.ReturnCode = 0 +trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + +# Should fail +tr = Test.AddTestRun("random.foo.com to server 2") +tr.StillRunningAfter = ts +tr.StillRunningAfter = server +tr.StillRunningAfter = server2 +tr.Processes.Default.Command = "curl -H host:random.foo.com http://127.0.0.1:{0}/case2".format(ts.Variables.port) +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + +# Should fail +trfail = Test.AddTestRun("random.foo.com to server 1") +trfail.StillRunningAfter = ts +trfail.StillRunningAfter = server +trfail.StillRunningAfter = server2 +trfail.Processes.Default.Command = 'curl -H host:random.foo.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port) +trfail.Processes.Default.ReturnCode = 0 +trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") diff --git a/tests/gold_tests/tls/tls_client_cert_override_plugin.test.py b/tests/gold_tests/tls/tls_client_cert_override_plugin.test.py new file mode 100644 index 00000000000..1aa46fa48d1 --- /dev/null +++ b/tests/gold_tests/tls/tls_client_cert_override_plugin.test.py @@ -0,0 +1,227 @@ +# 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. + +import subprocess +import os +Test.Summary = ''' +Test conf_remp to specify different client certificates to offer to the origin. Loading certs/keys via plugin. +''' + + +ts = Test.MakeATSProcess("ts", command="traffic_manager", select_ports=True) +cafile = "{0}/signer.pem".format(Test.RunDirectory) +cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) +server = Test.MakeOriginServer("server", + ssl=True, + options={"--clientCA": cafile, + "--clientverify": ""}, + clientcert="{0}/signed-foo.pem".format(Test.RunDirectory), + clientkey="{0}/signed-foo.key".format(Test.RunDirectory)) +server2 = Test.MakeOriginServer("server2", + ssl=True, + options={"--clientCA": cafile2, + "--clientverify": ""}, + clientcert="{0}/signed2-bar.pem".format(Test.RunDirectory), + clientkey="{0}/signed-bar.key".format(Test.RunDirectory)) +server3 = Test.MakeOriginServer("server3") +server.Setup.Copy("ssl/signer.pem") +server.Setup.Copy("ssl/signer2.pem") +server.Setup.Copy("ssl/signed-foo.pem") +server.Setup.Copy("ssl/signed-foo.key") +server.Setup.Copy("ssl/signed2-foo.pem") +server.Setup.Copy("ssl/signed2-bar.pem") +server.Setup.Copy("ssl/signed-bar.key") +server2.Setup.Copy("ssl/signer.pem") +server2.Setup.Copy("ssl/signer2.pem") +server2.Setup.Copy("ssl/signed-foo.pem") +server2.Setup.Copy("ssl/signed-foo.key") +server2.Setup.Copy("ssl/signed2-foo.pem") +server2.Setup.Copy("ssl/signed2-bar.pem") +server2.Setup.Copy("ssl/signed-bar.key") + +request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nCache-Control: no-cache\r\n\r\n", + "timestamp": "1469733493.993", + "body": ""} +server.addResponse("sessionlog.json", request_header, response_header) +request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_header = { + "headers": "HTTP/1.1 200 OK\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": ""} +server.addResponse("sessionlog.json", request_header, response_header) + +ts.addSSLfile("ssl/server.pem") +ts.addSSLfile("ssl/server.key") +ts.addSSLfile("ssl/signed-foo.pem") +ts.addSSLfile("ssl/signed-foo.key") +ts.addSSLfile("ssl/signed2-foo.pem") +ts.addSSLfile("ssl/signed-bar.pem") +ts.addSSLfile("ssl/signed2-bar.pem") +ts.addSSLfile("ssl/signed-bar.key") + +ts.Disk.sni_yaml.AddLine('sni:') +ts.Disk.sni_yaml.AddLine('- fqdn: random') +ts.Disk.sni_yaml.AddLine(' verify_server_properties: NONE') +snipath = ts.Disk.sni_yaml.AbsPath + +Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) + +shortdir = ts.Variables.SSLDir[0:ts.Variables.SSLDir.rfind("/")] + +ts.Disk.records_config.update({ + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.cert.path': '{0}'.format(shortdir), + 'proxy.config.ssl.client.cert.filename': 'signed-foo.pem', + 'proxy.config.ssl.client.private_key.path': '{0}'.format(shortdir), + 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_secret_load|http|ssl', +}) + +ts.Disk.ssl_multicert_config.AddLine( + 'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key' +) + +ts.Disk.remap_config.AddLine( + 'map /case1 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}'.format( + server.Variables.SSL_Port, + "signed-foo.pem", + "signed-foo.key")) +ts.Disk.remap_config.AddLine( + 'map /badcase1 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}'.format( + server.Variables.SSL_Port, + "signed2-foo.pem", + "signed-foo.key")) +ts.Disk.remap_config.AddLine( + 'map /case2 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}'.format( + server2.Variables.SSL_Port, + "signed2-foo.pem", + "signed-foo.key")) +ts.Disk.remap_config.AddLine( + 'map /badcase2 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}'.format( + server2.Variables.SSL_Port, + "signed-foo.pem", + "signed-foo.key")) + +# Should succeed +tr = Test.AddTestRun("Connect with correct client cert to first server") +tr.Processes.Default.StartBefore(Test.Processes.ts) +tr.Processes.Default.StartBefore(server) +tr.Processes.Default.StartBefore(server2) +tr.StillRunningAfter = ts +tr.StillRunningAfter = server +tr.StillRunningAfter = server2 +tr.Processes.Default.Command = "curl -H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port) +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Should fail +trfail = Test.AddTestRun("Connect with bad client cert to first server") +trfail.StillRunningAfter = ts +trfail.StillRunningAfter = server +trfail.StillRunningAfter = server2 +trfail.Processes.Default.Command = 'curl -H host:example.com http://127.0.0.1:{0}/badcase1'.format(ts.Variables.port) +trfail.Processes.Default.ReturnCode = 0 +trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + +# Should succeed +trbar = Test.AddTestRun("Connect with correct client cert to second server") +trbar.StillRunningAfter = ts +trbar.StillRunningAfter = server +trbar.StillRunningAfter = server2 +trbar.Processes.Default.Command = "curl -H host:bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port) +trbar.Processes.Default.ReturnCode = 0 +trbar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Should fail +trbarfail = Test.AddTestRun("Connect with bad client cert to second server") +trbarfail.StillRunningAfter = ts +trbarfail.StillRunningAfter = server +trbarfail.StillRunningAfter = server2 +trbarfail.Processes.Default.Command = 'curl -H host:bar.com http://127.0.0.1:{0}/badcase2'.format(ts.Variables.port) +trbarfail.Processes.Default.ReturnCode = 0 +trbarfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + +# Test the case of updating certificate contents without changing file name. +trupdate = Test.AddTestRun("Update client cert file in place") +trupdate.StillRunningAfter = ts +trupdate.StillRunningAfter = server +trupdate.StillRunningAfter = server2 +# in the config/ssl directory for records.config +trupdate.Setup.CopyAs("ssl/signed-foo.pem", ".", "{0}/signed2-foo.pem".format(ts.Variables.SSLDir)) +trupdate.Processes.Default.Command = 'traffic_ctl config set proxy.config.ssl.client.cert.path {0}/; touch {1}'.format( + shortdir, snipath) +# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket +trupdate.Processes.Default.Env = ts.Env +trupdate.Processes.Default.ReturnCode = 0 + +tr2reload = Test.AddTestRun("Reload config") +tr2reload.StillRunningAfter = ts +tr2reload.StillRunningAfter = server +tr2reload.StillRunningAfter = server2 +tr2reload.Processes.Default.Command = 'traffic_ctl config reload' +# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket +tr2reload.Processes.Default.Env = ts.Env +tr2reload.Processes.Default.ReturnCode = 0 + +tr3bar = Test.AddTestRun("Make request with other foo. badcase1 should now work") +# Wait for the reload to complete +tr3bar.Processes.Default.StartBefore(server3, ready=When.FileContains(ts.Disk.diags_log.Name, 'sni.yaml finished loading', 3)) +tr3bar.StillRunningAfter = ts +tr3bar.StillRunningAfter = server +tr3bar.StillRunningAfter = server2 +tr3bar.Processes.Default.Command = 'curl -H host:foo.com http://127.0.0.1:{0}/badcase1'.format(ts.Variables.port) +tr3bar.Processes.Default.ReturnCode = 0 +tr3bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Test the hot-reload feature. Update cert file and wait about. Should update without reload +trupdate = Test.AddTestRun("Update signed-foo cert file in place") +trupdate.StillRunningAfter = ts +trupdate.StillRunningAfter = server +trupdate.Setup.CopyAs("ssl/signed2-foo.pem", ".", "{0}/signed-foo.pem".format(ts.Variables.SSLDir)) +# For some reason the Setup.CopyAs does not change the modification time, so we touch +trupdate.Processes.Default.Command = 'touch {0}/signed-foo.pem {0}/signed-foo.key'.format(ts.Variables.SSLDir) +# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket +trupdate.Processes.Default.Env = ts.Env +trupdate.Processes.Default.ReturnCode = 0 + +# The plugin will pull every 3 seconds. So wait 4 seconds and test again. +# case1 should fail +# badcase1 should succeed +tr = Test.AddTestRun("Retest case1") +tr.DelayStart = 4 +tr.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = server2 +tr.StillRunningAfter = ts +tr.Processes.Default.Command = "curl -H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port) +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + +tr = Test.AddTestRun("Retest badcase1") +tr.ReturnCode = 0 +tr.StillRunningAfter = server +tr.StillRunningAfter = server2 +tr.StillRunningAfter = ts +tr.Processes.Default.Command = "curl -H host:example.com http://127.0.0.1:{0}/badcase1".format(ts.Variables.port) +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") diff --git a/tests/gold_tests/tls/tls_client_cert_plugin.test.py b/tests/gold_tests/tls/tls_client_cert_plugin.test.py new file mode 100644 index 00000000000..e493b97a65f --- /dev/null +++ b/tests/gold_tests/tls/tls_client_cert_plugin.test.py @@ -0,0 +1,297 @@ +''' +Test offering client cert to origin, but using plugin for cert loading +''' +# 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. + +import subprocess +import os + +Test.Summary = ''' +Test offering client cert to origin, but using plugin for cert loading +''' + +ts = Test.MakeATSProcess("ts", command="traffic_manager", select_ports=True) +cafile = "{0}/signer.pem".format(Test.RunDirectory) +cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) +# --clientverify: "" empty string because microserver does store_true for argparse, but options is a dictionary +server = Test.MakeOriginServer("server", + ssl=True, + options={"--clientCA": cafile, + "--clientverify": ""}, + clientcert="{0}/signed-foo.pem".format(Test.RunDirectory), + clientkey="{0}/signed-foo.key".format(Test.RunDirectory)) +server2 = Test.MakeOriginServer("server2", + ssl=True, + options={"--clientCA": cafile2, + "--clientverify": ""}, + clientcert="{0}/signed2-bar.pem".format(Test.RunDirectory), + clientkey="{0}/signed-bar.key".format(Test.RunDirectory)) +server3 = Test.MakeOriginServer("server3") +server.Setup.Copy("ssl/signer.pem") +server.Setup.Copy("ssl/signer2.pem") +server.Setup.Copy("ssl/signed-foo.pem") +server.Setup.Copy("ssl/signed-foo.key") +server.Setup.Copy("ssl/signed2-foo.pem") +server.Setup.Copy("ssl/signed2-bar.pem") +server.Setup.Copy("ssl/signed-bar.key") +server2.Setup.Copy("ssl/signer.pem") +server2.Setup.Copy("ssl/signer2.pem") +server2.Setup.Copy("ssl/signed-foo.pem") +server2.Setup.Copy("ssl/signed-foo.key") +server2.Setup.Copy("ssl/signed2-foo.pem") +server2.Setup.Copy("ssl/signed2-bar.pem") +server2.Setup.Copy("ssl/signed-bar.key") + +request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +server.addResponse("sessionlog.json", request_header, response_header) +request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +server.addResponse("sessionlog.json", request_header, response_header) + +# +# Certs and keys loaded into the ts/ssl directory, but the paths in the +# configs omit the ssl subdirectory. The ssl_secret_load_test plugin adds this back in +ts.addSSLfile("ssl/server.pem") +ts.addSSLfile("ssl/server.key") +ts.addSSLfile("ssl/signed-foo.pem") +ts.addSSLfile("ssl/signed-foo.key") +ts.addSSLfile("ssl/signed2-foo.pem") +ts.addSSLfile("ssl/signed-bar.pem") +ts.addSSLfile("ssl/signed2-bar.pem") +ts.addSSLfile("ssl/signed-bar.key") + +Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) + +ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_secret_load_test|ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.cert.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.filename': 'signed-foo.pem', + 'proxy.config.ssl.client.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, +}) + +ts.Disk.ssl_multicert_config.AddLine( + 'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key' +) + +ts.Disk.remap_config.AddLine( + 'map /case1 https://127.0.0.1:{0}/'.format(server.Variables.SSL_Port) +) +ts.Disk.remap_config.AddLine( + 'map /case2 https://127.0.0.1:{0}/'.format(server2.Variables.SSL_Port) +) + +ts.Disk.sni_yaml.AddLine( + 'sni:') +ts.Disk.sni_yaml.AddLine( + '- fqdn: bar.com') +ts.Disk.sni_yaml.AddLine( + ' client_cert: {0}/../signed2-bar.pem'.format(ts.Variables.SSLDir)) +ts.Disk.sni_yaml.AddLine( + ' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir)) + + +# Should succeed +tr = Test.AddTestRun("Connect with first client cert to first server") +tr.Processes.Default.StartBefore(Test.Processes.ts) +tr.Processes.Default.StartBefore(server) +tr.Processes.Default.StartBefore(server2) +tr.StillRunningAfter = ts +tr.StillRunningAfter = server +tr.StillRunningAfter = server2 +tr.Processes.Default.Command = "curl -H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port) +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Should fail +trfail = Test.AddTestRun("Connect with first client cert to second server") +trfail.StillRunningAfter = ts +trfail.StillRunningAfter = server +trfail.StillRunningAfter = server2 +trfail.Processes.Default.Command = 'curl -H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port) +trfail.Processes.Default.ReturnCode = 0 +trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + +# Should succeed +trbar = Test.AddTestRun("Connect with signed2 bar to second server") +trbar.StillRunningAfter = ts +trbar.StillRunningAfter = server +trbar.StillRunningAfter = server2 +trbar.Processes.Default.Command = "curl -H host:bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port) +trbar.Processes.Default.ReturnCode = 0 +trbar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Should fail +trbarfail = Test.AddTestRun("Connect with signed2 bar cert to first server") +trbarfail.StillRunningAfter = ts +trbarfail.StillRunningAfter = server +trbarfail.StillRunningAfter = server2 +trbarfail.Processes.Default.Command = 'curl -H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port) +trbarfail.Processes.Default.ReturnCode = 0 +trbarfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + +tr2 = Test.AddTestRun("Update config files") +# Update the SNI config +snipath = ts.Disk.sni_yaml.AbsPath +recordspath = ts.Disk.records_config.AbsPath +tr2.Disk.File(snipath, id="sni_yaml", typename="ats:config"), +tr2.Disk.sni_yaml.AddLine( + 'sni:') +tr2.Disk.sni_yaml.AddLine( + '- fqdn: bar.com') +tr2.Disk.sni_yaml.AddLine( + ' client_cert: {0}/../signed-bar.pem'.format(ts.Variables.SSLDir)) +tr2.Disk.sni_yaml.AddLine( + ' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir)) +# recreate the records.config with the cert filename changed +tr2.Disk.File(recordspath, id="records_config", typename="ats:config:records"), +tr2.Disk.records_config.update({ + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.cert.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.filename': 'signed2-foo.pem', + 'proxy.config.ssl.client.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_secret_load_test|ssl', +}) +tr2.StillRunningAfter = ts +tr2.StillRunningAfter = server +tr2.StillRunningAfter = server2 +tr2.Processes.Default.Command = 'echo Updated configs' +# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket +tr2.Processes.Default.Env = ts.Env +tr2.Processes.Default.ReturnCode = 0 + +tr2reload = Test.AddTestRun("Reload config") +tr2reload.StillRunningAfter = ts +tr2reload.StillRunningAfter = server +tr2reload.StillRunningAfter = server2 +tr2reload.Processes.Default.Command = 'traffic_ctl config reload' +# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket +tr2reload.Processes.Default.Env = ts.Env +tr2reload.Processes.Default.ReturnCode = 0 + +# Should succeed +tr3bar = Test.AddTestRun("Make request with other bar cert to first server") +# Wait for the reload to complete +tr3bar.Processes.Default.StartBefore(server3, ready=When.FileContains(ts.Disk.diags_log.Name, 'sni.yaml finished loading', 3)) +tr3bar.StillRunningAfter = ts +tr3bar.StillRunningAfter = server +tr3bar.StillRunningAfter = server2 +tr3bar.Processes.Default.Command = 'curl -H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port) +tr3bar.Processes.Default.ReturnCode = 0 +tr3bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Should fail +tr3barfail = Test.AddTestRun("Make request with other bar cert to second server") +tr3barfail.StillRunningAfter = ts +tr3barfail.StillRunningAfter = server +tr3barfail.StillRunningAfter = server2 +tr3barfail.Processes.Default.Command = 'curl -H host:bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port) +tr3barfail.Processes.Default.ReturnCode = 0 +tr3barfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + +# Should succeed +tr3 = Test.AddTestRun("Make request with other cert to second server") +# Wait for the reload to complete +tr3.StillRunningAfter = ts +tr3.StillRunningAfter = server +tr3.StillRunningAfter = server2 +tr3.Processes.Default.Command = 'curl -H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port) +tr3.Processes.Default.ReturnCode = 0 +tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Should fail +tr3fail = Test.AddTestRun("Make request with other cert to first server") +tr3fail.StillRunningAfter = ts +tr3fail.StillRunningAfter = server +tr3fail.StillRunningAfter = server2 +tr3fail.Processes.Default.Command = 'curl -H host:example.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port) +tr3fail.Processes.Default.ReturnCode = 0 +tr3fail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + +# Test the case of updating certificate contents without changing file name. +trupdate = Test.AddTestRun("Update client cert file in place") +trupdate.StillRunningAfter = ts +trupdate.StillRunningAfter = server +trupdate.StillRunningAfter = server2 +# Make a meaningless config change on the path so the records.config reload logic will trigger +trupdate.Setup.CopyAs("ssl/signed2-bar.pem", ".", "{0}/signed-bar.pem".format(ts.Variables.SSLDir)) +# in the config/ssl directory for records.config +trupdate.Setup.CopyAs("ssl/signed-foo.pem", ".", "{0}/signed2-foo.pem".format(ts.Variables.SSLDir)) +trupdate.Processes.Default.Command = 'traffic_ctl config set proxy.config.ssl.client.cert.path {0}/../; touch {1}; touch {2}'.format( + ts.Variables.SSLDir, snipath, recordspath) +# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket +trupdate.Processes.Default.Env = ts.Env +trupdate.Processes.Default.ReturnCode = 0 + +trreload = Test.AddTestRun("Reload config after renaming certs") +trreload.StillRunningAfter = ts +trreload.StillRunningAfter = server +trreload.StillRunningAfter = server2 +trreload.Processes.Default.Command = 'traffic_ctl config reload' +trreload.Processes.Default.Env = ts.Env +trreload.Processes.Default.ReturnCode = 0 + +# Should succeed +tr4bar = Test.AddTestRun("Make request with renamed bar cert to second server") +# Wait for the reload to complete +tr4bar.DelayStart = 10 +tr4bar.StillRunningAfter = ts +tr4bar.StillRunningAfter = server +tr4bar.StillRunningAfter = server2 +tr4bar.Processes.Default.Command = 'curl -H host:bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port) +tr4bar.Processes.Default.ReturnCode = 0 +tr4bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Should fail +tr4barfail = Test.AddTestRun("Make request with renamed bar cert to first server") +tr4barfail.StillRunningAfter = ts +tr4barfail.StillRunningAfter = server +tr4barfail.StillRunningAfter = server2 +tr4barfail.Processes.Default.Command = 'curl -H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port) +tr4barfail.Processes.Default.ReturnCode = 0 +tr4barfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + +# Should succeed +tr4 = Test.AddTestRun("Make request with renamed foo cert to first server") +tr4.StillRunningAfter = ts +tr4.StillRunningAfter = server +tr4.StillRunningAfter = server2 +tr4.Processes.Default.Command = 'curl -H host:example.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port) +tr4.Processes.Default.ReturnCode = 0 +tr4.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + +# Should fail +tr4fail = Test.AddTestRun("Make request with renamed foo cert to second server") +tr4fail.StillRunningAfter = ts +tr4fail.StillRunningAfter = server +tr4fail.StillRunningAfter = server2 +tr4fail.Processes.Default.Command = 'curl -H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port) +tr4fail.Processes.Default.ReturnCode = 0 +tr4fail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") diff --git a/tests/gold_tests/tls/tls_keepalive.test.py b/tests/gold_tests/tls/tls_keepalive.test.py index ac51424781c..a18d7bba95d 100644 --- a/tests/gold_tests/tls/tls_keepalive.test.py +++ b/tests/gold_tests/tls/tls_keepalive.test.py @@ -66,7 +66,7 @@ '''.split("\n") ) -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-preaccept=1') +Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) tr = Test.AddTestRun("Test two HTTP/1.1 requests over one TLS connection") tr.Processes.Default.StartBefore(server) diff --git a/tests/tools/plugins/Makefile.inc b/tests/tools/plugins/Makefile.inc index e90c045a62a..9659e7b39ba 100644 --- a/tests/tools/plugins/Makefile.inc +++ b/tests/tools/plugins/Makefile.inc @@ -53,6 +53,9 @@ tools_plugins_ssl_client_verify_test_la_SOURCES = tools/plugins/ssl_client_verif noinst_LTLIBRARIES += tools/plugins/ssl_hook_test.la tools_plugins_ssl_hook_test_la_SOURCES = tools/plugins/ssl_hook_test.cc +noinst_LTLIBRARIES += tools/plugins/ssl_secret_load_test.la +tools_plugins_ssl_secret_load_test_la_SOURCES = tools/plugins/ssl_secret_load_test.cc + noinst_LTLIBRARIES += tools/plugins/ssl_verify_test.la tools_plugins_ssl_verify_test_la_SOURCES = tools/plugins/ssl_verify_test.cc diff --git a/tests/tools/plugins/ssl_secret_load_test.cc b/tests/tools/plugins/ssl_secret_load_test.cc new file mode 100644 index 00000000000..654a717dbb1 --- /dev/null +++ b/tests/tools/plugins/ssl_secret_load_test.cc @@ -0,0 +1,184 @@ +/** @file + + SSL Preaccept test plugin + Implements blind tunneling based on the client IP address + The client ip addresses are specified in the plugin's + config file as an array of IP addresses or IP address ranges under the + key "client-blind-tunnel" + + @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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PN "ssl_secret_load_test" +#define PCP "[" PN " Plugin] " + +// Map of secret name to last modified time +std::unordered_map secret_versions; + +void +update_file_name(const std::string_view &path, std::string &newname) +{ + // insert the "ssl" directory into the path + auto offset = path.find_last_of("/"); + if (offset == std::string::npos) { + newname = "ssl/"; + newname.append(path); + } else { + newname = path.substr(0, offset + 1); + newname.append("ssl/"); + newname.append(path.substr(offset + 1)); + } +} + +bool +load_file(const std::string &newname, struct stat *statdata, std::string &data_item) +{ + if (stat(newname.c_str(), statdata) < 0) { + return false; + } + + int fd = open(newname.c_str(), O_RDONLY); + if (fd < 0) { + TSDebug(PN, "Failed to load %s", newname.c_str()); + return false; + } + size_t total_size = statdata->st_size; + data_item.resize(total_size); + size_t offset = 0; + char *data = data_item.data(); + while (offset < total_size) { + int num_read = read(fd, data + offset, total_size - offset); + if (num_read < 0) { + close(fd); + return false; + } + offset += num_read; + } + close(fd); + return true; +} + +int +CB_Load_Secret(TSCont cont, TSEvent event, void *edata) +{ + TSSecretID *id = reinterpret_cast(edata); + + TSDebug(PN, "Load secret for %*.s", static_cast(id->cert_name_len), id->cert_name); + + std::string newname; + std::string data_item; + struct stat statdata; + + update_file_name(std::string_view{id->cert_name, id->cert_name_len}, newname); + + TSDebug(PN, "Really load secret for %s", newname.c_str()); + + // Load the secret and add it to the map + if (!load_file(newname, &statdata, data_item)) { + return TS_ERROR; + } + secret_versions.insert(std::make_pair(std::string{id->cert_name, id->cert_name_len}, statdata.st_mtime)); + + TSSslSecretSet(id->cert_name, id->cert_name_len, data_item.data(), data_item.size()); + + if (id->key_name_len > 0) { + TSDebug(PN, "Load secret for %*.s", static_cast(id->key_name_len), id->key_name); + update_file_name(std::string_view{id->key_name, id->key_name_len}, newname); + + TSDebug(PN, "Really load secret for %s", newname.c_str()); + + // Load the secret and add it to the map + if (!load_file(newname, &statdata, data_item)) { + return TS_ERROR; + } + secret_versions.insert(std::make_pair(std::string{id->key_name, id->key_name_len}, statdata.st_mtime)); + + TSSslSecretSet(id->key_name, id->key_name_len, data_item.data(), data_item.size()); + } + + return TS_SUCCESS; +} + +int +CB_Update_Secret(TSCont cont, TSEvent event, void *edata) +{ + std::vector updates; + for (auto iter = secret_versions.begin(); iter != secret_versions.end(); ++iter) { + std::string newname; + std::string data_item; + struct stat statdata; + + update_file_name(iter->first, newname); + TSDebug(PN, "check secret for %s, really %s", iter->first.c_str(), newname.c_str()); + + if (stat(newname.c_str(), &statdata) < 0) { + continue; + } + + if (statdata.st_mtime > iter->second) { + TSDebug(PN, "check secret %s has been updated", newname.c_str()); + if (!load_file(newname, &statdata, data_item)) { + continue; + } + TSSslSecretSet(iter->first.c_str(), iter->first.length(), data_item.data(), data_item.size()); + updates.push_back(iter->first); + iter->second = statdata.st_mtime; + } + } + for (auto name : updates) { + TSDebug(PN, "update cert for secret %s", name.c_str()); + TSSslSecretUpdate(name.c_str(), name.length()); + } + TSContScheduleOnPool(cont, 3000, TS_THREAD_POOL_TASK); + return TS_SUCCESS; +} + +// Called by ATS as our initialization point +void +TSPluginInit(int argc, const char *argv[]) +{ + TSPluginRegistrationInfo info; + info.plugin_name = const_cast("SSL secret load test"); + info.vendor_name = const_cast("apache"); + info.support_email = const_cast("shinrich@apache.org"); + if (TSPluginRegister(&info) != TS_SUCCESS) { + TSError("[%s] Plugin registration failed", PN); + } + + TSCont cb = TSContCreate(&CB_Load_Secret, nullptr); + TSLifecycleHookAdd(TS_LIFECYCLE_SSL_SECRET_HOOK, cb); + + // Scheduled a call back to trigger every 3 seconds to look for changes to the files + TSCont cb_update = TSContCreate(&CB_Update_Secret, TSMutexCreate()); + TSContScheduleOnPool(cb_update, 3000, TS_THREAD_POOL_TASK); + + return; +}