Repository: randombit/botan
Pull request #3795 (2023-11-02 16:35:26)

FIX: Alert handling in ASIO stream and HTTP Server
Turns out that #3778 seems to be caused by a long-standing bug in the ASIO stream wrapper. Consider a client sending a ClientHello that a server has to reject with a lack of compatible ciphersuites. Botan's TLS server would signal that with a `TLS_Exception(Alert::HandshakeFailure, "...")`.

The TLS stream wrapper (as the server) didn't properly handle that (as in: send a fatal alert message, terminate the connection and report a reasonable error code back to ASIO). Instead it just closed the connection immediately.
Likewise, when receiving a fatal alert message from a peer, the ASIO stream wrapper didn't report that back to the application as an error code but also simply terminated the connection.

This surfaced because the TLS Anvil nightly test is based on `./botan tls_http_server`, which is now based on the ASIO stream: #3763. Here we add proper handling for both cases and a suitable regression test.

Fortunately, the buggy behavior was arguable "okayish": The connection was hard-reset. Just, the application and the peer didn't get a proper error message. TLS-Anvil isn't satisfied with just that, though.

@FAlbertDev Let's verify tomorrow that this helps with #3778.


Patch:
diff --git a/.gitignore b/.gitignore
index f0b64ff48..169aa6ae5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 /Makefile
+CMakeLists.txt*
 build.ninja
 .ninja_log
 libbotan*.so.*
diff --git a/configure.py b/configure.py
index f8c5ba08b..3af0bd1e6 100755
--- a/configure.py
+++ b/configure.py
@@ -2315,9 +2315,6 @@ def create_template_vars(source_paths, build_paths, options, modules, cc, arch,
         if osinfo.soname_pattern_patch is not None:
             variables['soname_patch'] = osinfo.soname_pattern_patch.format(**variables)
 
-        if options.os == 'windows':
-            variables['implib_name'] = variables['static_lib_name']
-
         variables['lib_link_cmd'] = variables['lib_link_cmd'].format(**variables)
 
     for var in ['exe_link_cmd']:
@@ -3345,10 +3342,6 @@ def do_io_for_build(cc, arch, osinfo, using_mods, info_modules, build_paths, sou
     write_template(in_build_dir('build.h'), in_build_data('buildh.in'))
     write_template(in_build_dir('botan.doxy'), in_build_data('botan.doxy.in'))
 
-    robust_makedirs(in_build_dir("cmake"))
-    write_template(in_build_dir('cmake/botan-config.cmake'), in_build_data('botan-config.cmake.in'))
-    write_template(in_build_dir('cmake/botan-config-version.cmake'), in_build_data('botan-config-version.cmake.in'))
-
     if 'botan_pkgconfig' in template_vars:
         write_template(template_vars['botan_pkgconfig'], in_build_data('botan.pc.in'))
 
diff --git a/src/build-data/botan-config-version.cmake.in b/src/build-data/botan-config-version.cmake.in
deleted file mode 100644
index 890468d3a..000000000
--- a/src/build-data/botan-config-version.cmake.in
+++ /dev/null
@@ -1,16 +0,0 @@
-set(PACKAGE_VERSION %{version})
-
-# Botan follows semver:
-# * the requested version should be less or equal to the installed version, however
-# * the requested major version must fit exactly.
-
-if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION)
-  set(PACKAGE_VERSION_COMPATIBLE FALSE)
-elseif(PACKAGE_FIND_VERSION_MAJOR VERSION_LESS %{version_major})
-  set(PACKAGE_VERSION_COMPATIBLE FALSE)
-else()
-  set(PACKAGE_VERSION_COMPATIBLE TRUE)
-  if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION)
-    set(PACKAGE_VERSION_EXACT TRUE)
-  endif()
-endif()
diff --git a/src/build-data/botan-config.cmake.in b/src/build-data/botan-config.cmake.in
deleted file mode 100644
index 8d14c4e77..000000000
--- a/src/build-data/botan-config.cmake.in
+++ /dev/null
@@ -1,116 +0,0 @@
-#.rst:
-# botan-config.cmake
-# -----------
-#
-# Find the botan library.
-#
-# This CMake configuration file, installed as part of the Botan build,
-# provides support for find_package(Botan).
-#
-# Required version(s) can be passed as usual:
-# find_package(Botan 3.3.0 REQUIRED)
-#
-# COMPONENTS and OPTIONAL_COMPONENTS can be used to specify Botan
-# modules that must or should be enabled in the Botan build:
-# find_package(Botan 3.3.0 COMPONENTS rsa ecdsa)
-#
-# IMPORTED Targets
-# ^^^^^^^^^^^^^^^^
-#
-# This module defines :prop_tgt:`IMPORTED` targets:
-#
-# ``Botan::Botan``
-#   The botan shared library, if found.
-# ``Botan::Botan-static``
-#   The botan static library, if found.
-#
-# Result variables
-# ^^^^^^^^^^^^^^^^
-#
-# This module defines the following variables:
-#
-# ::
-#
-#   Botan_FOUND          - true if the headers and library were found
-#   Botan_VERSION        - library version that was found, if any
-#
-
-set(_Botan_supported_components
-%{for mod_list}
-%{i}
-%{endfor}
-)
-
-unset(${CMAKE_FIND_PACKAGE_NAME}_FOUND)
-unset(_Botan_missing_required_modules)
-
-foreach(_comp IN LISTS ${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS)
-  if (NOT _comp IN_LIST _Botan_supported_components)
-    set(${CMAKE_FIND_PACKAGE_NAME}_${_comp}_FOUND False)
-    if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED_${_comp})
-      list(APPEND _Botan_missing_required_modules ${_comp})
-    endif()
-  else()
-    set(${CMAKE_FIND_PACKAGE_NAME}_${_comp}_FOUND True)
-  endif()
-endforeach()
-
-if(_Botan_missing_required_modules)
-  set(${CMAKE_FIND_PACKAGE_NAME}_FOUND False)
-  list(JOIN _Botan_missing_required_modules ", " _missing_modules)
-  set(${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE "Unsupported module(s): ${_missing_modules}")
-endif()
-
-if(DEFINED ${CMAKE_FIND_PACKAGE_NAME}_FOUND AND NOT ${${CMAKE_FIND_PACKAGE_NAME}_FOUND})
-  return()
-endif()
-
-# botan-config.cmake lives in "${_Botan_PREFIX}/lib/cmake/Botan-X": traverse up to $_Botan_PREFIX
-set(_Botan_PREFIX "${CMAKE_CURRENT_LIST_DIR}")
-get_filename_component(_Botan_PREFIX "${_Botan_PREFIX}" DIRECTORY)
-get_filename_component(_Botan_PREFIX "${_Botan_PREFIX}" DIRECTORY)
-get_filename_component(_Botan_PREFIX "${_Botan_PREFIX}" DIRECTORY)
-
-%{if build_static_lib}
-if(NOT TARGET Botan::Botan-static)
-  add_library(Botan::Botan-static STATIC IMPORTED)
-  set_target_properties(Botan::Botan-static
-    PROPERTIES
-      IMPORTED_LOCATION                 "${_Botan_PREFIX}/lib/%{static_lib_name}"
-      INTERFACE_INCLUDE_DIRECTORIES     "${_Botan_PREFIX}/include/botan-%{version_major}"
-      IMPORTED_LINK_INTERFACE_LANGUAGES "CXX"
-      INTERFACE_LINK_OPTIONS            "SHELL:%{cxx_abi_flags}")
-endif()
-%{endif}
-
-%{if implib_name}
-set(_Botan_implib     "${_Botan_PREFIX}/lib/%{implib_name}")
-set(_Botan_shared_lib "${_Botan_PREFIX}/bin/%{shared_lib_name}")
-%{endif}
-%{unless implib_name}
-set(_Botan_implib "")
-%{endif}
-
-%{if build_shared_lib}
-if(NOT TARGET Botan::Botan)
-  if(NOT DEFINED _Botan_shared_lib)
-    set(_Botan_shared_lib "${_Botan_PREFIX}/lib/%{shared_lib_name}")
-  endif()
-
-  add_library(Botan::Botan SHARED IMPORTED)
-  set_target_properties(Botan::Botan
-    PROPERTIES
-      IMPORTED_LOCATION             "${_Botan_shared_lib}"
-      IMPORTED_IMPLIB               "${_Botan_implib}"
-      INTERFACE_INCLUDE_DIRECTORIES "${_Botan_PREFIX}/include/botan-%{version_major}"
-      INTERFACE_LINK_OPTIONS        "SHELL:%{cxx_abi_flags}")
-  set_property(TARGET Botan::Botan APPEND PROPERTY IMPORTED_CONFIGURATIONS NOCONFIG)
-  set_target_properties(Botan::Botan
-    PROPERTIES
-      IMPORTED_LOCATION_NOCONFIG "${_Botan_PREFIX}/lib/%{shared_lib_name}"
-      IMPORTED_SONAME_NOCONFIG   "%{shared_lib_name}"
-      IMPORTED_IMPLIB_NOCONFIG   "${_Botan_implib}")
-endif()
-%{endif}
-
-set(${CMAKE_FIND_PACKAGE_NAME}_FOUND True)
diff --git a/src/cli/tls_http_server.cpp b/src/cli/tls_http_server.cpp
index a6958079e..e5c57f22d 100644
--- a/src/cli/tls_http_server.cpp
+++ b/src/cli/tls_http_server.cpp
@@ -226,6 +226,8 @@ net::awaitable<void> do_session(tcp_stream stream,
    auto callbacks = std::make_shared<TlsHttpCallbacks>();
    Botan::TLS::Stream<tcp_stream> tls_stream(std::move(stream), std::move(tls_ctx), callbacks);
 
+   std::exception_ptr protocol_exception;
+
    try {
       // Perform a TLS handshake with the peer
       co_await tls_stream.async_handshake(Botan::TLS::Connection_Side::Server);
@@ -253,14 +255,31 @@ net::awaitable<void> do_session(tcp_stream stream,
       }
    } catch(boost::system::system_error& se) {
       if(se.code() != http::error::end_of_stream) {
+         // Something went wrong during the communication, as good citizens we
+         // try to shutdown the connection gracefully, anyway. The protocol
+         // exception is kept for later re-throw.
+         protocol_exception = std::current_exception();
+      }
+   }
+
+   try {
+      // Shut down the connection gracefully. It gives the stream a chance to
+      // flush remaining send buffers and/or close the connection gracefully. If
+      // the communication above failed this may or may not be successful.
+      co_await tls_stream.async_shutdown();
+      tls_stream.next_layer().close();
+   } catch(const std::exception&) {
+      // if the protocol interaction above produced an exception the shutdown
+      // was "best effort" anyway and we swallow the secondary exception that
+      // happened during shutdown.
+      if(!protocol_exception) {
          throw;
       }
    }
 
-   // Shut down the connection gracefully
-   co_await tls_stream.async_shutdown();
-   beast::error_code ec;
-   tls_stream.next_layer().socket().shutdown(tcp::socket::shutdown_send, ec);
+   if(protocol_exception) {
+      std::rethrow_exception(protocol_exception);
+   }
 
    // At this point the connection is closed gracefully
    // we ignore the error because the client might have
diff --git a/src/examples/aes.cpp b/src/examples/aes.cpp
index e3944b72f..7c3ab9dbc 100644
--- a/src/examples/aes.cpp
+++ b/src/examples/aes.cpp
@@ -6,10 +6,10 @@
 int main() {
    std::vector<uint8_t> key = Botan::hex_decode("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F");
    std::vector<uint8_t> block = Botan::hex_decode("00112233445566778899AABBCCDDEEFF");
-   const auto cipher = Botan::BlockCipher::create_or_throw("AES-256");
+   auto cipher = Botan::BlockCipher::create("AES-256");
    cipher->set_key(key);
    cipher->encrypt(block);
-   std::cout << '\n' << cipher->name() << "single block encrypt: " << Botan::hex_encode(block);
+   std::cout << std::endl << cipher->name() << "single block encrypt: " << Botan::hex_encode(block);
 
    // clear cipher for 2nd encryption with other key
    cipher->clear();
@@ -17,6 +17,6 @@ int main() {
    cipher->set_key(key);
    cipher->encrypt(block);
 
-   std::cout << '\n' << cipher->name() << "single block encrypt: " << Botan::hex_encode(block);
+   std::cout << std::endl << cipher->name() << "single block encrypt: " << Botan::hex_encode(block);
    return 0;
 }
diff --git a/src/examples/aes_cbc.cpp b/src/examples/aes_cbc.cpp
index 032acd14c..5375106ea 100644
--- a/src/examples/aes_cbc.cpp
+++ b/src/examples/aes_cbc.cpp
@@ -13,7 +13,7 @@ int main() {
       "luck. Unfortunately, Dane's luck wasn't as good as his old man's.");
    const std::vector<uint8_t> key = Botan::hex_decode("2B7E151628AED2A6ABF7158809CF4F3C");
 
-   const auto enc = Botan::Cipher_Mode::create_or_throw("AES-128/CBC/PKCS7", Botan::Cipher_Dir::Encryption);
+   auto enc = Botan::Cipher_Mode::create("AES-128/CBC/PKCS7", Botan::Cipher_Dir::Encryption);
    enc->set_key(key);
 
    // generate fresh nonce (IV)
@@ -25,6 +25,6 @@ int main() {
    enc->start(iv);
    enc->finish(pt);
 
-   std::cout << enc->name() << " with iv " << Botan::hex_encode(iv) << " " << Botan::hex_encode(pt) << '\n';
+   std::cout << enc->name() << " with iv " << Botan::hex_encode(iv) << " " << Botan::hex_encode(pt) << "\n";
    return 0;
 }
diff --git a/src/examples/chacha.cpp b/src/examples/chacha.cpp
index 8c253fae1..8ef8f9af0 100644
--- a/src/examples/chacha.cpp
+++ b/src/examples/chacha.cpp
@@ -9,7 +9,7 @@ int main() {
    std::vector<uint8_t> pt(plaintext.data(), plaintext.data() + plaintext.length());
    const std::vector<uint8_t> key =
       Botan::hex_decode("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F");
-   const auto cipher = Botan::StreamCipher::create_or_throw("ChaCha(20)");
+   auto cipher = Botan::StreamCipher::create("ChaCha(20)");
 
    // generate fresh nonce (IV)
    Botan::AutoSeeded_RNG rng;
@@ -20,6 +20,6 @@ int main() {
    cipher->set_iv(iv);
    cipher->encipher(pt);
 
-   std::cout << cipher->name() << " with iv " << Botan::hex_encode(iv) << ": " << Botan::hex_encode(pt) << '\n';
+   std::cout << cipher->name() << " with iv " << Botan::hex_encode(iv) << ": " << Botan::hex_encode(pt) << "\n";
    return 0;
 }
diff --git a/src/examples/cmac.cpp b/src/examples/cmac.cpp
index 55eded856..d51b7a4b6 100644
--- a/src/examples/cmac.cpp
+++ b/src/examples/cmac.cpp
@@ -6,7 +6,7 @@
 int main() {
    const std::vector<uint8_t> key = Botan::hex_decode("2B7E151628AED2A6ABF7158809CF4F3C");
    std::vector<uint8_t> data = Botan::hex_decode("6BC1BEE22E409F96E93D7E117393172A");
-   const auto mac = Botan::MessageAuthenticationCode::create_or_throw("CMAC(AES-128)");
+   std::unique_ptr<Botan::MessageAuthenticationCode> mac(Botan::MessageAuthenticationCode::create("CMAC(AES-128)"));
    if(!mac) {
       return 1;
    }
diff --git a/src/examples/dl_group.cpp b/src/examples/dl_group.cpp
index 6ecda5004..fc21b9c8c 100644
--- a/src/examples/dl_group.cpp
+++ b/src/examples/dl_group.cpp
@@ -7,10 +7,10 @@
 int main() {
    Botan::AutoSeeded_RNG rng;
    auto group = std::make_unique<Botan::DL_Group>(rng, Botan::DL_Group::Strong, 2048);
-   std::cout << "\np: " << group->get_p();
-   std::cout << "\nq: " << group->get_q();
-   std::cout << "\ng: " << group->get_q();
-   std::cout << "\nANSI_X9_42:\n" << group->PEM_encode(Botan::DL_Group_Format::ANSI_X9_42);
+   std::cout << std::endl << "p: " << group->get_p();
+   std::cout << std::endl << "q: " << group->get_q();
+   std::cout << std::endl << "g: " << group->get_q();
+   std::cout << std::endl << "ANSI_X9_42: " << std::endl << group->PEM_encode(Botan::DL_Group_Format::ANSI_X9_42);
 
    return 0;
 }
diff --git a/src/examples/ecdh.cpp b/src/examples/ecdh.cpp
index 93f5af86c..4d01e207e 100644
--- a/src/examples/ecdh.cpp
+++ b/src/examples/ecdh.cpp
@@ -32,6 +32,6 @@ int main() {
       return 1;
    }
 
-   std::cout << "agreed key:\n" << Botan::hex_encode(sA);
+   std::cout << "agreed key: " << std::endl << Botan::hex_encode(sA);
    return 0;
 }
diff --git a/src/examples/ecdsa.cpp b/src/examples/ecdsa.cpp
index 536c53756..f835ff408 100644
--- a/src/examples/ecdsa.cpp
+++ b/src/examples/ecdsa.cpp
@@ -17,11 +17,11 @@ int main() {
    Botan::PK_Signer signer(key, rng, "SHA-256");
    signer.update(message);
    std::vector<uint8_t> signature = signer.signature(rng);
-   std::cout << "Signature:\n" << Botan::hex_encode(signature);
+   std::cout << "Signature:" << std::endl << Botan::hex_encode(signature);
 
    // now verify the signature
    Botan::PK_Verifier verifier(key, "SHA-256");
    verifier.update(message);
-   std::cout << "\nis " << (verifier.check_signature(signature) ? "valid" : "invalid");
+   std::cout << std::endl << "is " << (verifier.check_signature(signature) ? "valid" : "invalid");
    return 0;
 }
diff --git a/src/examples/entropy.cpp b/src/examples/entropy.cpp
index 5351259c4..ab1c23a56 100644
--- a/src/examples/entropy.cpp
+++ b/src/examples/entropy.cpp
@@ -108,5 +108,5 @@ int main() {
    es.add_source(std::make_unique<Timer_Entropy_Source>());
    es.add_source(std::make_unique<Hardware_RNG_Entropy_Source>());
    Botan::AutoSeeded_RNG rng(es);
-   std::cout << hex_encode(rng.random_vec(32)) << '\n';
+   std::cout << hex_encode(rng.random_vec(32)) << "\n";
 }
diff --git a/src/examples/gmac.cpp b/src/examples/gmac.cpp
index b4ba81490..83ab0f4da 100644
--- a/src/examples/gmac.cpp
+++ b/src/examples/gmac.cpp
@@ -8,7 +8,7 @@ int main() {
       Botan::hex_decode("1337133713371337133713371337133713371337133713371337133713371337");
    const std::vector<uint8_t> nonce = Botan::hex_decode("FFFFFFFFFFFFFFFFFFFFFFFF");
    const std::vector<uint8_t> data = Botan::hex_decode("6BC1BEE22E409F96E93D7E117393172A");
-   const auto mac = Botan::MessageAuthenticationCode::create_or_throw("GMAC(AES-256)");
+   std::unique_ptr<Botan::MessageAuthenticationCode> mac(Botan::MessageAuthenticationCode::create("GMAC(AES-256)"));
    if(!mac) {
       return 1;
    }
@@ -16,7 +16,7 @@ int main() {
    mac->start(nonce);
    mac->update(data);
    Botan::secure_vector<uint8_t> tag = mac->final();
-   std::cout << mac->name() << ": " << Botan::hex_encode(tag) << '\n';
+   std::cout << mac->name() << ": " << Botan::hex_encode(tag) << std::endl;
 
    // Verify created MAC
    mac->start(nonce);
diff --git a/src/examples/hash.cpp b/src/examples/hash.cpp
index cf575e61a..c4de11094 100644
--- a/src/examples/hash.cpp
+++ b/src/examples/hash.cpp
@@ -4,9 +4,9 @@
 #include <iostream>
 
 int main() {
-   const auto hash1 = Botan::HashFunction::create_or_throw("SHA-256");
-   const auto hash2 = Botan::HashFunction::create_or_throw("SHA-384");
-   const auto hash3 = Botan::HashFunction::create_or_throw("SHA-3");
+   auto hash1 = Botan::HashFunction::create("SHA-256");
+   auto hash2 = Botan::HashFunction::create("SHA-384");
+   auto hash3 = Botan::HashFunction::create("SHA-3");
    std::vector<uint8_t> buf(2048);
 
    while(std::cin.good()) {
@@ -18,8 +18,8 @@ int main() {
       hash2->update(buf.data(), readcount);
       hash3->update(buf.data(), readcount);
    }
-   std::cout << "SHA-256: " << Botan::hex_encode(hash1->final()) << '\n';
-   std::cout << "SHA-384: " << Botan::hex_encode(hash2->final()) << '\n';
-   std::cout << "SHA-3: " << Botan::hex_encode(hash3->final()) << '\n';
+   std::cout << "SHA-256: " << Botan::hex_encode(hash1->final()) << std::endl;
+   std::cout << "SHA-384: " << Botan::hex_encode(hash2->final()) << std::endl;
+   std::cout << "SHA-3: " << Botan::hex_encode(hash3->final()) << std::endl;
    return 0;
 }
diff --git a/src/examples/hybrid_encryption.cpp b/src/examples/hybrid_encryption.cpp
index 6e3819036..8259e8786 100644
--- a/src/examples/hybrid_encryption.cpp
+++ b/src/examples/hybrid_encryption.cpp
@@ -78,7 +78,7 @@ int main() {
    const auto ciphertext = encrypt(as<Botan::secure_vector<uint8_t>>(plaintext), privkey->public_key(), rng);
    const auto new_plaintext = decrypt(ciphertext, *privkey, rng);
 
-   std::cout << as<std::string>(new_plaintext) << '\n';
+   std::cout << as<std::string>(new_plaintext) << std::endl;
 
    return 0;
 }
diff --git a/src/examples/kdf.cpp b/src/examples/kdf.cpp
index e295cfbdc..644b8d22b 100644
--- a/src/examples/kdf.cpp
+++ b/src/examples/kdf.cpp
@@ -14,5 +14,5 @@ int main() {
    auto derived_key = kdf->derive_key(derived_key_len, input_secret, salt, label);
 
    // OKM = 0x3cb25f25faacd57a90434f64d0362f2a...
-   std::cout << Botan::hex_encode(derived_key) << '\n';
+   std::cout << Botan::hex_encode(derived_key) << "\n";
 }
diff --git a/src/examples/pkcs11_module.cpp b/src/examples/pkcs11_module.cpp
index 38ab15726..db6d80915 100644
--- a/src/examples/pkcs11_module.cpp
+++ b/src/examples/pkcs11_module.cpp
@@ -13,7 +13,8 @@ int main() {
    Botan::PKCS11::Info info = module.get_info();
 
    // print library version
-   std::cout << std::to_string(info.libraryVersion.major) << "." << std::to_string(info.libraryVersion.minor) << '\n';
+   std::cout << std::to_string(info.libraryVersion.major) << "." << std::to_string(info.libraryVersion.minor)
+             << std::endl;
 
    return 0;
 }
diff --git a/src/examples/pkcs11_session.cpp b/src/examples/pkcs11_session.cpp
index fdcef3a8a..6dc0e257b 100644
--- a/src/examples/pkcs11_session.cpp
+++ b/src/examples/pkcs11_session.cpp
@@ -36,7 +36,7 @@ int main() {
 
    // get session info
    Botan::PKCS11::SessionInfo info = session.get_info();
-   std::cout << info.slotID << '\n';
+   std::cout << info.slotID << std::endl;
 
    // login
    Botan::PKCS11::secure_string pin = {'1', '2', '3', '4', '5', '6'};
diff --git a/src/examples/pkcs11_slot.cpp b/src/examples/pkcs11_slot.cpp
index 7096cd282..d5c181812 100644
--- a/src/examples/pkcs11_slot.cpp
+++ b/src/examples/pkcs11_slot.cpp
@@ -17,12 +17,12 @@ int main() {
    // print firmware version of the slot
    Botan::PKCS11::SlotInfo slot_info = slot.get_slot_info();
    std::cout << std::to_string(slot_info.firmwareVersion.major) << "."
-             << std::to_string(slot_info.firmwareVersion.minor) << '\n';
+             << std::to_string(slot_info.firmwareVersion.minor) << std::endl;
 
    // print firmware version of the token
    Botan::PKCS11::TokenInfo token_info = slot.get_token_info();
    std::cout << std::to_string(token_info.firmwareVersion.major) << "."
-             << std::to_string(token_info.firmwareVersion.minor) << '\n';
+             << std::to_string(token_info.firmwareVersion.minor) << std::endl;
 
    // retrieve all mechanisms supported by the token
    std::vector<Botan::PKCS11::MechanismType> mechanisms = slot.get_mechanism_list();
@@ -31,7 +31,7 @@ int main() {
    Botan::PKCS11::MechanismInfo mech_info = slot.get_mechanism_info(Botan::PKCS11::MechanismType::RsaPkcsOaep);
 
    // maximum RSA key length supported:
-   std::cout << mech_info.ulMaxKeySize << '\n';
+   std::cout << mech_info.ulMaxKeySize << std::endl;
 
    // initialize the token
    Botan::PKCS11::secure_string so_pin(8, '0');
diff --git a/src/examples/pwdhash.cpp b/src/examples/pwdhash.cpp
index fbf5b47be..70e52a192 100644
--- a/src/examples/pwdhash.cpp
+++ b/src/examples/pwdhash.cpp
@@ -15,7 +15,7 @@ int main() {
 
    auto pwdhash = pwd_fam->tune(output_hash, pbkdf_runtime, max_pbkdf_mb);
 
-   std::cout << "Using params " << pwdhash->to_string() << '\n';
+   std::cout << "Using params " << pwdhash->to_string() << "\n";
 
    std::array<uint8_t, 32> salt;
    Botan::system_rng().randomize(salt);
@@ -25,7 +25,7 @@ int main() {
    std::array<uint8_t, output_hash> key;
    pwdhash->hash(key, password, salt);
 
-   std::cout << Botan::hex_encode(key) << '\n';
+   std::cout << Botan::hex_encode(key) << "\n";
 
    return 0;
 }
diff --git a/src/examples/rsa_encrypt.cpp b/src/examples/rsa_encrypt.cpp
index 1046a0f6c..c204be3a4 100644
--- a/src/examples/rsa_encrypt.cpp
+++ b/src/examples/rsa_encrypt.cpp
@@ -29,7 +29,7 @@ int main(int argc, char* argv[]) {
    Botan::PK_Decryptor_EME dec(*kp, rng, "OAEP(SHA-256)");
    Botan::secure_vector<uint8_t> pt2 = dec.decrypt(ct);
 
-   std::cout << "\nenc: " << Botan::hex_encode(ct) << "\ndec: " << Botan::hex_encode(pt2);
+   std::cout << std::endl << "enc: " << Botan::hex_encode(ct) << std::endl << "dec: " << Botan::hex_encode(pt2);
 
    return 0;
 }
diff --git a/src/examples/tls_stream_client.cpp b/src/examples/tls_stream_client.cpp
index f168301cc..1c591c920 100644
--- a/src/examples/tls_stream_client.cpp
+++ b/src/examples/tls_stream_client.cpp
@@ -49,7 +49,7 @@ class client {
 
       void handle_connect(const boost::system::error_code& error) {
          if(error) {
-            std::cout << "Connect failed: " << error.message() << '\n';
+            std::cout << "Connect failed: " << error.message() << "\n";
             return;
          }
          m_stream.async_handshake(Botan::TLS::Connection_Side::Client,
@@ -58,7 +58,7 @@ class client {
 
       void handle_handshake(const boost::system::error_code& error) {
          if(error) {
-            std::cout << "Handshake failed: " << error.message() << '\n';
+            std::cout << "Handshake failed: " << error.message() << "\n";
             return;
          }
          http::async_write(
@@ -67,7 +67,7 @@ class client {
 
       void handle_write(const boost::system::error_code& error, size_t) {
          if(error) {
-            std::cout << "Write failed: " << error.message() << '\n';
+            std::cout << "Write failed: " << error.message() << "\n";
             return;
          }
          http::async_read(
@@ -77,9 +77,9 @@ class client {
       void handle_read(const boost::system::error_code& error, size_t) {
          if(!error) {
             std::cout << "Reply: ";
-            std::cout << m_response.body() << '\n';
+            std::cout << m_response.body() << "\n";
          } else {
-            std::cout << "Read failed: " << error.message() << '\n';
+            std::cout << "Read failed: " << error.message() << "\n";
          }
       }
 
diff --git a/src/examples/xmss.cpp b/src/examples/xmss.cpp
index 40f0cadd4..ed09e88fb 100644
--- a/src/examples/xmss.cpp
+++ b/src/examples/xmss.cpp
@@ -29,10 +29,10 @@ int main() {
    // verify the signature for the previously generated message.
    verifier.update(msg.data(), msg.size());
    if(verifier.check_signature(sig.data(), sig.size())) {
-      std::cout << "Success.\n";
+      std::cout << "Success." << std::endl;
       return 0;
    } else {
-      std::cout << "Error.\n";
+      std::cout << "Error." << std::endl;
       return 1;
    }
 }
diff --git a/src/lib/stream/stream_cipher.cpp b/src/lib/stream/stream_cipher.cpp
index b24584ac7..470396d86 100644
--- a/src/lib/stream/stream_cipher.cpp
+++ b/src/lib/stream/stream_cipher.cpp
@@ -8,7 +8,6 @@
 #include <botan/stream_cipher.h>
 
 #include <botan/exceptn.h>
-#include <botan/mem_ops.h>
 #include <botan/internal/scan_name.h>
 
 #if defined(BOTAN_HAS_CHACHA)
diff --git a/src/lib/tls/asio/asio_async_ops.h b/src/lib/tls/asio/asio_async_ops.h
index 9601f0342..fb0e3afe7 100644
--- a/src/lib/tls/asio/asio_async_ops.h
+++ b/src/lib/tls/asio/asio_async_ops.h
@@ -252,6 +252,10 @@ class AsyncHandshakeOperation : public AsyncBase<Handler, typename Stream::execu
 
       void operator()(boost::system::error_code ec, std::size_t bytesTransferred, bool isContinuation = true) {
          reenter(this) {
+            if(!ec && m_stashed_ec) {
+               ec = std::exchange(m_stashed_ec, {});
+            }
+
             if(ec == boost::asio::error::eof) {
                ec = StreamError::StreamTruncated;
             }
@@ -260,6 +264,16 @@ class AsyncHandshakeOperation : public AsyncBase<Handler, typename Stream::execu
                // 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};
                m_stream.process_encrypted_data(read_buffer, ec);
+
+               // m_stream.process_encrypted_data() might set an error code based
+               // on the data received from the peer but also generate data that
+               // must be sent to the peer (ie. an alert like 'handshake_failure').
+               // In that case, we stash the error code and continue. This will let
+               // the next block write the alert message and the stashed error code
+               // will be re-produced in the next iteration.
+               if(ec && m_stream.has_data_to_send()) {
+                  m_stashed_ec = std::exchange(ec, {});
+               }
             }
 
             if(!ec && m_stream.has_data_to_send()) {
@@ -297,6 +311,7 @@ class AsyncHandshakeOperation : public AsyncBase<Handler, typename Stream::execu
    private:
       Stream& m_stream;
       boost::system::error_code m_ec;
+      boost::system::error_code m_stashed_ec;
 };
 
 }  // namespace Botan::TLS::detail
diff --git a/src/lib/tls/asio/asio_stream.h b/src/lib/tls/asio/asio_stream.h
index 33afc764e..74153a14c 100644
--- a/src/lib/tls/asio/asio_stream.h
+++ b/src/lib/tls/asio/asio_stream.h
@@ -57,7 +57,7 @@ class Stream;
  */
 class StreamCallbacks : public Callbacks {
    public:
-      StreamCallbacks() : m_shutdown_received(false) {}
+      StreamCallbacks() {}
 
       void tls_emit_data(std::span<const uint8_t> data) final {
          m_send_buffer.commit(boost::asio::buffer_copy(m_send_buffer.prepare(data.size()),
@@ -76,10 +76,17 @@ class StreamCallbacks : public Callbacks {
       }
 
       void tls_alert(TLS::Alert alert) final {
-         if(alert.type() == TLS::AlertType::CloseNotify) {
-            m_shutdown_received = true;
-            // Channel::process_alert will automatically write the corresponding close_notify response to the
-            // send_buffer and close the native_handle after this function returns.
+         if(alert.is_fatal() || alert.type() == TLS::AlertType::CloseNotify) {
+            // Channel::process_alert() will automatically handle fatal errors.
+            //
+            // For close_notify it writes the corresponding response to the
+            // send_buffer and closes the native_handle after this function
+            // returns.
+            //
+            // For other alerts it will shutdown the native_handle and
+            // Stream::process_encrypted_data() will hand out the received alert
+            // as an error code.
+            m_alert_from_peer = alert;
          }
       }
 
@@ -115,10 +122,14 @@ class StreamCallbacks : public Callbacks {
 
       const boost::beast::flat_buffer& receive_buffer() const { return m_receive_buffer; }
 
-      bool shutdown_received() const { return m_shutdown_received; }
+      bool shutdown_received() const {
+         return m_alert_from_peer && m_alert_from_peer->type() == AlertType::CloseNotify;
+      }
+
+      std::optional<Alert> alert_from_peer() const { return m_alert_from_peer; }
 
    private:
-      bool m_shutdown_received;
+      std::optional<Alert> m_alert_from_peer;
       boost::beast::flat_buffer m_receive_buffer;
       boost::beast::flat_buffer m_send_buffer;
 
@@ -341,7 +352,15 @@ class Stream {
 
             process_encrypted_data(read_buffer, ec);
 
-            send_pending_encrypted_data(ec);
+            // process_encrypted_data() might set an error code based on the
+            // data received from the peer but also generate data that must be
+            // sent to the peer (ie. an alert like 'handshake_failure'). In that
+            // case, we ignore the error code and send the data regardless.
+            if(has_data_to_send()) {
+               boost::system::error_code send_ec;
+               send_pending_encrypted_data(send_ec);
+               ec = (send_ec) ? send_ec : ec;
+            }
          }
       }
 
@@ -764,11 +783,25 @@ class Stream {
        * @param ec Set to indicate what error occurred, if any.
        */
       void process_encrypted_data(const boost::asio::const_buffer& read_buffer, boost::system::error_code& ec) {
+         BOTAN_ASSERT(!m_core->alert_from_peer(), "peer didn't send an alert before (no data allowed after that)");
+
+         // If the local TLS implementation generates an alert, we are notified
+         // with an exception that is caught in try_with_error_code().
          try_with_error_code(
             [&] {
                native_handle()->received_data({static_cast<const uint8_t*>(read_buffer.data()), read_buffer.size()});
             },
             ec);
+
+         if(!ec) {
+            // When the peer sends an alert, _no exception_ is called in the
+            // local TLS implementation but a callback (on m_core) is invoked to
+            // notify about the peer's alert.
+            const auto alert = m_core->alert_from_peer();
+            if(alert && alert->type() != AlertType::CloseNotify) {
+               ec = alert->type();
+            }
+         }
       }
 
       //! @brief Catch exceptions and set an error_code
diff --git a/src/scripts/ci_check_install.py b/src/scripts/ci_check_install.py
index dd4d88e01..e691bd05a 100755
--- a/src/scripts/ci_check_install.py
+++ b/src/scripts/ci_check_install.py
@@ -6,7 +6,6 @@ Botan CI check installation script
 This script is used to validate the results of `make install`
 
 (C) 2020 Jack Lloyd, René Meusel, Hannes Rantzsch
-(C) 2023 René Meusel
 
 Botan is released under the Simplified BSD License (see license.txt)
 """
@@ -15,7 +14,6 @@ import os
 import sys
 import json
 import re
-import subprocess
 
 def verify_library(build_config):
     lib_dir = build_config['libdir']
@@ -77,34 +75,6 @@ def verify_includes(build_config):
 
     return True
 
-def verify_cmake_package(build_config):
-    if build_config['os'] not in ['windows', 'linux', 'macos']:
-        return True # skip (e.g. for mingw)
-
-    cmake_build_dir = os.path.join(build_config['abs_root_dir'], build_config['build_dir'], 'cmake_test')
-    cmake_test_dir = os.path.join(build_config['abs_root_dir'], "src", "scripts", "ci", "cmake_tests")
-
-    def cmake_preset():
-        if build_config['os'] == 'windows':
-            return 'windows_x86_64' if build_config['arch'] == 'x86_64' else 'windows_x86'
-        return 'unix'
-
-    def test_target():
-        return 'test' if build_config['os'] != 'windows' else 'RUN_TESTS'
-
-    try:
-        subprocess.run(["cmake", "--preset", cmake_preset(),
-                                 "-B", cmake_build_dir,
-                                 "-S", cmake_test_dir,
-                                 "-DCMAKE_PREFIX_PATH=%s" % build_config['prefix']], check=True)
-        subprocess.run(["cmake", "--build", cmake_build_dir, "--config", "Release"], check=True)
-        subprocess.run(["cmake", "--build", cmake_build_dir, "--config", "Release", "--target", test_target()], check=True)
-    except RuntimeError as e:
-        print("Using the CMake package failed: %s" % str(e))
-        return False
-
-    return True
-
 def main(args=None):
     if args is None:
         args = sys.argv
@@ -128,9 +98,6 @@ def main(args=None):
     if not verify_library(build_config):
         return 1
 
-    if not verify_cmake_package(build_config):
-        return 1
-
     return 0
 
 if __name__ == '__main__':
diff --git a/src/scripts/install.py b/src/scripts/install.py
index 81ba766f9..f4aee90b8 100755
--- a/src/scripts/install.py
+++ b/src/scripts/install.py
@@ -146,7 +146,6 @@ def main(args):
     lib_dir = cfg['libdir']
     target_include_dir = cfg['installed_include_dir']
     pkgconfig_dir = 'pkgconfig'
-    cmake_dir = 'cmake'
 
     prefix = cfg['prefix']
 
@@ -163,7 +162,7 @@ def main(args):
         copy_file(full_header_path,
                   prepend_destdir(os.path.join(target_include_dir, header)))
 
-    if build_static_lib:
+    if build_static_lib or target_os == 'windows':
         static_lib = cfg['static_lib_name']
         copy_file(os.path.join(out_dir, static_lib),
                   prepend_destdir(os.path.join(lib_dir, os.path.basename(static_lib))))
@@ -172,11 +171,8 @@ def main(args):
         if target_os == "windows":
             libname = cfg['libname']
             soname_base = libname + '.dll'
-            implib = cfg['implib_name']
             copy_executable(os.path.join(out_dir, soname_base),
                             prepend_destdir(os.path.join(bin_dir, soname_base)))
-            copy_file(os.path.join(out_dir, implib),
-                      prepend_destdir(os.path.join(lib_dir, os.path.basename(implib))))
         elif target_os == "mingw":
             shared_lib_name = cfg['shared_lib_name']
             copy_executable(os.path.join(out_dir, shared_lib_name),
@@ -210,13 +206,6 @@ def main(args):
         copy_file(cfg['botan_pkgconfig'],
                   prepend_destdir(os.path.join(pkgconfig_dir, os.path.basename(cfg['botan_pkgconfig']))))
 
-    cmake_dir = os.path.join(prefix, lib_dir, cmake_dir, 'Botan-%s' % cfg["version"])
-    makedirs(prepend_destdir(cmake_dir))
-    copy_file('build/cmake/botan-config.cmake',
-                  prepend_destdir(os.path.join(cmake_dir, 'botan-config.cmake')))
-    copy_file('build/cmake/botan-config-version.cmake',
-                  prepend_destdir(os.path.join(cmake_dir, 'botan-config-version.cmake')))
-
     if 'ffi' in cfg['mod_list'] and cfg['build_shared_lib'] is True and cfg['install_python_module'] is True:
         for ver in cfg['python_version'].split(','):
             py_lib_path = os.path.join(lib_dir, 'python%s' % (ver), 'site-packages')


Test patch:
diff --git a/src/scripts/ci/cmake_tests/CMakeLists.txt b/src/scripts/ci/cmake_tests/CMakeLists.txt
deleted file mode 100644
index e1a546a26..000000000
--- a/src/scripts/ci/cmake_tests/CMakeLists.txt
+++ /dev/null
@@ -1,128 +0,0 @@
-cmake_minimum_required(VERSION 3.24)
-
-project(Botan_CMake_Tests CXX)
-
-set(CMAKE_CXX_STANDARD 20)
-set(CMAKE_CXX_STANDARD_REQUIRED On)
-set(CMAKE_CXX_EXTENSIONS Off)
-
-# test #1: find any Botan version and link dynamically
-find_package(Botan REQUIRED)
-if(NOT Botan_FOUND)
-  message(FATAL_ERROR "Test #1 failed: Failed to find Botan with any version")
-endif()
-
-if(TARGET Botan::Botan)
-  add_executable(botan_version_test main.cpp)
-  target_link_libraries(botan_version_test Botan::Botan)
-
-  if(WIN32)
-    add_custom_command(TARGET botan_version_test POST_BUILD
-                       COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_RUNTIME_DLLS:botan_version_test> $<TARGET_FILE_DIR:botan_version_test>
-                       COMMAND_EXPAND_LISTS)
-  endif()
-
-  add_test(NAME "Using shared library"
-           COMMAND botan_version_test
-           PASS_REGULAR_EXPRESSION "${Botan_VERSION}")
-endif()
-
-unset(Botan_FOUND)
-
-# test #2: link statically
-find_package(Botan REQUIRED)
-if(TARGET Botan::Botan-static)
-  add_executable(botan_version_test_static main.cpp)
-  target_link_libraries(botan_version_test_static Botan::Botan-static)
-
-  add_test(NAME "Using static library"
-           COMMAND botan_version_test_static
-           PASS_REGULAR_EXPRESSION "${Botan_VERSION}")
-endif()
-
-unset(Botan_FOUND)
-
-# test #3: find Botan version installed by version number
-find_package(Botan ${Botan_VERSION} REQUIRED)
-if(NOT Botan_FOUND)
-  message(FATAL_ERROR "Failed to find Botan with version ${Botan_VERSION}")
-endif()
-
-unset(Botan_FOUND)
-
-# test #4: find Botan with specific modules
-find_package(Botan ${Botan_VERSION} COMPONENTS ecdsa rsa REQUIRED)
-if(NOT Botan_ecdsa_FOUND OR NOT Botan_rsa_FOUND)
-  message(FATAL_ERROR "Package did not specify that the (optional) components were found: ecdsa, rsa")
-endif()
-
-if(NOT Botan_FOUND)
-  message(FATAL_ERROR "Failed to find Botan with version ${Botan_VERSION} containing modules: ecdsa, rsa")
-endif()
-
-unset(Botan_FOUND)
-
-# test #5: fail if required module is not included
-find_package(Botan ${Botan_VERSION} COMPONENTS tpm QUIET)
-if(Botan_FOUND)
-  message(FATAL_ERROR "Found Botan with version ${Botan_VERSION}: Found module tpm but expected it to be absent")
-endif()
-
-unset(Botan_FOUND)
-
-# test #6: do not fail if optional module is not included
-find_package(Botan ${Botan_VERSION} OPTIONAL_COMPONENTS tpm REQUIRED)
-if(NOT Botan_FOUND)
-  message(FATAL_ERROR "Failed to find Botan with version ${Botan_VERSION}, despite missing optional modules: tpm")
-endif()
-if(NOT DEFINED Botan_tpm_FOUND OR ${Botan_tpm_FOUND})
-  message(FATAL_ERROR "Failed to notify that the optional component was not found")
-endif()
-
-unset(Botan_FOUND)
-
-# test #7: fail with some specific (but unavailable) component and fall-back to generic
-find_package(Botan ${Botan_VERSION} COMPONENTS nonexistent unobtainium QUIET)
-if(NOT Botan_FOUND)
-  # try again without requiring specific components
-  find_package(Botan ${Botan_VERSION})
-else()
-  message(FATAL_ERROR "Found Botan regardless of the non-existent component requirement")
-endif()
-if(NOT Botan_FOUND)
-  message(FATAL_ERROR "Failed to find generic Botan as a second attempt")
-endif()
-
-unset(Botan_FOUND)
-
-# test #8: try to find a future version
-math(EXPR Botan_FUTURE_MAJOR_VERSION "${Botan_VERSION_MAJOR} + 1")
-find_package(Botan ${Botan_FUTURE_MAJOR_VERSION} QUIET)
-if(Botan_FOUND)
-  message(FATAL_ERROR "Found a future major version that doesn't exist yet: ${Botan_FUTURE_MAJOR_VERSION}")
-endif()
-
-unset(Botan_FOUND)
-
-# test #9: try to find an older version (that is not compatible with the installed one)
-math(EXPR Botan_PAST_MAJOR_VERSION "${Botan_VERSION_MAJOR} - 1")
-find_package(Botan ${Botan_PAST_MAJOR_VERSION} QUIET)
-if(Botan_FOUND)
-  message(FATAL_ERROR "Found a legacy major version that doesn't exist yet: ${Botan_PAST_MAJOR_VERSION}")
-endif()
-
-unset(Botan_FOUND)
-
-# test #10: try to find Botan with a different case-sensitive spelling
-find_package(botan COMPONENTS rsa REQUIRED)
-if(NOT botan_FOUND)
-  message(FATAL_ERROR "Failed to find Botan with a different case-spelling")
-endif()
-if(NOT botan_rsa_FOUND)
-  message(FATAL_ERROR "Failed to find Botan component with a different case-spelling")
-endif()
-
-unset(Botan_FOUND)
-unset(botan_FOUND)
-
-enable_testing()
diff --git a/src/scripts/ci/cmake_tests/CMakePresets.json b/src/scripts/ci/cmake_tests/CMakePresets.json
deleted file mode 100644
index 68b0ef539..000000000
--- a/src/scripts/ci/cmake_tests/CMakePresets.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-    "version": 5,
-    "cmakeMinimumRequired": {
-        "major": 3,
-        "minor": 24,
-        "patch": 0
-    },
-    "configurePresets": [
-        {
-            "name": "windows_x86_64",
-            "architecture": "x64"
-        },
-        {
-            "name": "windows_x86",
-            "architecture": "Win32"
-        },
-        {
-            "name": "unix"
-        }
-    ]
-}
diff --git a/src/scripts/ci/cmake_tests/main.cpp b/src/scripts/ci/cmake_tests/main.cpp
deleted file mode 100644
index 632dc92d1..000000000
--- a/src/scripts/ci/cmake_tests/main.cpp
+++ /dev/null
@@ -1,7 +0,0 @@
-#include <botan/version.h>
-#include <iostream>
-
-int main() {
-   std::cout << Botan::short_version_string() << std::endl;
-   return 0;
-}
diff --git a/src/tests/test_tls_stream_integration.cpp b/src/tests/test_tls_stream_integration.cpp
index 4fe11269c..7037dd2de 100644
--- a/src/tests/test_tls_stream_integration.cpp
+++ b/src/tests/test_tls_stream_integration.cpp
@@ -22,6 +22,7 @@
 
       #include <botan/asio_stream.h>
       #include <botan/auto_rng.h>
+      #include <botan/tls_messages.h>
       #include <botan/tls_session_manager_noop.h>
 
       #include <boost/asio.hpp>
@@ -57,24 +58,43 @@ class Timeout_Exception : public std::runtime_error {
       using std::runtime_error::runtime_error;
 };
 
-class Peer {
+class PeerCallbacks : public Botan::TLS::StreamCallbacks {
    public:
-      Peer(const std::shared_ptr<const Botan::TLS::Policy>& policy, net::io_context& ioc) :
+      void fail_on_handshake_message(const Botan::TLS::Handshake_Type msg_type, Botan::TLS::AlertType alert) {
+         m_handshake_alerts[msg_type] = alert;
+      }
+
+      void tls_inspect_handshake_msg(const Botan::TLS::Handshake_Message& msg) final {
+         if(m_handshake_alerts.contains(msg.type())) {
+            throw Botan::TLS::TLS_Exception(m_handshake_alerts[msg.type()], "Test was configured to throw");
+         }
+      }
+
+   private:
+      std::map<Botan::TLS::Handshake_Type, Botan::TLS::AlertType> m_handshake_alerts;
+};
+
+class Peer {
+   private:
+      Peer(const std::shared_ptr<const Botan::TLS::Policy>& policy,
+           net::io_context& ioc,
+           std::shared_ptr<Basic_Credentials_Manager> credentials_manager) :
             m_rng(std::make_shared<Botan::AutoSeeded_RNG>()),
-            m_credentials_manager(std::make_shared<Basic_Credentials_Manager>(true, "")),
+            m_credentials_manager(std::move(credentials_manager)),
             m_session_mgr(std::make_shared<Botan::TLS::Session_Manager_Noop>()),
+            m_callbacks(std::make_shared<PeerCallbacks>()),
             m_ctx(std::make_shared<Botan::TLS::Context>(m_credentials_manager, m_rng, m_session_mgr, policy)),
             m_timeout_timer(ioc) {}
 
+   public:
+      Peer(const std::shared_ptr<const Botan::TLS::Policy>& policy, net::io_context& ioc) :
+            Peer(policy, ioc, std::make_shared<Basic_Credentials_Manager>(true, "")) {}
+
       Peer(const std::shared_ptr<const Botan::TLS::Policy>& policy,
            net::io_context& ioc,
            const std::string& server_cert,
            const std::string& server_key) :
-            m_rng(std::make_shared<Botan::AutoSeeded_RNG>()),
-            m_credentials_manager(std::make_shared<Basic_Credentials_Manager>(server_cert, server_key)),
-            m_session_mgr(std::make_shared<Botan::TLS::Session_Manager_Noop>()),
-            m_ctx(std::make_shared<Botan::TLS::Context>(m_credentials_manager, m_rng, m_session_mgr, policy)),
-            m_timeout_timer(ioc) {}
+            Peer(policy, ioc, std::make_shared<Basic_Credentials_Manager>(server_cert, server_key)) {}
 
       Peer(const Peer& other) = delete;
       Peer(Peer&& other) = delete;
@@ -138,10 +158,14 @@ class Peer {
 
       std::shared_ptr<Botan::TLS::Context> ctx() { return m_ctx; }
 
+   protected:
+      std::shared_ptr<PeerCallbacks> callbacks() { return m_callbacks; }
+
    private:
       std::shared_ptr<Botan::AutoSeeded_RNG> m_rng;
       std::shared_ptr<Basic_Credentials_Manager> m_credentials_manager;
       std::shared_ptr<Botan::TLS::Session_Manager_Noop> m_session_mgr;
+      std::shared_ptr<PeerCallbacks> m_callbacks;
       std::shared_ptr<Botan::TLS::Context> m_ctx;
       std::unique_ptr<ssl_stream> m_stream;
       net::system_timer m_timeout_timer;
@@ -163,7 +187,7 @@ class Result_Wrapper {
 
       void expect_ec(const std::string& msg, const error_code& expected, const error_code& ec) {
          if(ec != expected) {
-            m_result.test_failure(msg, "Unexpected error code: " + ec.message());
+            m_result.test_failure(msg, "Unexpected error code: " + ec.message() + " expected: " + expected.message());
          } else {
             m_result.test_success(msg);
          }
@@ -219,6 +243,11 @@ class Server : public Peer,
 
       void move_before_accept() { m_move_before_accept = true; }
 
+      void fail_on_handshake_message(const Botan::TLS::Handshake_Type msg_type, Botan::TLS::AlertType alert) {
+         callbacks()->fail_on_handshake_message(msg_type, alert);
+         m_expected_handshake_failure = alert;
+      }
+
       Result_Wrapper result() { return m_result; }
 
    private:
@@ -231,10 +260,10 @@ class Server : public Peer,
 
          if(m_move_before_accept) {
             // regression test for #2635
-            ssl_stream s(std::move(socket), ctx());
+            ssl_stream s(std::move(socket), ctx(), callbacks());
             create_stream(std::make_unique<ssl_stream>(std::move(s)));
          } else {
-            create_stream(std::make_unique<ssl_stream>(std::move(socket), ctx()));
+            create_stream(std::make_unique<ssl_stream>(std::move(socket), ctx(), callbacks()));
          }
 
          reset_timeout("handshake");
@@ -250,8 +279,13 @@ class Server : public Peer,
       }
 
       void handle_handshake(const error_code& ec) {
-         m_result.expect_success("handshake", ec);
-         handle_write(error_code{});
+         if(!m_expected_handshake_failure) {
+            m_result.expect_success("handshake", ec);
+            handle_write(error_code{});
+         } else {
+            m_result.expect_ec("handshake", m_expected_handshake_failure.value(), ec);
+            cancel_timeout();
+         }
       }
 
       void handle_write(const error_code& ec) {
@@ -307,6 +341,7 @@ class Server : public Peer,
    private:
       tcp::acceptor m_acceptor;
       Result_Wrapper m_result;
+      std::optional<Botan::TLS::AlertType> m_expected_handshake_failure;
       bool m_short_read_expected;
 
       // regression test for #2635
@@ -324,7 +359,7 @@ class Client : public Peer {
    public:
       Client(const std::shared_ptr<const Botan::TLS::Policy>& policy, net::io_context& ioc) : Peer(policy, ioc) {
          ctx()->set_verify_callback(accept_all);
-         create_stream(std::make_unique<ssl_stream>(ioc, ctx()));
+         create_stream(std::make_unique<ssl_stream>(ioc, ctx(), callbacks()));
       }
 
       void close_socket() {
@@ -751,6 +786,68 @@ class Test_Conversation_With_Move : public Test_Conversation {
       }
 };
 
+/* In this test we provoke a handshake failure early on in the server and expect
+ * the stream to handle it by providing the configured error code to the client.
+ *
+ * This is a regression test for #3778: Alerts during handshakes resulted in an
+ * immediate closure of the socket without transmitting the Alert message first.
+ */
+class Test_Handshake_Failure : public TestBase,
+                               public net::coroutine,
+                               public std::enable_shared_from_this<Test_Handshake_Failure> {
+   public:
+      Test_Handshake_Failure(net::io_context& ioc,
+                             const std::string& config_name,
+                             const std::shared_ptr<const Botan::TLS::Policy>& client_policy,
+                             const std::shared_ptr<const Botan::TLS::Policy>& server_policy) :
+            TestBase(ioc, client_policy, server_policy, "Test Handshake Failure", config_name) {
+         server()->fail_on_handshake_message(Botan::TLS::Handshake_Type::ClientHello,
+                                             Botan::TLS::Alert::HandshakeFailure);
+      }
+
+      void run(const error_code& ec) {
+         static auto test_case = &Test_Handshake_Failure::run;
+         reenter(*this) {
+            client()->reset_timeout("connect");
+            yield net::async_connect(
+               client()->stream().lowest_layer(), k_endpoints, std::bind(test_case, shared_from_this(), _1));
+            result().expect_success("connect", ec);
+
+            client()->reset_timeout("handshake");
+            yield client()->stream().async_handshake(Botan::TLS::Connection_Side::Client,
+                                                     std::bind(test_case, shared_from_this(), _1));
+            result().expect_ec("handshake", Botan::TLS::Alert::HandshakeFailure, ec);
+
+            client()->close_socket();
+            teardown();
+         }
+      }
+};
+
+class Test_Handshake_Failure_Sync : public Synchronous_Test {
+   public:
+      Test_Handshake_Failure_Sync(net::io_context& ioc,
+                                  const std::string& config_name,
+                                  const std::shared_ptr<const Botan::TLS::Policy>& client_policy,
+                                  const std::shared_ptr<const Botan::TLS::Policy>& server_policy) :
+            Synchronous_Test(ioc, client_policy, server_policy, "Test Handshake Failure Sync", config_name) {
+         server()->fail_on_handshake_message(Botan::TLS::Handshake_Type::ClientHello,
+                                             Botan::TLS::Alert::HandshakeFailure);
+      }
+
+      void run_synchronous_client() override {
+         error_code ec;
+         net::connect(client()->stream().lowest_layer(), k_endpoints, ec);
+         result().expect_success("connect", ec);
+
+         client()->stream().handshake(Botan::TLS::Connection_Side::Client, ec);
+         result().expect_ec("handshake", Botan::TLS ::Alert::HandshakeFailure, ec);
+
+         client()->close_socket();
+         teardown();
+      }
+};
+
 class SystemConfiguration {
    public:
       SystemConfiguration(std::string n, const std::string& cp, const std::string& sp) :
@@ -820,10 +917,12 @@ class Tls_Stream_Integration_Tests final : public Test {
             config.run<Test_Eager_Close>(results);
             config.run<Test_Close_Without_Shutdown>(results);
             config.run<Test_No_Shutdown_Response>(results);
+            config.run<Test_Handshake_Failure>(results);
             config.run<Test_Conversation_Sync>(results);
             config.run<Test_Eager_Close_Sync>(results);
             config.run<Test_Close_Without_Shutdown_Sync>(results);
             config.run<Test_No_Shutdown_Response_Sync>(results);
+            config.run<Test_Handshake_Failure_Sync>(results);
             config.run<Test_Conversation_With_Move>(results);
          }