/** * Copyright (C) 2018 MongoDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the GNU Affero General Public License in all respects * for all of the code used other than as permitted herein. If you modify * file(s) with this exception, you may extend this exception to your * version of the file(s), but you are not obligated to do so. If you do not * wish to do so, delete this exception statement from your version. If you * delete this exception statement from all source files in the program, * then also delete it in the license file. */ #pragma once #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kNetwork #include "asio/detail/config.hpp" #include "asio/detail/push_options.hpp" #include "asio/detail/throw_error.hpp" #include "asio/error.hpp" #include "mongo/util/log.h" #include "mongo/util/mongoutils/str.h" #include "mongo/util/net/ssl/apple.hpp" #include "mongo/util/net/ssl/detail/engine.hpp" #include "mongo/util/net/ssl/error.hpp" namespace asio { namespace ssl { namespace detail { namespace { const class osstatus_category : public error_category { public: const char* name() const noexcept final { return "Secure.Transport"; } std::string message(int value) const noexcept final { const auto status = static_cast<::OSStatus>(value); apple::CFUniquePtr<::CFStringRef> errstr(::SecCopyErrorMessageString(status, nullptr)); if (!errstr) { return mongo::str::stream() << "Secure.Transport unknown error: " << static_cast(status); } const auto len = ::CFStringGetMaximumSizeForEncoding(::CFStringGetLength(errstr.get()), ::kCFStringEncodingUTF8); std::string ret; ret.resize(len + 1); if (!::CFStringGetCString(errstr.get(), &ret[0], len, ::kCFStringEncodingUTF8)) { return mongo::str::stream() << "Secure.Transport unknown error: " << static_cast(status); } ret.resize(strlen(ret.c_str())); return mongo::str::stream() << "Secure.Transport: " << ret; } } OSStatus_category; asio::error_code errorCode(::OSStatus status) { return asio::error_code(static_cast(status), OSStatus_category); } /** * Verify that an SSL session is ready for I/O (state: Connected). * In all other states, asio should be speaking to the socket directly. */ bool verifyConnected(::SSLContextRef ssl, asio::error_code* ec) { auto state = ::kSSLAborted; auto status = ::SSLGetSessionState(ssl, &state); if (status != ::errSecSuccess) { // Unable to determine session state. *ec = errorCode(status); return false; } switch (state) { case ::kSSLIdle: *ec = asio::error::not_connected; return false; case ::kSSLHandshake: *ec = asio::error::in_progress; return false; case ::kSSLConnected: return true; case ::kSSLClosed: *ec = asio::error::shut_down; return false; case ::kSSLAborted: *ec = asio::error::connection_aborted; return false; default: // Undefined state, call it an internal error. *ec = errorCode(::errSSLInternal); return false; } } } // namespace engine::engine(context::native_handle_type context, const std::string& remoteHostName) : _remoteHostName(remoteHostName) { if (context) { if (context->certs) { ::CFRetain(context->certs.get()); _certs.reset(context->certs.get()); } _protoMin = context->protoMin; _protoMax = context->protoMax; } else { apple::Context def; _protoMin = def.protoMin; _protoMax = def.protoMax; } } bool engine::_initSSL(stream_base::handshake_type type, asio::error_code& ec) { if (_ssl) { return true; } const auto side = (type == stream_base::client) ? ::kSSLClientSide : ::kSSLServerSide; _ssl.reset(::SSLCreateContext(nullptr, side, ::kSSLStreamType)); if (!_ssl) { mongo::error() << "Failed allocating SSLContext"; ec = errorCode(::errSSLInternal); return false; } auto status = ::SSLSetConnection(_ssl.get(), static_cast(this)); if (_certs && (status == ::errSecSuccess)) { status = ::SSLSetCertificate(_ssl.get(), _certs.get()); } if (status == ::errSecSuccess) { status = ::SSLSetPeerID(_ssl.get(), _ssl.get(), sizeof(native_handle_type)); } if (status == ::errSecSuccess) { status = ::SSLSetIOFuncs(_ssl.get(), read_func, write_func); } if (status == ::errSecSuccess) { status = ::SSLSetProtocolVersionMin(_ssl.get(), _protoMin); } if (status == ::errSecSuccess) { status = ::SSLSetProtocolVersionMax(_ssl.get(), _protoMax); } if (status == ::errSecSuccess) { status = ::SSLSetClientSideAuthenticate(_ssl.get(), ::kTryAuthenticate); } if (status == ::errSecSuccess) { status = ::SSLSetSessionOption(_ssl.get(), ::kSSLSessionOptionBreakOnServerAuth, true); } if (status == ::errSecSuccess) { status = ::SSLSetSessionOption(_ssl.get(), ::kSSLSessionOptionBreakOnClientAuth, true); } if (!_remoteHostName.empty() && (status == ::errSecSuccess)) { status = ::SSLSetPeerDomainName(_ssl.get(), _remoteHostName.c_str(), _remoteHostName.size()); } if (status != ::errSecSuccess) { _ssl.reset(nullptr); ec = errorCode(status); return false; } return true; } engine::want engine::handshake(stream_base::handshake_type type, asio::error_code& ec) { if (!_initSSL(type, ec)) { // Error happened, ec has been set. return want::want_nothing; } // We use BreakOnClientAuth and BreakOnServerAuth above to // convince the OS not to validate the certs for us. // In practice, we'll be validating the peer in ssl_manager_apple.cpp later. // As a side effect, we have to call SSLHandshake up to three times. // Breaking once for client auth, then for server auth, and finally on completion. ::OSStatus status; do { status = ::SSLHandshake(_ssl.get()); } while ((status == ::errSSLServerAuthCompleted) || (status == ::errSSLClientAuthCompleted)); if (status == ::errSSLWouldBlock) { return wouldBlock(); } if (status != ::errSecSuccess) { _ssl.reset(nullptr); ec = errorCode(status); return want::want_nothing; } return _outbuf.size() ? want::want_output : want::want_nothing; } engine::want engine::shutdown(asio::error_code& ec) { if (_ssl) { const auto status = ::SSLClose(_ssl.get()); if (status == ::errSSLWouldBlock) { return wouldBlock(); } if (status == ::errSecSuccess) { _ssl.reset(nullptr); } else { ec = errorCode(status); } } else { mongo::error() << "SSL connection already shut down"; ec = errorCode(::errSSLInternal); } return want::want_nothing; } const asio::error_code& engine::map_error_code(asio::error_code& ec) const { if (ec != asio::error::eof) { return ec; } if (_inbuf.size() || _outbuf.size()) { ec = asio::ssl::error::stream_truncated; return ec; } invariant(_ssl); auto state = ::kSSLAborted; const auto status = ::SSLGetSessionState(_ssl.get(), &state); if (status != ::errSecSuccess) { ec = errorCode(status); return ec; } if (state == ::kSSLConnected) { ec = asio::ssl::error::stream_truncated; return ec; } return ec; } engine::want engine::write(const asio::const_buffer& data, asio::error_code& ec, std::size_t& bytes_transferred) { if (!verifyConnected(_ssl.get(), &ec)) { return want::want_nothing; } const auto status = ::SSLWrite(_ssl.get(), data.data(), data.size(), &bytes_transferred); if (status == ::errSSLWouldBlock) { return (bytes_transferred < data.size()) ? want::want_output_and_retry : want::want_nothing; } if (status != ::errSecSuccess) { ec = errorCode(status); } return _outbuf.size() ? want::want_output : want::want_nothing; } asio::mutable_buffer engine::get_output(const asio::mutable_buffer& data) { const auto len = std::min(data.size(), _outbuf.size()); if (len > 0) { auto* p = const_cast(static_cast(data.data())); std::copy(_outbuf.begin(), _outbuf.begin() + len, p); _outbuf.erase(_outbuf.begin(), _outbuf.begin() + len); } return asio::mutable_buffer(data.data(), len); } ::OSStatus engine::write_func(::SSLConnectionRef ctx, const void* data, size_t* data_len) { auto* this_ = const_cast(static_cast(ctx)); const auto* p = static_cast(data); this_->_outbuf.insert(this_->_outbuf.end(), p, p + *data_len); return ::errSecSuccess; } engine::want engine::read(const asio::mutable_buffer& data, asio::error_code& ec, std::size_t& bytes_transferred) { if (!verifyConnected(_ssl.get(), &ec)) { return want::want_nothing; } const auto status = ::SSLRead(_ssl.get(), data.data(), data.size(), &bytes_transferred); if ((status != ::errSSLWouldBlock) && (status != ::errSecSuccess)) { ec = errorCode(status); } return bytes_transferred ? want::want_nothing : wouldBlock(); } asio::const_buffer engine::put_input(const asio::const_buffer& data) { const auto* p = static_cast(data.data()); _inbuf.insert(_inbuf.end(), p, p + data.size()); return asio::buffer(data + data.size()); } ::OSStatus engine::read_func(::SSLConnectionRef ctx, void* data, size_t* data_len) { ::OSStatus ret = ::errSecSuccess; auto* this_ = const_cast(static_cast(ctx)); if (*data_len > this_->_inbuf.size()) { // If we're able to 100% satisfy the read request Secure Transport made, // then we should ultimately signal that the read is incomplete. ret = ::errSSLWouldBlock; } *data_len = std::min(*data_len, this_->_inbuf.size()); if (*data_len > 0) { std::copy( this_->_inbuf.begin(), this_->_inbuf.begin() + *data_len, static_cast(data)); this_->_inbuf.erase(this_->_inbuf.begin(), this_->_inbuf.begin() + *data_len); } return ret; } engine::want engine::wouldBlock() const { return _outbuf.empty() ? want::want_input_and_retry : want::want_output_and_retry; } #include "asio/detail/pop_options.hpp" } // namespace detail } // namespace ssl } // namespace asio