diff options
author | Jack Lloyd <[email protected]> | 2019-05-01 08:31:30 -0400 |
---|---|---|
committer | Jack Lloyd <[email protected]> | 2019-05-01 08:31:30 -0400 |
commit | d3e25fd06d121b4db37df9cef5be48d5885ea1f8 (patch) | |
tree | b52229a285d5278f6b64ce32a4e7fe5783db4cf6 | |
parent | 7fe0d20bbc7bc4dc831f396cdc55bf8547d3f4bf (diff) | |
parent | 8d996f0b92d42631322f8b586113e11fb4bb6bae (diff) |
Merge GH #1839 Add TLS asio stream wrapper
-rw-r--r-- | src/build-data/botan.doxy.in | 5 | ||||
-rw-r--r-- | src/lib/tls/asio/asio_async_ops.h | 368 | ||||
-rw-r--r-- | src/lib/tls/asio/asio_context.h | 39 | ||||
-rw-r--r-- | src/lib/tls/asio/asio_error.h | 98 | ||||
-rw-r--r-- | src/lib/tls/asio/asio_stream.h | 674 | ||||
-rw-r--r-- | src/lib/tls/asio/info.txt | 15 | ||||
-rw-r--r-- | src/lib/utils/exceptn.cpp | 58 | ||||
-rw-r--r-- | src/lib/utils/exceptn.h | 3 | ||||
-rw-r--r-- | src/tests/unit_asio_stream.cpp | 775 |
9 files changed, 2034 insertions, 1 deletions
diff --git a/src/build-data/botan.doxy.in b/src/build-data/botan.doxy.in index 71fd71010..ac032f1bd 100644 --- a/src/build-data/botan.doxy.in +++ b/src/build-data/botan.doxy.in @@ -172,7 +172,10 @@ PREDEFINED = BOTAN_HAS_AES_ARMV8 \ BOTAN_HAS_SM4_ARMV8 \ BOTAN_HAS_THREEFISH_512_AVX2 \ BOTAN_DEPRECATED(msg)= \ - BOTAN_PUBLIC_API(maj,min)= + BOTAN_PUBLIC_API(maj,min)= \ + BOTAN_HAS_TLS \ + BOTAN_HAS_BOOST_ASIO \ + BOOST_VERSION=106600 EXPAND_AS_DEFINED = diff --git a/src/lib/tls/asio/asio_async_ops.h b/src/lib/tls/asio/asio_async_ops.h new file mode 100644 index 000000000..34d9ce0dd --- /dev/null +++ b/src/lib/tls/asio/asio_async_ops.h @@ -0,0 +1,368 @@ +/* +* Helpers for TLS ASIO Stream +* (C) 2018-2019 Jack Lloyd +* 2018-2019 Hannes Rantzsch, Tim Oesterreich, Rene Meusel +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_ASIO_ASYNC_OPS_H_ +#define BOTAN_ASIO_ASYNC_OPS_H_ + +#include <botan/build.h> + +#include <boost/version.hpp> +#if BOOST_VERSION >= 106600 + +#include <botan/asio_error.h> + +// We need to define BOOST_ASIO_DISABLE_SERIAL_PORT before any asio imports. Otherwise asio will include <termios.h>, +// which interferes with Botan's amalgamation by defining macros like 'B0' and 'FF1'. +#define BOOST_ASIO_DISABLE_SERIAL_PORT +#include <boost/asio.hpp> +#include <boost/asio/yield.hpp> + +namespace Botan { +namespace TLS { +namespace detail { + +/** + * Base class for asynchronous stream operations. + * + * Asynchronous operations, used for example to implement an interface for boost::asio::async_read_some and + * boost::asio::async_write_some, are based on boost::asio::coroutines. + * Derived operations should implement a call operator and invoke it with the correct parameters upon construction. The + * call operator needs to make sure that the user-provided handler is not called directly. Typically, yield / reenter is + * used for this in the following fashion: + * + * ``` + * void operator()(boost::system::error_code ec, std::size_t bytes_transferred, bool isContinuation = true) + * { + * reenter(this) + * { + * // operation specific logic, repeatedly interacting with the stream_core and the next_layer (socket) + * + * // make sure intermediate initiating function is called + * if(!isContinuation) + * { + * yield next_layer.async_operation(empty_buffer, this); + * } + * + * // call the completion handler + * complete_now(error_code, bytes_transferred); + * } + * } + * ``` + * + * Once the operation is completed and ready to call the completion handler it checks if an intermediate initiating + * function has been called using the `isContinuation` parameter. If not, it will call an asynchronous operation, such + * as `async_read_some`, with and empty buffer, set the object itself as the handler, and `yield`. As a result, the call + * operator will be invoked again, this time as a continuation, and will jump to the location where it yielded before + * using `reenter`. It is now safe to call the handler function via `complete_now`. + * + * \tparam Handler Type of the completion handler + * \tparam Executor1 Type of the asio executor (usually derived from the lower layer) + * \tparam Allocator Type of the allocator to be used + */ +template <class Handler, class Executor1, class Allocator> +class AsyncBase : public boost::asio::coroutine + { + public: + using allocator_type = boost::asio::associated_allocator_t<Handler, Allocator>; + using executor_type = boost::asio::associated_executor_t<Handler, Executor1>; + + allocator_type get_allocator() const noexcept + { + return boost::asio::get_associated_allocator(m_handler); + } + + executor_type get_executor() const noexcept + { + return boost::asio::get_associated_executor(m_handler, m_work_guard_1.get_executor()); + } + + protected: + template <class HandlerT> + AsyncBase(HandlerT&& handler, const Executor1& executor) + : m_handler(std::forward<HandlerT>(handler)) + , m_work_guard_1(executor) + { + } + + /** + * Call the completion handler. + * + * This function should only be called after an intermediate initiating function has been called. + * + * @param args Arguments forwarded to the completion handler function. + */ + template<class... Args> + void complete_now(Args&& ... args) + { + m_work_guard_1.reset(); + m_handler(std::forward<Args>(args)...); + } + + Handler m_handler; + boost::asio::executor_work_guard<Executor1> m_work_guard_1; + }; + +template <class Handler, class Stream, class MutableBufferSequence, class Allocator = std::allocator<void>> +class AsyncReadOperation : public AsyncBase<Handler, typename Stream::executor_type, Allocator> + { + public: + /** + * Construct and invoke an AsyncWriteOperation. + * + * @param handler Handler function to be called upon completion. + * @param stream The stream from which the data will be read + * @param buffers The buffers into which the data will be read. + * @param ec Optional error code; used to report an error to the handler function. + */ + template <class HandlerT> + AsyncReadOperation(HandlerT&& handler, + Stream& stream, + const MutableBufferSequence& buffers, + const boost::system::error_code& ec = {}) + : AsyncBase<Handler, typename Stream::executor_type, Allocator>( + std::forward<HandlerT>(handler), + stream.get_executor()) + , m_stream(stream) + , m_buffers(buffers) + , m_decodedBytes(0) + { + this->operator()(ec, std::size_t(0), false); + } + + AsyncReadOperation(AsyncReadOperation&&) = default; + + void operator()(boost::system::error_code ec, std::size_t bytes_transferred, bool isContinuation = true) + { + reenter(this) + { + if(bytes_transferred > 0 && !ec) + { + // We have received encrypted data from the network, now hand it to TLS::Channel for decryption. + boost::asio::const_buffer read_buffer{m_stream.input_buffer().data(), bytes_transferred}; + try + { + m_stream.native_handle()->received_data( + static_cast<const uint8_t*>(read_buffer.data()), read_buffer.size() + ); + } + catch(const TLS_Exception& e) + { + ec = e.type(); + } + catch(const Botan::Exception& e) + { + ec = e.error_type(); + } + catch(...) + { + ec = Botan::ErrorType::Unknown; + } + } + + if(!m_stream.has_received_data() && !ec && boost::asio::buffer_size(m_buffers) > 0) + { + // The channel did not decrypt a complete record yet, we need more data from the socket. + m_stream.next_layer().async_read_some(m_stream.input_buffer(), std::move(*this)); + return; + } + + if(m_stream.has_received_data() && !ec) + { + // The channel has decrypted a TLS record, now copy it to the output buffers. + m_decodedBytes = m_stream.copy_received_data(m_buffers); + } + + if(!isContinuation) + { + // Make sure the handler is not called without an intermediate initiating function. + // "Reading" into a zero-byte buffer will complete immediately. + m_ec = ec; + yield m_stream.next_layer().async_read_some(boost::asio::mutable_buffer(), std::move(*this)); + ec = m_ec; + } + + this->complete_now(ec, m_decodedBytes); + } + } + + private: + Stream& m_stream; + MutableBufferSequence m_buffers; + std::size_t m_decodedBytes; + boost::system::error_code m_ec; + }; + +template <typename Handler, class Stream, class Allocator = std::allocator<void>> +class AsyncWriteOperation : public AsyncBase<Handler, typename Stream::executor_type, Allocator> + { + public: + /** + * Construct and invoke an AsyncWriteOperation. + * + * @param handler Handler function to be called upon completion. + * @param stream The stream from which the data will be read + * @param plainBytesTransferred Number of bytes to be reported to the user-provided handler function as + * bytes_transferred. This needs to be provided since the amount of plaintext data + * consumed from the input buffer can differ from the amount of encrypted data written + * to the next layer. + * @param ec Optional error code; used to report an error to the handler function. + */ + template <class HandlerT> + AsyncWriteOperation(HandlerT&& handler, + Stream& stream, + std::size_t plainBytesTransferred, + const boost::system::error_code& ec = {}) + : AsyncBase<Handler, typename Stream::executor_type, Allocator>( + std::forward<HandlerT>(handler), + stream.get_executor()) + , m_stream(stream) + , m_plainBytesTransferred(plainBytesTransferred) + { + this->operator()(ec, std::size_t(0), false); + } + + AsyncWriteOperation(AsyncWriteOperation&&) = default; + + void operator()(boost::system::error_code ec, std::size_t bytes_transferred, bool isContinuation = true) + { + reenter(this) + { + // mark the number of encrypted bytes sent to the network as "consumed" + // Note: bytes_transferred will be zero on first call + m_stream.consume_send_buffer(bytes_transferred); + + if(m_stream.has_data_to_send() && !ec) + { + m_stream.next_layer().async_write_some(m_stream.send_buffer(), std::move(*this)); + return; + } + + if(!isContinuation) + { + // Make sure the handler is not called without an intermediate initiating function. + // "Writing" to a zero-byte buffer will complete immediately. + m_ec = ec; + yield m_stream.next_layer().async_write_some(boost::asio::const_buffer(), std::move(*this)); + ec = m_ec; + } + + // The size of the sent TLS record can differ from the size of the payload due to TLS encryption. We need to + // tell the handler how many bytes of the original data we already processed. + this->complete_now(ec, m_plainBytesTransferred); + } + } + + private: + Stream& m_stream; + std::size_t m_plainBytesTransferred; + boost::system::error_code m_ec; + }; + +template <class Handler, class Stream, class Allocator = std::allocator<void>> +class AsyncHandshakeOperation : public AsyncBase<Handler, typename Stream::executor_type, Allocator> + { + public: + /** + * Construct and invoke an AsyncHandshakeOperation. + * + * @param handler Handler function to be called upon completion. + * @param stream The stream from which the data will be read + * @param ec Optional error code; used to report an error to the handler function. + */ + template<class HandlerT> + AsyncHandshakeOperation( + HandlerT&& handler, + Stream& stream, + const boost::system::error_code& ec = {}) + : AsyncBase<Handler, typename Stream::executor_type, Allocator>( + std::forward<HandlerT>(handler), + stream.get_executor()) + , m_stream(stream) + { + this->operator()(ec, std::size_t(0), false); + } + + AsyncHandshakeOperation(AsyncHandshakeOperation&&) = default; + + void operator()(boost::system::error_code ec, std::size_t bytesTransferred, bool isContinuation = true) + { + reenter(this) + { + if(bytesTransferred > 0 && !ec) + { + // Provide encrypted TLS data received from the network to TLS::Channel for decryption + boost::asio::const_buffer read_buffer {m_stream.input_buffer().data(), bytesTransferred}; + try + { + m_stream.native_handle()->received_data( + static_cast<const uint8_t*>(read_buffer.data()), read_buffer.size() + ); + } + catch(const TLS_Exception& e) + { + ec = e.type(); + } + catch(const Botan::Exception& e) + { + ec = e.error_type(); + } + catch(...) + { + ec = Botan::ErrorType::Unknown; + } + } + + if(m_stream.has_data_to_send() && !ec) + { + // Write encrypted TLS data provided by the TLS::Channel on the wire + + // Note: we construct `AsyncWriteOperation` with 0 as its last parameter (`plainBytesTransferred`). This + // operation will eventually call `*this` as its own handler, passing the 0 back to this call operator. + // This is necessary because the check of `bytesTransferred > 0` assumes that `bytesTransferred` bytes + // were just read and are available in input_buffer for further processing. + AsyncWriteOperation< + AsyncHandshakeOperation<typename std::decay<Handler>::type, Stream, Allocator>, + Stream, + Allocator> + op{std::move(*this), m_stream, 0}; + return; + } + + if(!m_stream.native_handle()->is_active() && !ec) + { + // Read more encrypted TLS data from the network + m_stream.next_layer().async_read_some(m_stream.input_buffer(), std::move(*this)); + return; + } + + if(!isContinuation) + { + // Make sure the handler is not called without an intermediate initiating function. + // "Reading" into a zero-byte buffer will complete immediately. + m_ec = ec; + yield m_stream.next_layer().async_read_some(boost::asio::mutable_buffer(), std::move(*this)); + ec = m_ec; + } + + this->complete_now(ec); + } + } + + private: + Stream& m_stream; + boost::system::error_code m_ec; + }; + +} // namespace detail +} // namespace TLS +} // namespace Botan + +#include <boost/asio/unyield.hpp> + +#endif // BOOST_VERSION +#endif // BOTAN_ASIO_ASYNC_OPS_H_ diff --git a/src/lib/tls/asio/asio_context.h b/src/lib/tls/asio/asio_context.h new file mode 100644 index 000000000..7de88ebce --- /dev/null +++ b/src/lib/tls/asio/asio_context.h @@ -0,0 +1,39 @@ +/* + * TLS Context + * (C) 2018-2019 Jack Lloyd + * 2018-2019 Hannes Rantzsch, Tim Oesterreich, Rene Meusel + * + * Botan is released under the Simplified BSD License (see license.txt) + */ + +#ifndef BOTAN_ASIO_TLS_CONTEXT_H_ +#define BOTAN_ASIO_TLS_CONTEXT_H_ + +#include <botan/build.h> + +#include <boost/version.hpp> +#if BOOST_VERSION >= 106600 + +#include <botan/credentials_manager.h> +#include <botan/rng.h> +#include <botan/tls_policy.h> +#include <botan/tls_server_info.h> +#include <botan/tls_session_manager.h> + +namespace Botan { +namespace TLS { + +struct Context + { + Credentials_Manager* credentialsManager; + RandomNumberGenerator* randomNumberGenerator; + Session_Manager* sessionManager; + Policy* policy; + Server_Information serverInfo; + }; + +} // namespace TLS +} // namespace Botan + +#endif // BOOST_VERSION +#endif // BOTAN_ASIO_TLS_CONTEXT_H_ diff --git a/src/lib/tls/asio/asio_error.h b/src/lib/tls/asio/asio_error.h new file mode 100644 index 000000000..7356ab3d7 --- /dev/null +++ b/src/lib/tls/asio/asio_error.h @@ -0,0 +1,98 @@ +/* +* TLS Stream Errors +* (C) 2018-2019 Jack Lloyd +* 2018-2019 Hannes Rantzsch, Tim Oesterreich, Rene Meusel +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_ASIO_ERROR_H_ +#define BOTAN_ASIO_ERROR_H_ + +#include <botan/build.h> + +#include <boost/version.hpp> +#if BOOST_VERSION >= 106600 + +#include <boost/system/system_error.hpp> + +#include <botan/exceptn.h> +#include <botan/tls_alert.h> +#include <botan/tls_exceptn.h> + +namespace Botan { +namespace TLS { + +//! @brief An error category for TLS alerts +struct BotanAlertCategory : boost::system::error_category + { + const char* name() const noexcept override + { + return "asio.botan.tls.alert"; + } + + std::string message(int ev) const override + { + Botan::TLS::Alert alert(static_cast<Botan::TLS::Alert::Type>(ev)); + return alert.type_string(); + } + }; + +inline const BotanAlertCategory& botan_alert_category() noexcept + { + static BotanAlertCategory category; + return category; + } + +inline boost::system::error_code make_error_code(Botan::TLS::Alert::Type c) + { + return boost::system::error_code(static_cast<int>(c), Botan::TLS::botan_alert_category()); + } + +} // namespace TLS + +//! @brief An error category for errors from Botan (other than TLS alerts) +struct BotanErrorCategory : boost::system::error_category + { + const char* name() const noexcept override + { + return "asio.botan.tls"; + } + + std::string message(int ev) const override + { + return Botan::to_string(static_cast<Botan::ErrorType>(ev)); + } + }; + +inline const BotanErrorCategory& botan_category() noexcept + { + static BotanErrorCategory category; + return category; + } + +inline boost::system::error_code make_error_code(Botan::ErrorType e) + { + return boost::system::error_code(static_cast<int>(e), Botan::botan_category()); + } + +} // namespace Botan + +namespace boost { +namespace system { + +template<> struct is_error_code_enum<Botan::TLS::Alert::Type> + { + static const bool value = true; + }; + +template<> struct is_error_code_enum<Botan::ErrorType> + { + static const bool value = true; + }; + +} // namespace system +} // namespace boost + +#endif // BOOST_VERSION +#endif // BOTAN_ASIO_ERROR_H_ diff --git a/src/lib/tls/asio/asio_stream.h b/src/lib/tls/asio/asio_stream.h new file mode 100644 index 000000000..4449cb14a --- /dev/null +++ b/src/lib/tls/asio/asio_stream.h @@ -0,0 +1,674 @@ +/* +* TLS ASIO Stream +* (C) 2018-2019 Jack Lloyd +* 2018-2019 Hannes Rantzsch, Tim Oesterreich, Rene Meusel +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_ASIO_STREAM_H_ +#define BOTAN_ASIO_STREAM_H_ + +#include <botan/build.h> + +// first version to be compatible with Networking TS (N4656) and boost::beast +#include <boost/version.hpp> +#if BOOST_VERSION >= 106600 + +#include <botan/asio_async_ops.h> +#include <botan/asio_context.h> +#include <botan/asio_error.h> + +#include <botan/tls_callbacks.h> +#include <botan/tls_channel.h> +#include <botan/tls_client.h> +#include <botan/tls_magic.h> + +// We need to define BOOST_ASIO_DISABLE_SERIAL_PORT before any asio imports. Otherwise asio will include <termios.h>, +// which interferes with Botan's amalgamation by defining macros like 'B0' and 'FF1'. +#define BOOST_ASIO_DISABLE_SERIAL_PORT +#include <boost/asio.hpp> +#include <boost/beast/core/flat_buffer.hpp> + +#include <algorithm> +#include <memory> +#include <type_traits> + +namespace Botan { +namespace TLS { + +/** + * @brief boost::asio compatible SSL/TLS stream + * + * Currently only the TLS::Client specialization is implemented. + * + * @tparam StreamLayer type of the next layer, usually a network socket + * @tparam ChannelT type of the native_handle, defaults to Botan::TLS::Channel, only needed for testing purposes + */ +template <class StreamLayer, class ChannelT = Channel> +class Stream + { + public: + //! \name construction + //! @{ + + template <typename... Args> + explicit Stream(Context& context, Args&& ... args) + : m_context(context) + , m_nextLayer(std::forward<Args>(args)...) + , m_core(m_receive_buffer, m_send_buffer) + , m_input_buffer_space(MAX_CIPHERTEXT_SIZE, '\0') + , m_input_buffer(m_input_buffer_space.data(), m_input_buffer_space.size()) + {} + + // overload for boost::asio::ssl::stream compatibility + template <typename Arg> + explicit Stream(Arg&& arg, Context& context) + : m_context(context) + , m_nextLayer(std::forward<Arg>(arg)) + , m_core(m_receive_buffer, m_send_buffer) + , m_input_buffer_space(MAX_CIPHERTEXT_SIZE, '\0') + , m_input_buffer(m_input_buffer_space.data(), m_input_buffer_space.size()) + {} + + virtual ~Stream() = default; + + Stream(Stream&& other) = default; + Stream& operator=(Stream&& other) = default; + + Stream(const Stream& other) = delete; + Stream& operator=(const Stream& other) = delete; + + //! @} + //! \name boost::asio accessor methods + //! @{ + + using next_layer_type = typename std::remove_reference<StreamLayer>::type; + using lowest_layer_type = typename next_layer_type::lowest_layer_type; + using executor_type = typename next_layer_type::executor_type; + using native_handle_type = typename std::add_pointer<ChannelT>::type; + + executor_type get_executor() noexcept { return m_nextLayer.get_executor(); } + + const next_layer_type& next_layer() const { return m_nextLayer; } + next_layer_type& next_layer() { return m_nextLayer; } + + lowest_layer_type& lowest_layer() { return m_nextLayer.lowest_layer(); } + const lowest_layer_type& lowest_layer() const { return m_nextLayer.lowest_layer(); } + + native_handle_type native_handle() { return m_native_handle.get(); } + + //! @} + //! \name configuration and callback setters + //! @{ + + //! @throws Not_Implemented + template<typename VerifyCallback> + void set_verify_callback(VerifyCallback callback) + { + BOTAN_UNUSED(callback); + throw Not_Implemented("set_verify_callback is not implemented"); + } + + /** + * Not Implemented. + * @param ec Will be set to `Botan::ErrorType::NotImplemented` + */ + template<typename VerifyCallback> + void set_verify_callback(VerifyCallback callback, + boost::system::error_code& ec) + { + BOTAN_UNUSED(callback); + ec = Botan::ErrorType::NotImplemented; + } + + //! @throws Not_Implemented + void set_verify_depth(int depth) + { + BOTAN_UNUSED(depth); + throw Not_Implemented("set_verify_depth is not implemented"); + } + + /** + * Not Implemented. + * @param ec Will be set to `Botan::ErrorType::NotImplemented` + */ + void set_verify_depth(int depth, + boost::system::error_code& ec) + { + BOTAN_UNUSED(depth); + ec = Botan::ErrorType::NotImplemented; + } + + //! @throws Not_Implemented + template <typename verify_mode> + void set_verify_mode(verify_mode v) + { + BOTAN_UNUSED(v); + throw Not_Implemented("set_verify_mode is not implemented"); + } + + /** + * Not Implemented. + * @param ec Will be set to `Botan::ErrorType::NotImplemented` + */ + template <typename verify_mode> + void set_verify_mode(verify_mode v, + boost::system::error_code& ec) + { + BOTAN_UNUSED(v); + ec = Botan::ErrorType::NotImplemented; + } + + //! @} + //! \name handshake methods + //! @{ + + /** + * @brief Performs SSL handshaking. + * + * The function call will block until handshaking is complete or an error occurs. + * + * @param type The type of handshaking to be performed, i.e. as a client or as a server. + * @throws boost::system::system_error if error occured, or if the chosen Connection_Side is not available + */ + void handshake(Connection_Side side) + { + boost::system::error_code ec; + handshake(side, ec); + boost::asio::detail::throw_error(ec, "handshake"); + } + + /** + * @brief Performs SSL handshaking. + * + * The function call will block until handshaking is complete or an error occurs. + * + * @param type The type of handshaking to be performed, i.e. as a client or as a server. + * @param ec Set to indicate what error occurred, if any. + */ + void handshake(Connection_Side side, boost::system::error_code& ec) + { + setup_native_handle(side, ec); + + // send client hello, which was written to the send buffer on client instantiation + send_pending_encrypted_data(ec); + + while(!native_handle()->is_active() && !ec) + { + boost::asio::const_buffer read_buffer{input_buffer().data(), m_nextLayer.read_some(input_buffer(), ec)}; + if(ec) + { return; } + + try + { + native_handle()->received_data(static_cast<const uint8_t*>(read_buffer.data()), read_buffer.size()); + } + catch(const TLS_Exception& e) + { + ec = e.type(); + } + catch(const Botan::Exception& e) + { + ec = e.error_type(); + } + catch(const std::exception&) + { + ec = Botan::ErrorType::Unknown; + } + + send_pending_encrypted_data(ec); + } + } + + /** + * @brief Starts an asynchronous SSL handshake. + * + * This function call always returns immediately. + * + * @param type The type of handshaking to be performed, i.e. as a client or as a server. + * @param handler The handler to be called when the handshake operation completes. + * The equivalent function signature of the handler must be: void(boost::system::error_code) + * @throws Invalid_Argument if Connection_Side could not be validated + */ + template <typename HandshakeHandler> + BOOST_ASIO_INITFN_RESULT_TYPE(HandshakeHandler, + void(boost::system::error_code)) + async_handshake(Connection_Side side, HandshakeHandler&& handler) + { + BOOST_ASIO_HANDSHAKE_HANDLER_CHECK(HandshakeHandler, handler) type_check; + + boost::system::error_code ec; + setup_native_handle(side, ec); + // If ec is set by setup_native_handle, the AsyncHandshakeOperation created below will do nothing but call the + // handler with the error_code set appropriately - no need to early return here. + + boost::asio::async_completion<HandshakeHandler, void(boost::system::error_code)> init(handler); + + detail::AsyncHandshakeOperation<typename std::decay<HandshakeHandler>::type, Stream> + op{std::move(init.completion_handler), *this, ec}; + + return init.result.get(); + } + + //! @throws Not_Implemented + template <typename ConstBufferSequence, typename BufferedHandshakeHandler> + BOOST_ASIO_INITFN_RESULT_TYPE(BufferedHandshakeHandler, + void(boost::system::error_code, std::size_t)) + async_handshake(Connection_Side side, const ConstBufferSequence& buffers, + BufferedHandshakeHandler&& handler) + { + BOTAN_UNUSED(side, buffers, handler); + BOOST_ASIO_HANDSHAKE_HANDLER_CHECK(BufferedHandshakeHandler, handler) type_check; + throw Not_Implemented("buffered async handshake is not implemented"); + } + + //! @} + //! \name shutdown methods + //! @{ + + /** + * @brief Shut down SSL on the stream. + * + * This function is used to shut down SSL on the stream. The function call will block until SSL has been shut down + * or an error occurs. Note that this will not close the lowest layer. + * + * @param ec Set to indicate what error occured, if any. + */ + void shutdown(boost::system::error_code& ec) + { + try + { + native_handle()->close(); + } + catch(const TLS_Exception& e) + { + ec = e.type(); + } + catch(const Botan::Exception& e) + { + ec = e.error_type(); + } + catch(const std::exception&) + { + ec = Botan::ErrorType::Unknown; + } + + if(!ec) + { send_pending_encrypted_data(ec); } + } + + /** + * @brief Shut down SSL on the stream. + * + * This function is used to shut down SSL on the stream. The function call will block until SSL has been shut down + * or an error occurs. Note that this will not close the lowest layer. + * + * @throws boost::system::system_error if error occured + */ + void shutdown() + { + boost::system::error_code ec; + shutdown(ec); + boost::asio::detail::throw_error(ec, "shutdown"); + } + + /** + * @brief Asynchronously shut down SSL on the stream. + * + * This function call always returns immediately. + * + * @param handler The handler to be called when the handshake operation completes. + * The equivalent function signature of the handler must be: void(boost::system::error_code) + */ + template <typename ShutdownHandler> + void async_shutdown(ShutdownHandler&& handler) + { + BOOST_ASIO_HANDSHAKE_HANDLER_CHECK(ShutdownHandler, handler) type_check; + BOTAN_UNUSED(handler); + throw Not_Implemented("async shutdown is not implemented"); + // TODO: Implement a subclass of AsyncBase that calls native_handle()->close() and writes pending data from + // the core to the network, e.g. using AsyncWriteOperation. + } + + //! @} + //! \name I/O methods + //! @{ + + /** + * @brief Read some data from the stream. + * + * The function call will block until one or more bytes of data has been read successfully, or until an error + * occurs. + * + * @param buffers The buffers into which the data will be read. + * @param ec Set to indicate what error occured, if any. + * @return The number of bytes read. Returns 0 if an error occurred. + */ + template <typename MutableBufferSequence> + std::size_t read_some(const MutableBufferSequence& buffers, + boost::system::error_code& ec) + { + if(has_received_data()) + { return copy_received_data(buffers); } + + boost::asio::const_buffer read_buffer{input_buffer().data(), m_nextLayer.read_some(input_buffer(), ec)}; + if(ec) + { return 0; } + + try + { + native_handle()->received_data(static_cast<const uint8_t*>(read_buffer.data()), read_buffer.size()); + } + catch(const TLS_Exception& e) + { + ec = e.type(); + } + catch(const Botan::Exception& e) + { + ec = e.error_type(); + } + catch(const std::exception&) + { + ec = Botan::ErrorType::Unknown; + } + + return !ec ? copy_received_data(buffers) : 0; + } + + /** + * @brief Read some data from the stream. + * + * The function call will block until one or more bytes of data has been read successfully, or until an error + * occurs. + * + * @param buffers The buffers into which the data will be read. + * @return The number of bytes read. Returns 0 if an error occurred. + * @throws boost::system::system_error if error occured + */ + template <typename MutableBufferSequence> + std::size_t read_some(const MutableBufferSequence& buffers) + { + boost::system::error_code ec; + auto const n = read_some(buffers, ec); + boost::asio::detail::throw_error(ec, "read_some"); + return n; + } + + /** + * @brief Write some data to the stream. + * + * The function call will block until one or more bytes of data has been written successfully, or until an error + * occurs. + * + * @param buffers The data to be written. + * @param ec Set to indicate what error occurred, if any. + * @return The number of bytes processed from the input buffers. + */ + template <typename ConstBufferSequence> + std::size_t write_some(const ConstBufferSequence& buffers, + boost::system::error_code& ec) + { + tls_encrypt(buffers, ec); + send_pending_encrypted_data(ec); + return !ec ? boost::asio::buffer_size(buffers) : 0; + } + + /** + * @brief Write some data to the stream. + * + * The function call will block until one or more bytes of data has been written successfully, or until an error + * occurs. + * + * @param buffers The data to be written. + * @return The number of bytes written. + * @throws boost::system::system_error if error occured + */ + template <typename ConstBufferSequence> + std::size_t write_some(const ConstBufferSequence& buffers) + { + boost::system::error_code ec; + auto const n = write_some(buffers, ec); + boost::asio::detail::throw_error(ec, "write_some"); + return n; + } + + /** + * @brief Start an asynchronous write. The function call always returns immediately. + * + * @param buffers The data to be written. + * @param handler The handler to be called when the write operation completes. Copies will be made of the handler + * as required. The equivalent function signature of the handler must be: + * void(boost::system::error_code, std::size_t) + */ + template <typename ConstBufferSequence, typename WriteHandler> + BOOST_ASIO_INITFN_RESULT_TYPE(WriteHandler, + void(boost::system::error_code, std::size_t)) + async_write_some(const ConstBufferSequence& buffers, WriteHandler&& handler) + { + BOOST_ASIO_WRITE_HANDLER_CHECK(WriteHandler, handler) type_check; + + boost::asio::async_completion<WriteHandler, void(boost::system::error_code, std::size_t)> init(handler); + + boost::system::error_code ec; + tls_encrypt(buffers, ec); + if(ec) + { + // we cannot be sure how many bytes were committed here so clear the send_buffer and let the + // AsyncWriteOperation call the handler with the error_code set + consume_send_buffer(m_send_buffer.size()); + detail::AsyncWriteOperation<typename std::decay<WriteHandler>::type, Stream> + op{std::move(init.completion_handler), *this, std::size_t(0), ec}; + return init.result.get(); + } + + detail::AsyncWriteOperation<typename std::decay<WriteHandler>::type, Stream> + op{std::move(init.completion_handler), *this, boost::asio::buffer_size(buffers)}; + + return init.result.get(); + } + + /** + * @brief Start an asynchronous read. The function call always returns immediately. + * + * @param buffers The buffers into which the data will be read. Although the buffers object may be copied as + * necessary, ownership of the underlying buffers is retained by the caller, which must guarantee + * that they remain valid until the handler is called. + * @param handler The handler to be called when the read operation completes. The equivalent function signature of + * the handler must be: + * void(boost::system::error_code, std::size_t) + */ + template <typename MutableBufferSequence, typename ReadHandler> + BOOST_ASIO_INITFN_RESULT_TYPE(ReadHandler, + void(boost::system::error_code, std::size_t)) + async_read_some(const MutableBufferSequence& buffers, ReadHandler&& handler) + { + BOOST_ASIO_READ_HANDLER_CHECK(ReadHandler, handler) type_check; + + boost::asio::async_completion<ReadHandler, void(boost::system::error_code, std::size_t)> init(handler); + + detail::AsyncReadOperation<typename std::decay<ReadHandler>::type, Stream, MutableBufferSequence> + op{std::move(init.completion_handler), *this, buffers}; + return init.result.get(); + } + + //! @} + + protected: + template <typename> friend class detail::AsyncWriteOperation; + template <typename> friend class detail::AsyncReadOperation; + template <typename> friend class detail::AsyncHandshakeOperation; + + /** + * @brief Helper class that implements Botan::TLS::Callbacks + * + * This class is provided to the stream's native_handle (Botan::TLS::Channel) and implements the callback + * functions triggered by the native_handle. + * + * @param receive_buffer reference to the buffer where decrypted data should be placed + * @param send_buffer reference to the buffer where encrypted data should be placed + */ + class StreamCore : public Botan::TLS::Callbacks + { + public: + StreamCore(boost::beast::flat_buffer& receive_buffer, boost::beast::flat_buffer& send_buffer) + : m_receive_buffer(receive_buffer), m_send_buffer(send_buffer) {} + + virtual ~StreamCore() = default; + + void tls_emit_data(const uint8_t data[], std::size_t size) override + { + m_send_buffer.commit( + boost::asio::buffer_copy(m_send_buffer.prepare(size), boost::asio::buffer(data, size)) + ); + } + + void tls_record_received(uint64_t, const uint8_t data[], std::size_t size) override + { + m_receive_buffer.commit( + boost::asio::buffer_copy(m_receive_buffer.prepare(size), boost::asio::const_buffer(data, size)) + ); + } + + void tls_alert(Botan::TLS::Alert alert) override + { + BOTAN_UNUSED(alert); + } + + std::chrono::milliseconds tls_verify_cert_chain_ocsp_timeout() const override + { + return std::chrono::milliseconds(1000); + } + + bool tls_session_established(const Botan::TLS::Session&) override + { + // TODO: it should be possible to configure this in the using application (via callback?) + return true; + } + + boost::beast::flat_buffer& m_receive_buffer; + boost::beast::flat_buffer& m_send_buffer; + }; + + const boost::asio::mutable_buffer& input_buffer() { return m_input_buffer; } + boost::asio::const_buffer send_buffer() const { return m_send_buffer.data(); } + + //! @brief Check if decrypted data is available in the receive buffer + bool has_received_data() const { return m_receive_buffer.size() > 0; } + + //! @brief Copy decrypted data into the user-provided buffer + template <typename MutableBufferSequence> + std::size_t copy_received_data(MutableBufferSequence buffers) + { + // Note: It would be nice to avoid this buffer copy. This could be achieved by equipping the StreamCore with + // the user's desired target buffer once a read is started, and reading directly into that buffer in tls_record + // received. However, we need to deal with the case that the receive buffer provided by the caller is smaller + // than the decrypted record, so this optimization might not be worth the additional complexity. + const auto copiedBytes = boost::asio::buffer_copy(buffers, m_receive_buffer.data()); + m_receive_buffer.consume(copiedBytes); + return copiedBytes; + } + + //! @brief Check if encrypted data is available in the send buffer + bool has_data_to_send() const { return m_send_buffer.size() > 0; } + + //! @brief Mark bytes in the send buffer as consumed, removing them from the buffer + void consume_send_buffer(std::size_t bytesConsumed) { m_send_buffer.consume(bytesConsumed); } + + // This is a helper construct to allow mocking the native_handle in test code. It is activated by explicitly + // specifying a (mocked) channel type template parameter when constructing the stream and does not attempt to + // instantiate the native_handle. + // Note: once we have C++17 we can achieve this much more elegantly using constexpr if. + template<class T = ChannelT> + typename std::enable_if<!std::is_same<Channel, T>::value>::type + setup_native_handle(Connection_Side, boost::system::error_code&) {} + + /** + * @brief Create the native handle. + * + * Depending on the desired connection side, this function will create a Botan::TLS::Client or a + * Botan::TLS::Server. + * + * @param side The desired connection side (client or server) + * @param ec Set to NotImplemented when side is SERVER - currently only CLIENT is implemented + */ + template<class T = ChannelT> + typename std::enable_if<std::is_same<Channel, T>::value>::type + setup_native_handle(Connection_Side side, boost::system::error_code& ec) + { + if(side == CLIENT) + { + m_native_handle = std::unique_ptr<Client>(new Client(m_core, + *m_context.sessionManager, + *m_context.credentialsManager, + *m_context.policy, + *m_context.randomNumberGenerator, + m_context.serverInfo)); + } + else + { + // TODO: First steps in order to support the server side of this stream would be to instantiate a + // Botan::TLS::Server instance as the stream's native_handle and implement the handshake appropriately. + ec = Botan::ErrorType::NotImplemented; + } + } + + size_t send_pending_encrypted_data(boost::system::error_code& ec) + { + if(ec) + { return 0; } + + auto writtenBytes = boost::asio::write(m_nextLayer, send_buffer(), ec); + consume_send_buffer(writtenBytes); + return writtenBytes; + } + + template <typename ConstBufferSequence> + void tls_encrypt(const ConstBufferSequence& buffers, boost::system::error_code& ec) + { + // NOTE: This is not asynchronous: it encrypts the data synchronously. + // The data encrypted by native_handle()->send() is synchronously stored in the send_buffer of m_core, + // but is not actually written to the wire, yet. + for(auto it = boost::asio::buffer_sequence_begin(buffers); + !ec && it != boost::asio::buffer_sequence_end(buffers); + it++) + { + const boost::asio::const_buffer buffer = *it; + try + { + native_handle()->send(static_cast<const uint8_t*>(buffer.data()), buffer.size()); + } + catch(const TLS_Exception& e) + { + ec = e.type(); + } + catch(const Botan::Exception& e) + { + ec = e.error_type(); + } + catch(const std::exception&) + { + ec = Botan::ErrorType::Unknown; + } + } + } + + Context m_context; + StreamLayer m_nextLayer; + + boost::beast::flat_buffer m_receive_buffer; + boost::beast::flat_buffer m_send_buffer; + + StreamCore m_core; + std::unique_ptr<ChannelT> m_native_handle; + + // Buffer space used to read input intended for the core + std::vector<uint8_t> m_input_buffer_space; + const boost::asio::mutable_buffer m_input_buffer; + }; + +} // namespace TLS +} // namespace Botan + +#endif // BOOST_VERSION +#endif // BOTAN_ASIO_STREAM_H_ diff --git a/src/lib/tls/asio/info.txt b/src/lib/tls/asio/info.txt new file mode 100644 index 000000000..7862a1c28 --- /dev/null +++ b/src/lib/tls/asio/info.txt @@ -0,0 +1,15 @@ +<defines> +TLS_ASIO_STREAM -> 20181218 +</defines> + +<header:public> +asio_context.h +asio_stream.h +asio_error.h +asio_async_ops.h +</header:public> + +<requires> +boost +tls +</requires> diff --git a/src/lib/utils/exceptn.cpp b/src/lib/utils/exceptn.cpp index 78365f98b..3876c3b26 100644 --- a/src/lib/utils/exceptn.cpp +++ b/src/lib/utils/exceptn.cpp @@ -8,6 +8,64 @@ namespace Botan { +std::string to_string(ErrorType type) +{ + switch(type) + { + case ErrorType::Unknown: + return "Unknown"; + case ErrorType::SystemError: + return "SystemError"; + case ErrorType::NotImplemented: + return "NotImplemented"; + case ErrorType::OutOfMemory: + return "OutOfMemory"; + case ErrorType::InternalError: + return "InternalError"; + case ErrorType::IoError: + return "IoError"; + case ErrorType::InvalidObjectState : + return "InvalidObjectState"; + case ErrorType::KeyNotSet: + return "KeyNotSet"; + case ErrorType::InvalidArgument: + return "InvalidArgument"; + case ErrorType::InvalidKeyLength: + return "InvalidKeyLength"; + case ErrorType::InvalidNonceLength: + return "InvalidNonceLength"; + case ErrorType::LookupError: + return "LookupError"; + case ErrorType::EncodingFailure: + return "EncodingFailure"; + case ErrorType::DecodingFailure: + return "DecodingFailure"; + case ErrorType::TLSError: + return "TLSError"; + case ErrorType::HttpError: + return "HttpError"; + case ErrorType::InvalidTag: + return "InvalidTag"; + case ErrorType::OpenSSLError : + return "OpenSSLError"; + case ErrorType::CommonCryptoError: + return "CommonCryptoError"; + case ErrorType::Pkcs11Error: + return "Pkcs11Error"; + case ErrorType::TPMError: + return "TPMError"; + case ErrorType::ZlibError : + return "ZlibError"; + case ErrorType::Bzip2Error: + return "Bzip2Error" ; + case ErrorType::LzmaError: + return "LzmaError"; + + default: + return "unrecognized botan error"; + } +} + Exception::Exception(const std::string& msg) : m_msg(msg) {} diff --git a/src/lib/utils/exceptn.h b/src/lib/utils/exceptn.h index c4934ed04..0ca888316 100644 --- a/src/lib/utils/exceptn.h +++ b/src/lib/utils/exceptn.h @@ -72,6 +72,9 @@ enum class ErrorType { }; +//! \brief Convert an ErrorType to string +std::string BOTAN_PUBLIC_API(2,11) to_string(ErrorType type); + /** * Base class for all exceptions thrown by the library */ diff --git a/src/tests/unit_asio_stream.cpp b/src/tests/unit_asio_stream.cpp new file mode 100644 index 000000000..ee80cdba4 --- /dev/null +++ b/src/tests/unit_asio_stream.cpp @@ -0,0 +1,775 @@ +/* +* TLS ASIO Stream Unit Tests +* (C) 2018-2019 Jack Lloyd +* 2018-2019 Hannes Rantzsch, Tim Oesterreich, Rene Meusel +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "tests.h" + +#if defined(BOTAN_HAS_TLS) && defined(BOTAN_HAS_BOOST_ASIO) + +#include <botan/asio_stream.h> +#include <botan/tls_callbacks.h> + +// first boost version to include boost/beast/experimental/test/stream.hpp +#include <boost/version.hpp> +#if BOOST_VERSION >= 106800 + +#include <boost/beast/experimental/test/stream.hpp> +#include <boost/bind.hpp> + +namespace Botan_Tests { + +namespace net = boost::asio; +using error_code = boost::system::error_code; + +constexpr uint8_t TEST_DATA[] = "The story so far: In the beginning the Universe was created. " + "This has made a lot of people very angry and been widely regarded as a bad move."; +constexpr std::size_t TEST_DATA_SIZE = 142; +static_assert(sizeof(TEST_DATA) == TEST_DATA_SIZE, "size of TEST_DATA must match TEST_DATA_SIZE"); + +/** + * Mocked Botan::TLS::Channel. Pretends to perform TLS operations and triggers appropriate callbacks in StreamCore. + */ +class MockChannel + { + public: + MockChannel(Botan::TLS::Callbacks& core) + : m_callbacks(core) + , m_bytes_till_complete_record(TEST_DATA_SIZE) + , m_active(false) {} + + public: + std::size_t received_data(const uint8_t[], std::size_t buf_size) + { + if(m_bytes_till_complete_record <= buf_size) + { + m_callbacks.tls_record_received(0, TEST_DATA, TEST_DATA_SIZE); + m_active = true; // claim to be active once a full record has been received (for handshake test) + return 0; + } + m_bytes_till_complete_record -= buf_size; + return m_bytes_till_complete_record; + } + + void send(const uint8_t buf[], std::size_t buf_size) { m_callbacks.tls_emit_data(buf, buf_size); } + + bool is_active() { return m_active; } + + protected: + Botan::TLS::Callbacks& m_callbacks; + std::size_t m_bytes_till_complete_record; // number of bytes still to read before tls record is completed + bool m_active; + + Botan::TLS::Session_Manager_Noop m_session_manager; + Botan::Null_RNG m_rng; + Botan::TLS::Default_Policy m_policy; + }; + +class ThrowingMockChannel : public MockChannel + { + public: + static boost::system::error_code expected_ec() + { + return Botan::TLS::Alert::UNEXPECTED_MESSAGE; + } + + ThrowingMockChannel(Botan::TLS::Callbacks& core) : MockChannel(core) + { + } + + std::size_t received_data(const uint8_t[], std::size_t) + { + throw Botan::TLS::Unexpected_Message("test_error"); + } + + void send(const uint8_t[], std::size_t) + { + throw Botan::TLS::Unexpected_Message("test_error"); + } + }; + +using TestStream = boost::beast::test::stream; +using FailCount = boost::beast::test::fail_count; + +class AsioStream : public Botan::TLS::Stream<TestStream, MockChannel> + { + public: + template <typename... Args> + AsioStream(Botan::TLS::Context& context, Args&& ... args) + : Stream(context, args...) + { + m_native_handle = std::unique_ptr<MockChannel>(new MockChannel(m_core)); + } + + virtual ~AsioStream() = default; + }; + +class ThrowingAsioStream : public Botan::TLS::Stream<TestStream, ThrowingMockChannel> + { + public: + template <typename... Args> + ThrowingAsioStream(Botan::TLS::Context& context, Args&& ... args) + : Stream(context, args...) + { + m_native_handle = std::unique_ptr<ThrowingMockChannel>(new ThrowingMockChannel(m_core)); + } + + virtual ~ThrowingAsioStream() = default; + }; + +/** + * Synchronous tests for Botan::Stream. + * + * This test validates the asynchronous behavior Botan::Stream, including its utility classes StreamCore and Async_*_Op. + * The stream's channel, i.e. TLS_Client or TLS_Server, is mocked and pretends to perform TLS operations (noop) and + * provides the test data to the stream. + * The underlying network socket, claiming it read / wrote a number of bytes. + */ +class Asio_Stream_Tests final : public Test + { + // use memcmp to check if the data in a is a prefix of the data in b + bool contains(const void* a, const void* b, const std::size_t size) { return memcmp(a, b, size) == 0; } + + boost::string_view test_data() const + { + return boost::string_view(reinterpret_cast<const char*>(TEST_DATA), TEST_DATA_SIZE); + } + + void test_sync_handshake(std::vector<Test::Result>& results) + { + net::io_context ioc; + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, test_data()); + + ssl.handshake(Botan::TLS::CLIENT); + + Test::Result result("sync TLS handshake"); + result.test_eq("feeds data into channel until active", ssl.native_handle()->is_active(), true); + results.push_back(result); + } + + void test_sync_handshake_error(std::vector<Test::Result>& results) + { + net::io_context ioc; + // fail right away + FailCount fc{0, net::error::eof}; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, fc); + ssl.next_layer().connect(remote); + + // mimic handshake initialization + ssl.native_handle()->send(TEST_DATA, TEST_DATA_SIZE); + + error_code ec; + ssl.handshake(Botan::TLS::CLIENT, ec); + + Test::Result result("sync TLS handshake error"); + result.test_eq("does not activate channel", ssl.native_handle()->is_active(), false); + result.confirm("propagates error code", ec == net::error::eof); + results.push_back(result); + } + + void test_sync_handshake_throw(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + ThrowingAsioStream ssl(ctx, ioc, test_data()); + ssl.next_layer().connect(remote); + + error_code ec; + ssl.handshake(Botan::TLS::CLIENT, ec); + + Test::Result result("sync TLS handshake error"); + result.test_eq("does not activate channel", ssl.native_handle()->is_active(), false); + result.confirm("propagates error code", ec == ThrowingMockChannel::expected_ec()); + results.push_back(result); + } + + void test_async_handshake(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, test_data()); + ssl.next_layer().connect(remote); + + // mimic handshake initialization + ssl.native_handle()->send(TEST_DATA, TEST_DATA_SIZE); + + Test::Result result("async TLS handshake"); + + auto handler = [&](const error_code&) + { + result.confirm("reads from socket", ssl.next_layer().nread() > 0); + result.confirm("writes from socket", ssl.next_layer().nwrite() > 0); + result.test_eq("feeds data into channel until active", ssl.native_handle()->is_active(), true); + }; + + ssl.async_handshake(Botan::TLS::CLIENT, handler); + + ssl.next_layer().close_remote(); + ioc.run(); + results.push_back(result); + } + + void test_async_handshake_error(std::vector<Test::Result>& results) + { + net::io_context ioc; + // fail right away + FailCount fc{0, net::error::eof}; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, fc); + ssl.next_layer().connect(remote); + + // mimic handshake initialization + ssl.native_handle()->send(TEST_DATA, TEST_DATA_SIZE); + + Test::Result result("async TLS handshake error"); + + auto handler = [&](const error_code &ec) + { + result.test_eq("does not activate channel", ssl.native_handle()->is_active(), false); + result.confirm("propagates error code", ec == net::error::eof); + }; + + ssl.async_handshake(Botan::TLS::CLIENT, handler); + + ioc.run(); + results.push_back(result); + } + + void test_async_handshake_throw(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + ThrowingAsioStream ssl(ctx, ioc, test_data()); + ssl.next_layer().connect(remote); + + Test::Result result("async TLS handshake throw"); + + auto handler = [&](const error_code &ec) + { + result.test_eq("does not activate channel", ssl.native_handle()->is_active(), false); + result.confirm("propagates error code", ec == ThrowingMockChannel::expected_ec()); + }; + + ssl.async_handshake(Botan::TLS::CLIENT, handler); + + ioc.run(); + results.push_back(result); + } + + void test_sync_read_some_success(std::vector<Test::Result>& results) + { + net::io_context ioc; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, test_data()); + + const std::size_t buf_size = 128; + uint8_t buf[buf_size]; + error_code ec; + + auto bytes_transferred = net::read(ssl, net::mutable_buffer(buf, sizeof(buf)), ec); + + Test::Result result("sync read_some success"); + result.confirm("reads the correct data", contains(buf, TEST_DATA, buf_size)); + result.test_eq("reads the correct amount of data", bytes_transferred, buf_size); + result.confirm("does not report an error", !ec); + + results.push_back(result); + } + + void test_sync_read_some_buffer_sequence(std::vector<Test::Result>& results) + { + net::io_context ioc; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, test_data()); + error_code ec; + + std::vector<net::mutable_buffer> data; + uint8_t buf1[TEST_DATA_SIZE/2]; + uint8_t buf2[TEST_DATA_SIZE/2]; + data.emplace_back(net::mutable_buffer(buf1, TEST_DATA_SIZE/2)); + data.emplace_back(net::mutable_buffer(buf2, TEST_DATA_SIZE/2)); + + auto bytes_transferred = net::read(ssl, data, ec); + + Test::Result result("sync read_some buffer sequence"); + + result.confirm("reads the correct data", + contains(buf1, TEST_DATA, TEST_DATA_SIZE/2) && + contains(buf2, TEST_DATA+TEST_DATA_SIZE/2, TEST_DATA_SIZE/2)); + result.test_eq("reads the correct amount of data", bytes_transferred, TEST_DATA_SIZE); + result.confirm("does not report an error", !ec); + + results.push_back(result); + } + + void test_sync_read_some_error(std::vector<Test::Result>& results) + { + net::io_context ioc; + // fail right away + FailCount fc{0, net::error::eof}; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, fc); + ssl.next_layer().connect(remote); + + uint8_t buf[128]; + error_code ec; + + auto bytes_transferred = net::read(ssl, net::mutable_buffer(buf, sizeof(buf)), ec); + + Test::Result result("sync read_some error"); + result.test_eq("didn't transfer anything", bytes_transferred, 0); + result.confirm("propagates error code", ec == net::error::eof); + + results.push_back(result); + } + + void test_sync_read_some_throw(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + ThrowingAsioStream ssl(ctx, ioc, test_data()); + ssl.next_layer().connect(remote); + + uint8_t buf[128]; + error_code ec; + + auto bytes_transferred = net::read(ssl, net::mutable_buffer(buf, sizeof(buf)), ec); + + Test::Result result("sync read_some throw"); + result.test_eq("didn't transfer anything", bytes_transferred, 0); + result.confirm("propagates error code", ec == ThrowingMockChannel::expected_ec()); + + results.push_back(result); + } + + void test_sync_read_zero_buffer(std::vector<Test::Result>& results) + { + net::io_context ioc; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc); + + const std::size_t buf_size = 128; + uint8_t buf[buf_size]; + error_code ec; + + auto bytes_transferred = net::read(ssl, net::mutable_buffer(buf, std::size_t(0)), ec); + + Test::Result result("sync read_some into zero-size buffer"); + result.test_eq("reads the correct amount of data", bytes_transferred, 0); + // This relies on an implementation detail of TestStream: A "real" asio::tcp::stream + // would block here. TestStream sets error_code::eof. + result.confirm("does not report an error", !ec); + + results.push_back(result); + } + + void test_async_read_some_success(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, test_data()); + uint8_t data[TEST_DATA_SIZE]; + + Test::Result result("async read_some success"); + + auto read_handler = [&](const error_code &ec, std::size_t bytes_transferred) + { + result.confirm("reads the correct data", contains(data, TEST_DATA, TEST_DATA_SIZE)); + result.test_eq("reads the correct amount of data", bytes_transferred, TEST_DATA_SIZE); + result.confirm("does not report an error", !ec); + }; + + net::mutable_buffer buf {data, TEST_DATA_SIZE}; + net::async_read(ssl, buf, read_handler); + + ssl.next_layer().close_remote(); + ioc.run(); + results.push_back(result); + } + + void test_async_read_some_buffer_sequence(std::vector<Test::Result>& results) + { + net::io_context ioc; + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, test_data()); + + std::vector<net::mutable_buffer> data; + uint8_t buf1[TEST_DATA_SIZE/2]; + uint8_t buf2[TEST_DATA_SIZE/2]; + data.emplace_back(net::mutable_buffer(buf1, TEST_DATA_SIZE/2)); + data.emplace_back(net::mutable_buffer(buf2, TEST_DATA_SIZE/2)); + + Test::Result result("async read_some buffer sequence"); + + auto read_handler = [&](const error_code &ec, std::size_t bytes_transferred) + { + result.confirm("reads the correct data", + contains(buf1, TEST_DATA, TEST_DATA_SIZE/2) && + contains(buf2, TEST_DATA+TEST_DATA_SIZE/2, TEST_DATA_SIZE/2)); + result.test_eq("reads the correct amount of data", bytes_transferred, TEST_DATA_SIZE); + result.confirm("does not report an error", !ec); + }; + + net::async_read(ssl, data, read_handler); + + ssl.next_layer().close_remote(); + ioc.run(); + results.push_back(result); + } + + void test_async_read_some_error(std::vector<Test::Result>& results) + { + net::io_context ioc; + // fail right away + FailCount fc{0, net::error::eof}; + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, fc); + uint8_t data[TEST_DATA_SIZE]; + + Test::Result result("async read_some error"); + + auto read_handler = [&](const error_code &ec, std::size_t bytes_transferred) + { + result.test_eq("didn't transfer anything", bytes_transferred, 0); + result.confirm("propagates error code", ec == net::error::eof); + }; + + net::mutable_buffer buf {data, TEST_DATA_SIZE}; + net::async_read(ssl, buf, read_handler); + + ssl.next_layer().close_remote(); + ioc.run(); + results.push_back(result); + } + + void test_async_read_some_throw(std::vector<Test::Result>& results) + { + net::io_context ioc; + Botan::TLS::Context ctx; + ThrowingAsioStream ssl(ctx, ioc, test_data()); + uint8_t data[TEST_DATA_SIZE]; + + Test::Result result("async read_some throw"); + + auto read_handler = [&](const error_code &ec, std::size_t bytes_transferred) + { + result.test_eq("didn't transfer anything", bytes_transferred, 0); + result.confirm("propagates error code", ec == ThrowingMockChannel::expected_ec()); + }; + + net::mutable_buffer buf {data, TEST_DATA_SIZE}; + net::async_read(ssl, buf, read_handler); + + ssl.next_layer().close_remote(); + ioc.run(); + results.push_back(result); + } + + void test_async_read_zero_buffer(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc); + uint8_t data[TEST_DATA_SIZE]; + + Test::Result result("async read_some into zero-size buffer"); + + auto read_handler = [&](const error_code &ec, std::size_t bytes_transferred) + { + result.test_eq("reads the correct amount of data", bytes_transferred, 0); + // This relies on an implementation detail of TestStream: A "real" asio::tcp::stream + // would block here. TestStream sets error_code::eof. + result.confirm("does not report an error", !ec); + }; + + net::mutable_buffer buf {data, std::size_t(0)}; + net::async_read(ssl, buf, read_handler); + + ssl.next_layer().close_remote(); + ioc.run(); + results.push_back(result); + } + + void test_sync_write_some_success(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc); + ssl.next_layer().connect(remote); + error_code ec; + + auto bytes_transferred = net::write(ssl, net::const_buffer(TEST_DATA, TEST_DATA_SIZE), ec); + + Test::Result result("sync write_some success"); + result.confirm("writes the correct data", remote.str() == test_data()); + result.test_eq("writes the correct amount of data", bytes_transferred, TEST_DATA_SIZE); + result.confirm("does not report an error", !ec); + + results.push_back(result); + } + + void test_sync_write_some_buffer_sequence(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc); + ssl.next_layer().connect(remote); + error_code ec; + + // this should be Botan::TLS::MAX_PLAINTEXT_SIZE + 1024 + 1 + std::array<uint8_t, 17 * 1024 + 1> random_data; + random_data.fill('4'); // chosen by fair dice roll + random_data.back() = '5'; + + std::vector<net::const_buffer> data; + data.emplace_back(net::const_buffer(random_data.data(), 1)); + for(std::size_t i = 1; i < random_data.size(); i += 1024) + { + data.emplace_back(net::const_buffer(random_data.data() + i, 1024)); + } + + auto bytes_transferred = net::write(ssl, data, ec); + + Test::Result result("sync write_some buffer sequence"); + + result.confirm("[precondition] MAX_PLAINTEXT_SIZE is still smaller than random_data.size()", + Botan::TLS::MAX_PLAINTEXT_SIZE < random_data.size()); + + result.confirm("writes the correct data", + contains(remote.buffer().data().data(), random_data.data(), random_data.size())); + result.test_eq("writes the correct amount of data", bytes_transferred, random_data.size()); + result.test_eq("correct number of writes", ssl.next_layer().nwrite(), 2); + result.confirm("does not report an error", !ec); + + results.push_back(result); + } + + void test_sync_write_some_error(std::vector<Test::Result>& results) + { + net::io_context ioc; + // fail right away + FailCount fc{0, net::error::eof}; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, fc); + ssl.next_layer().connect(remote); + + error_code ec; + + auto bytes_transferred = net::write(ssl, net::const_buffer(TEST_DATA, TEST_DATA_SIZE), ec); + + Test::Result result("sync write_some error"); + result.test_eq("didn't transfer anything", bytes_transferred, 0); + result.confirm("propagates error code", ec == net::error::eof); + + results.push_back(result); + } + + void test_sync_write_some_throw(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + ThrowingAsioStream ssl(ctx, ioc); + ssl.next_layer().connect(remote); + error_code ec; + + auto bytes_transferred = net::write(ssl, net::const_buffer(TEST_DATA, TEST_DATA_SIZE), ec); + + Test::Result result("sync write_some throw"); + result.test_eq("didn't transfer anything", bytes_transferred, 0); + result.confirm("propagates error code", ec == ThrowingMockChannel::expected_ec()); + + results.push_back(result); + } + + void test_async_write_some_success(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc); + ssl.next_layer().connect(remote); + + Test::Result result("async write_some success"); + + auto write_handler = [&](const error_code &ec, std::size_t bytes_transferred) + { + result.confirm("writes the correct data", remote.str() == test_data()); + result.test_eq("writes the correct amount of data", bytes_transferred, TEST_DATA_SIZE); + result.confirm("does not report an error", !ec); + }; + + net::async_write(ssl, net::const_buffer(TEST_DATA, TEST_DATA_SIZE), write_handler); + + ioc.run(); + results.push_back(result); + } + + void test_async_write_some_buffer_sequence(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc); + ssl.next_layer().connect(remote); + + // this should be Botan::TLS::MAX_PLAINTEXT_SIZE + 1024 + 1 + std::array<uint8_t, 17 * 1024 + 1> random_data; + random_data.fill('4'); // chosen by fair dice roll + random_data.back() = '5'; + + std::vector<net::const_buffer> src; + src.emplace_back(net::const_buffer(random_data.data(), 1)); + for(std::size_t i = 1; i < random_data.size(); i += 1024) + { + src.emplace_back(net::const_buffer(random_data.data() + i, 1024)); + } + + Test::Result result("async write_some buffer sequence"); + + result.confirm("[precondition] MAX_PLAINTEXT_SIZE is still smaller than random_data.size()", + Botan::TLS::MAX_PLAINTEXT_SIZE < random_data.size()); + + auto write_handler = [&](const error_code &ec, std::size_t bytes_transferred) + { + result.confirm("writes the correct data", + contains(remote.buffer().data().data(), random_data.data(), random_data.size())); + result.test_eq("writes the correct amount of data", bytes_transferred, random_data.size()); + result.test_eq("correct number of writes", ssl.next_layer().nwrite(), 2); + result.confirm("does not report an error", !ec); + }; + + net::async_write(ssl, src, write_handler); + + ioc.run(); + results.push_back(result); + } + + void test_async_write_some_error(std::vector<Test::Result>& results) + { + net::io_context ioc; + // fail right away + FailCount fc{0, net::error::eof}; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + AsioStream ssl(ctx, ioc, fc); + ssl.next_layer().connect(remote); + + Test::Result result("async write_some error"); + + auto write_handler = [&](const error_code &ec, std::size_t bytes_transferred) + { + result.test_eq("committed some bytes to the core", bytes_transferred, TEST_DATA_SIZE); + result.confirm("propagates error code", ec == net::error::eof); + }; + + net::async_write(ssl, net::const_buffer(TEST_DATA, TEST_DATA_SIZE), write_handler); + + ioc.run(); + results.push_back(result); + } + + void test_async_write_throw(std::vector<Test::Result>& results) + { + net::io_context ioc; + TestStream remote{ioc}; + + Botan::TLS::Context ctx; + ThrowingAsioStream ssl(ctx, ioc); + ssl.next_layer().connect(remote); + + Test::Result result("async write_some throw"); + + auto write_handler = [&](const error_code &ec, std::size_t bytes_transferred) + { + result.test_eq("didn't transfer anything", bytes_transferred, 0); + result.confirm("propagates error code", ec == ThrowingMockChannel::expected_ec()); + }; + + net::async_write(ssl, net::const_buffer(TEST_DATA, TEST_DATA_SIZE), write_handler); + + ioc.run(); + results.push_back(result); + } + + public: + std::vector<Test::Result> run() override + { + std::vector<Test::Result> results; + + test_sync_handshake(results); + test_sync_handshake_error(results); + test_sync_handshake_throw(results); + + test_async_handshake(results); + test_async_handshake_error(results); + test_async_handshake_throw(results); + + test_sync_read_some_success(results); + test_sync_read_some_buffer_sequence(results); + test_sync_read_some_error(results); + test_sync_read_some_throw(results); + test_sync_read_zero_buffer(results); + + test_async_read_some_success(results); + test_async_read_some_buffer_sequence(results); + test_async_read_some_error(results); + test_async_read_some_throw(results); + test_async_read_zero_buffer(results); + + test_sync_write_some_success(results); + test_sync_write_some_buffer_sequence(results); + test_sync_write_some_error(results); + test_sync_write_some_throw(results); + + test_async_write_some_success(results); + test_async_write_some_buffer_sequence(results); + test_async_write_some_error(results); + test_async_write_throw(results); + + return results; + } + }; + +BOTAN_REGISTER_TEST("tls_asio_stream", Asio_Stream_Tests); + +} // namespace Botan_Tests + +#endif // BOOST_VERSION +#endif // BOTAN_HAS_TLS && BOTAN_HAS_BOOST_ASIO |