#include "sslsocket.h" #include "../client.h" #include "../exceptions.h" #include #include #include #include #include namespace { std::string getCertificateInfo(X509* cert) { if (!cert) return "No certificate"; std::unique_ptr mem_bio(BIO_new(BIO_s_mem()), &BIO_free); X509_print(mem_bio.get(), cert); char * data = nullptr; auto len = BIO_get_mem_data(mem_bio.get(), &data); if (len < 0) return "Can't get certificate info due to BIO error " + std::to_string(len); return std::string(data, len); } void throwSSLError(SSL * ssl, int error, const char * /*location*/, const char * /*statement*/, const std::string prefix = "OpenSSL error: ") { const auto detail_error = ERR_get_error(); auto reason = ERR_reason_error_string(detail_error); reason = reason ? reason : "Unknown SSL error"; std::string reason_str = reason; if (ssl) { // Print certificate only if handshake isn't completed if (auto ssl_session = SSL_get_session(ssl); ssl_session && SSL_get_state(ssl) != TLS_ST_OK) reason_str += "\nServer certificate: " + getCertificateInfo(SSL_SESSION_get0_peer(ssl_session)); } // std::cerr << "!!! SSL error at " << location // << "\n\tcaused by " << statement // << "\n\t: "<< reason_str << "(" << error << ")" // << "\n\t last err: " << ERR_peek_last_error() // << std::endl; throw clickhouse::OpenSSLError(prefix + std::to_string(error) + " : " + reason_str); } void configureSSL(const clickhouse::SSLParams::ConfigurationType & configuration, SSL * ssl, SSL_CTX * context = nullptr) { std::unique_ptr conf_ctx_holder(SSL_CONF_CTX_new(), SSL_CONF_CTX_free); auto conf_ctx = conf_ctx_holder.get(); // To make both cmdline and flag file commands start with no prefix. SSL_CONF_CTX_set1_prefix(conf_ctx, ""); // Allow all set of client commands, also turn on proper error reporting to reuse throwSSLError(). SSL_CONF_CTX_set_flags(conf_ctx, SSL_CONF_FLAG_CMDLINE | SSL_CONF_FLAG_FILE | SSL_CONF_FLAG_CLIENT | SSL_CONF_FLAG_SHOW_ERRORS | SSL_CONF_FLAG_CERTIFICATE ); if (ssl) SSL_CONF_CTX_set_ssl(conf_ctx, ssl); else if (context) SSL_CONF_CTX_set_ssl_ctx(conf_ctx, context); for (const auto & kv : configuration) { const int err = SSL_CONF_cmd(conf_ctx, kv.first.c_str(), (kv.second ? kv.second->c_str() : nullptr)); // From the documentation: // 2 - both key and value used // 1 - only key used // 0 - error during processing // -2 - key not recodnized // -3 - missing value const bool value_present = !!kv.second; if (err == 2 || (err == 1 && !value_present)) continue; else if (err == 0) throwSSLError(ssl, SSL_ERROR_NONE, nullptr, nullptr, "Failed to configure OpenSSL with command '" + kv.first + "' "); else if (err == 1 && value_present) throw clickhouse::OpenSSLError("Failed to configure OpenSSL: command '" + kv.first + "' needs no value"); else if (err == -2) throw clickhouse::OpenSSLError("Failed to configure OpenSSL: unknown command '" + kv.first + "'"); else if (err == -3) throw clickhouse::OpenSSLError("Failed to configure OpenSSL: command '" + kv.first + "' requires a value"); else throw clickhouse::OpenSSLError("Failed to configure OpenSSL: command '" + kv.first + "' unknown error: " + std::to_string(err)); } } #define STRINGIFY_HELPER(x) #x #define STRINGIFY(x) STRINGIFY_HELPER(x) #define LOCATION __FILE__ ":" STRINGIFY(__LINE__) struct SSLInitializer { SSLInitializer() { SSL_library_init(); SSLeay_add_ssl_algorithms(); SSL_load_error_strings(); } }; SSL_CTX * prepareSSLContext(const clickhouse::SSLParams & context_params) { static const SSLInitializer ssl_initializer; const SSL_METHOD *method = TLS_client_method(); std::unique_ptr ctx(SSL_CTX_new(method), &SSL_CTX_free); if (!ctx) throw clickhouse::OpenSSLError("Failed to initialize SSL context"); #define HANDLE_SSL_CTX_ERROR(statement) do { \ if (const auto ret_code = (statement); !ret_code) \ throwSSLError(nullptr, static_cast(ERR_peek_error()), LOCATION, #statement); \ } while(false); if (context_params.use_default_ca_locations) HANDLE_SSL_CTX_ERROR(SSL_CTX_set_default_verify_paths(ctx.get())); if (!context_params.path_to_ca_directory.empty()) HANDLE_SSL_CTX_ERROR( SSL_CTX_load_verify_locations( ctx.get(), nullptr, context_params.path_to_ca_directory.c_str()) ); for (const auto & f : context_params.path_to_ca_files) HANDLE_SSL_CTX_ERROR(SSL_CTX_load_verify_locations(ctx.get(), f.c_str(), nullptr)); if (context_params.context_options != -1) SSL_CTX_set_options(ctx.get(), context_params.context_options); if (context_params.min_protocol_version != -1) HANDLE_SSL_CTX_ERROR( SSL_CTX_set_min_proto_version(ctx.get(), context_params.min_protocol_version)); if (context_params.max_protocol_version != -1) HANDLE_SSL_CTX_ERROR( SSL_CTX_set_max_proto_version(ctx.get(), context_params.max_protocol_version)); return ctx.release(); #undef HANDLE_SSL_CTX_ERROR } auto convertConfiguration(const decltype(clickhouse::ClientOptions::SSLOptions::configuration) & configuration) { auto result = decltype(clickhouse::SSLParams::configuration){}; for (const auto & cv : configuration) result.push_back({cv.command, cv.value}); return result; } clickhouse::SSLParams GetSSLParams(const clickhouse::ClientOptions& opts) { const auto& ssl_options = *opts.ssl_options; return clickhouse::SSLParams{ ssl_options.path_to_ca_files, ssl_options.path_to_ca_directory, ssl_options.use_default_ca_locations, ssl_options.context_options, ssl_options.min_protocol_version, ssl_options.max_protocol_version, ssl_options.use_sni, ssl_options.skip_verification, ssl_options.host_flags, convertConfiguration(ssl_options.configuration) }; } } namespace clickhouse { SSLContext::SSLContext(SSL_CTX & context) : context_(&context, &SSL_CTX_free) { SSL_CTX_up_ref(context_.get()); } SSLContext::SSLContext(const SSLParams & context_params) : context_(prepareSSLContext(context_params), &SSL_CTX_free) { } SSL_CTX * SSLContext::getContext() { return context_.get(); } // Allows caller to use returned value of `statement` if there was no error, throws exception otherwise. #define HANDLE_SSL_ERROR(SSL_PTR, statement) [&] { \ if (const auto ret_code = (statement); ret_code <= 0) { \ throwSSLError(SSL_PTR, SSL_get_error(SSL_PTR, static_cast(ret_code)), LOCATION, #statement); \ return static_cast>(0); \ } \ else \ return ret_code; \ } () /* // debug macro for tracing SSL state #define LOG_SSL_STATE() std::cerr << "!!!!" << LOCATION << " @" << __FUNCTION__ \ << "\t" << SSL_get_version(ssl_) << " state: " << SSL_state_string_long(ssl_) \ << "\n\t handshake state: " << SSL_get_state(ssl_) \ << std::endl */ SSLSocket::SSLSocket(const NetworkAddress& addr, const SocketTimeoutParams& timeout_params, const SSLParams & ssl_params, SSLContext& context) : Socket(addr, timeout_params) , ssl_(SSL_new(context.getContext()), &SSL_free) { auto ssl = ssl_.get(); if (!ssl) throw clickhouse::OpenSSLError("Failed to create SSL instance"); std::unique_ptr ip_addr(a2i_IPADDRESS(addr.Host().c_str()), &ASN1_OCTET_STRING_free); HANDLE_SSL_ERROR(ssl, SSL_set_fd(ssl, static_cast(handle_))); if (ssl_params.use_SNI) HANDLE_SSL_ERROR(ssl, SSL_set_tlsext_host_name(ssl, addr.Host().c_str())); if (ssl_params.host_flags != -1) SSL_set_hostflags(ssl, ssl_params.host_flags); HANDLE_SSL_ERROR(ssl, SSL_set1_host(ssl, addr.Host().c_str())); // DO NOT use SSL_set_verify(ssl, SSL_VERIFY_PEER, nullptr), since // we check verification result later, and that provides better error message. if (ssl_params.configuration.size() > 0) configureSSL(ssl_params.configuration, ssl); SSL_set_connect_state(ssl); HANDLE_SSL_ERROR(ssl, SSL_connect(ssl)); HANDLE_SSL_ERROR(ssl, SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY)); if (const auto verify_result = SSL_get_verify_result(ssl); !ssl_params.skip_verification && verify_result != X509_V_OK) { auto error_message = X509_verify_cert_error_string(verify_result); throw clickhouse::OpenSSLError("Failed to verify SSL connection, X509_v error: " + std::to_string(verify_result) + " " + error_message + "\nServer certificate: " + getCertificateInfo(SSL_get_peer_certificate(ssl))); } // Host name verification is done by OpenSSL itself, however if we are connecting to an ip-address, // no verification is made, so we have to do it manually. // Just in case if this is ever required, leave it here commented out. // if (ip_addr) { // // if hostname is actually an IP address // HANDLE_SSL_ERROR(ssl, X509_check_ip( // SSL_get_peer_certificate(ssl), // ASN1_STRING_get0_data(ip_addr.get()), // ASN1_STRING_length(ip_addr.get()), // 0)); // } } void SSLSocket::validateParams(const SSLParams & ssl_params) { // We need either SSL or SSL_CTX to properly validate configuration, so create a temporary one. std::unique_ptr ctx(SSL_CTX_new(TLS_client_method()), &SSL_CTX_free); configureSSL(ssl_params.configuration, nullptr, ctx.get()); } SSLSocketFactory::SSLSocketFactory(const ClientOptions& opts) : NonSecureSocketFactory() , ssl_params_(GetSSLParams(opts)) { if (opts.ssl_options->ssl_context) { ssl_context_ = std::make_unique(*opts.ssl_options->ssl_context); } else { ssl_context_ = std::make_unique(ssl_params_); } } SSLSocketFactory::~SSLSocketFactory() = default; std::unique_ptr SSLSocketFactory::doConnect(const NetworkAddress& address, const ClientOptions& opts) { SocketTimeoutParams timeout_params { opts.connection_connect_timeout, opts.connection_recv_timeout, opts.connection_send_timeout }; return std::make_unique(address, timeout_params, ssl_params_, *ssl_context_); } std::unique_ptr SSLSocket::makeInputStream() const { return std::make_unique(ssl_.get()); } std::unique_ptr SSLSocket::makeOutputStream() const { return std::make_unique(ssl_.get()); } SSLSocketInput::SSLSocketInput(SSL *ssl) : ssl_(ssl) {} size_t SSLSocketInput::DoRead(void* buf, size_t len) { size_t actually_read; HANDLE_SSL_ERROR(ssl_, SSL_read_ex(ssl_, buf, len, &actually_read)); return actually_read; } SSLSocketOutput::SSLSocketOutput(SSL *ssl) : ssl_(ssl) {} size_t SSLSocketOutput::DoWrite(const void* data, size_t len) { if (len > std::numeric_limits::max()) // FIXME(vnemkov): We should do multiple `SSL_write`s in this case. throw AssertionError("Failed to write too big chunk at once " + std::to_string(len) + " > " + std::to_string(std::numeric_limits::max())); return static_cast(HANDLE_SSL_ERROR(ssl_, SSL_write(ssl_, data, static_cast(len)))); } #undef HANDLE_SSL_ERROR }