Repository: randombit/botan
Pull request #3693 (2023-09-12 09:36:02)

Refactor: AlignmentBuffer<> helper for block-oriented Hashes
### Introduction and Motivation

[As promised](https://github.com/randombit/botan/pull/3681#issuecomment-1705313378) I am looking into a way to simplify the alignment code in block-oriented algorithms. To my mind, there is quite a bit of code duplication in many implementations. Also, its fairly critical code that handles raw memory, offsets and internal state that could easily go wrong.

In fact, I even found an issue in the existing code, while working on this. Below is [the `::add_data()` implementation for `MDx_HashFunction` in Botan 3.1.1](https://github.com/randombit/botan/blob/f60608b8818c7bb8579fe797122ed6116f4134af/src/lib/hash/mdx_hash/mdx_hash.cpp#L49-L75). Latest master is slightly different but contains the same issue.

Let's assume this is called twice on a pristine instance of SHA-512 (block size 128 bytes):

1. `add_data({...}, 1)`
2. `add_data({...}, 126)`

The first call fills `m_buffer` with one byte and sets `m_position = 1`. Both is happening at `buffer_insert` at the very end of the function (because `m_position` was initially 0).
In the second call we append an additional 126 bytes to `m_buffer` (in the first `buffer_insert()`, as `m_position` is initially 1). The block still isn't full, so no `compress_n()` is invoked.
**The issue:** We again set `m_position = 127` only at the very end after the second `buffer_insert` (that copies the 126 bytes once more into the same buffer location). The offset calculations check out, so there's no functional problem. But frankly, to me this doesn't look like intentional control flow.

Side note: one needs to know that `buffer_insert()` "fills `m_buffer` with as many bytes from `input` as possible, but at most `m_buffer.size() - m_position`".

```C++
void MDx_HashFunction::add_data(const uint8_t input[], size_t length) {
   const size_t block_len = static_cast<size_t>(1) << m_block_bits;

   m_count += length;

   if(m_position) {
      buffer_insert(m_buffer, m_position, input, length);

      if(m_position + length >= block_len) {
         compress_n(m_buffer.data(), 1);
         input += (block_len - m_position);
         length -= (block_len - m_position);
         m_position = 0;
      }
   }

   // Just in case the compiler can't figure out block_len is a power of 2
   const size_t full_blocks = length >> m_block_bits;
   const size_t remaining = length & (block_len - 1);

   if(full_blocks > 0) {
      compress_n(input, full_blocks);
   }

   buffer_insert(m_buffer, m_position, input + full_blocks * block_len, remaining);
   m_position += remaining;
}
```

### Description of Proposed Improvement

This is an attempt on encapsulating the intermittent buffering into an opinionated helper class with a (hopefully) clear and minimal interface. @randombit I'd be grateful for feedback early-on. I'll describe the details of the helper class' API in Doxygen comments.

With the helper, `MDx_HashFunction::add_data()` would roughly look like that and not pose the issue described above:

```C++
void MDx_HashFunction::add_data(std::span<const uint8_t> input) {
   BufferSlicer in(input);

   while(!in.empty()) {
      if(const auto one_block = m_buffer.handle_unaligned_data(in)) {
         compress_n(one_block->data(), 1);
      }

      if(m_buffer.in_alignment()) {
         const auto [aligned_data, full_blocks] = m_buffer.aligned_data_to_process(in);
         if(full_blocks > 0) {
            compress_n(aligned_data.data(), full_blocks);
         }
      }
   }

   m_count += input.size();
}
```

A quick trail benchmark run (average of 4 runs `./botan speed SHA-1`) :

| Branch | MiB/s |
| ------ | ----- |
| this1  | 1830  |
| master | 1829  |

coveralls: 
[![Coverage Status](https://coveralls.io/builds/62593262/badge)](https://coveralls.io/builds/62593262)

coverage: 91.705% (-0.002%) from 91.707% when pulling **cd9f6b611193acbe84c920cea23cd246ea991037 on Rohde-Schwarz:refactor/alignment_buffer** into **e097bd9c48d9172d61448f889a25c1bec2953a52 on randombit:master**.

randombit: Haven't reviewed this carefully yet but the broad idea looks ok to me
reneme: > Haven't reviewed this carefully yet but the broad idea looks ok to me

That'll do for now, thanks. I'll push this forward tomorrow.
reneme: This converged nicely for all hash implementations. The `HashFunction::add_data()` implementations are now basically all equivalent and unifying that into a single base implementation (using an abstract `compress()` method) should be easy to do.

I'll add more unit tests and documentation for `AlignmentBuffer<>` after lunch and will remove the "Draft" status afterwards.

### Future Work

Applying this to the hash implementations is probably enough complexity for this pull request. Other places (e.g. block cipher modes) might benefit from this refactoring as well, though. 

The **Keccak permutation** state may or may not benefit from `AlignmentBuffer<>` as well. Though, with the Keccak state not being a simple byte buffer, there's more work to be done.

@randombit Are there more places?
reneme: Thanks for the swift review! Re:`BOTAN_STATE_CHECK()`: [I changed the asserts (and also used `BOTAN_ARG_CHECK` where applicable)](https://github.com/randombit/botan/pull/3693/commits/5a3ff509f2619c6f7bf1f0f7def9cde0e9c1eff4). But, is that really correct? This class is internal and tightly coupled with the buffered computation use-case. If any of those ever throw, it is because we made a programming mistake in the consumer implementations. No particularly hard feelings about this, though, just double-checking.
randombit: Hmm you have a point there. In a nominal sense `Invalid_State` is the correct throw ("A request was made on an object while the object was in a state where the operation cannot be performed.") but you're right that since this is an internal-only API it throwing implies a bug elsewhere within the library, rather than some usage error by the application.
reneme: > since this is an internal-only API it throwing implies a bug elsewhere within the library, rather than some usage error by the application.

I read that as "let's revert 5a3ff50", right?

This commit contains a few additional tests for the state checks. I'm strongly assuming that it is not sensible to try and check asserts in the unittests, though. Particularly after introducing `./configure.py --terminate-on-assert`. 😅
randombit: Yeah let's revert that, sorry for the confusion.

Patch:
diff --git a/src/lib/hash/blake2/blake2b.cpp b/src/lib/hash/blake2/blake2b.cpp
index ac26b6081..14673e6dc 100644
--- a/src/lib/hash/blake2/blake2b.cpp
+++ b/src/lib/hash/blake2/blake2b.cpp
@@ -14,33 +14,26 @@
 #include <botan/internal/loadstor.h>
 #include <botan/internal/rotate.h>
 #include <botan/internal/stl_util.h>
+
 #include <algorithm>
+#include <array>
 
 namespace Botan {
 
 namespace {
 
-enum blake2b_constant { BLAKE2B_BLOCKBYTES = 128, BLAKE2B_IVU64COUNT = 8 };
-
-const uint64_t blake2b_IV[BLAKE2B_IVU64COUNT] = {0x6a09e667f3bcc908,
-                                                 0xbb67ae8584caa73b,
-                                                 0x3c6ef372fe94f82b,
-                                                 0xa54ff53a5f1d36f1,
-                                                 0x510e527fade682d1,
-                                                 0x9b05688c2b3e6c1f,
-                                                 0x1f83d9abfb41bd6b,
-                                                 0x5be0cd19137e2179};
+constexpr std::array<uint64_t, 8> blake2b_IV{0x6a09e667f3bcc908,
+                                             0xbb67ae8584caa73b,
+                                             0x3c6ef372fe94f82b,
+                                             0xa54ff53a5f1d36f1,
+                                             0x510e527fade682d1,
+                                             0x9b05688c2b3e6c1f,
+                                             0x1f83d9abfb41bd6b,
+                                             0x5be0cd19137e2179};
 
 }  // namespace
 
-BLAKE2b::BLAKE2b(size_t output_bits) :
-      m_output_bits(output_bits),
-      m_buffer(BLAKE2B_BLOCKBYTES),
-      m_bufpos(0),
-      m_H(BLAKE2B_IVU64COUNT),
-      m_T(),
-      m_F(),
-      m_key_size(0) {
+BLAKE2b::BLAKE2b(size_t output_bits) : m_output_bits(output_bits), m_H(blake2b_IV.size()), m_T(), m_F(), m_key_size(0) {
    if(output_bits == 0 || output_bits > 512 || output_bits % 8 != 0) {
       throw Invalid_Argument("Bad output bits size for BLAKE2b");
    }
@@ -49,17 +42,14 @@ BLAKE2b::BLAKE2b(size_t output_bits) :
 }
 
 void BLAKE2b::state_init() {
-   copy_mem(m_H.data(), blake2b_IV, BLAKE2B_IVU64COUNT);
+   copy_mem(m_H.data(), blake2b_IV.data(), blake2b_IV.size());
    m_H[0] ^= (0x01010000 | (static_cast<uint8_t>(m_key_size) << 8) | static_cast<uint8_t>(output_length()));
    m_T[0] = m_T[1] = 0;
    m_F = 0;
 
-   if(m_key_size == 0) {
-      m_bufpos = 0;
-   } else {
-      BOTAN_ASSERT_NOMSG(m_padded_key_buffer.size() == m_buffer.size());
-      copy_mem(m_buffer.data(), m_padded_key_buffer.data(), m_padded_key_buffer.size());
-      m_bufpos = m_padded_key_buffer.size();
+   m_buffer.clear();
+   if(m_key_size > 0) {
+      m_buffer.append(m_padded_key_buffer);
    }
 }
 
@@ -149,45 +139,28 @@ void BLAKE2b::compress(const uint8_t* input, size_t blocks, uint64_t increment)
 }
 
 void BLAKE2b::add_data(std::span<const uint8_t> input) {
-   if(input.empty()) {
-      return;
-   }
-
    BufferSlicer in(input);
 
-   if(m_bufpos > 0) {
-      if(m_bufpos < BLAKE2B_BLOCKBYTES) {
-         const auto part = in.take(std::min(BLAKE2B_BLOCKBYTES - m_bufpos, in.remaining()));
-         copy_mem(&m_buffer[m_bufpos], part.data(), part.size());
-         m_bufpos += part.size();
+   while(!in.empty()) {
+      if(const auto one_block = m_buffer.handle_unaligned_data(in)) {
+         compress(one_block->data(), 1, BLAKE2B_BLOCKBYTES);
       }
 
-      if(m_bufpos == m_buffer.size() && !in.empty()) {
-         compress(m_buffer.data(), 1, BLAKE2B_BLOCKBYTES);
-         m_bufpos = 0;
+      if(m_buffer.in_alignment()) {
+         const auto [aligned_data, full_blocks] = m_buffer.aligned_data_to_process(in);
+         if(full_blocks > 0) {
+            compress(aligned_data.data(), full_blocks, BLAKE2B_BLOCKBYTES);
+         }
       }
    }
-
-   if(in.remaining() > BLAKE2B_BLOCKBYTES) {
-      const size_t full_blocks = ((in.remaining() - 1) / BLAKE2B_BLOCKBYTES);
-      compress(in.take(BLAKE2B_BLOCKBYTES * full_blocks).data(), full_blocks, BLAKE2B_BLOCKBYTES);
-   }
-
-   if(!in.empty()) {
-      const auto take = in.remaining();
-      copy_mem(&m_buffer[m_bufpos], in.take(take).data(), take);
-      m_bufpos += take;
-   }
-
-   BOTAN_ASSERT_NOMSG(in.empty());
 }
 
 void BLAKE2b::final_result(std::span<uint8_t> output) {
-   if(m_bufpos != BLAKE2B_BLOCKBYTES) {
-      clear_mem(&m_buffer[m_bufpos], BLAKE2B_BLOCKBYTES - m_bufpos);
-   }
+   const auto pos = m_buffer.elements_in_buffer();
+   m_buffer.fill_up_with_zeros();
+
    m_F = 0xFFFFFFFFFFFFFFFF;
-   compress(m_buffer.data(), 1, m_bufpos);
+   compress(m_buffer.consume().data(), 1, pos);
    copy_out_vec_le(output.data(), output_length(), m_H);
    state_init();
 }
@@ -229,9 +202,8 @@ void BLAKE2b::key_schedule(std::span<const uint8_t> key) {
 
 void BLAKE2b::clear() {
    zeroise(m_H);
-   zeroise(m_buffer);
+   m_buffer.clear();
    zeroise(m_padded_key_buffer);
-   m_bufpos = 0;
    m_key_size = 0;
    state_init();
 }
diff --git a/src/lib/hash/blake2/blake2b.h b/src/lib/hash/blake2/blake2b.h
index 1ce4596a7..8e12cc533 100644
--- a/src/lib/hash/blake2/blake2b.h
+++ b/src/lib/hash/blake2/blake2b.h
@@ -10,6 +10,7 @@
 
 #include <botan/hash.h>
 #include <botan/sym_algo.h>
+#include <botan/internal/alignment_buffer.h>
 #include <memory>
 #include <string>
 
@@ -17,6 +18,8 @@ namespace Botan {
 
 class BLAKE2bMAC;
 
+constexpr size_t BLAKE2B_BLOCKBYTES = 128;
+
 /**
 * BLAKE2B
 */
@@ -57,8 +60,7 @@ class BLAKE2b final : public HashFunction,
 
       const size_t m_output_bits;
 
-      secure_vector<uint8_t> m_buffer;
-      size_t m_bufpos;
+      AlignmentBuffer<uint8_t, BLAKE2B_BLOCKBYTES, AlignmentBufferFinalBlock::must_be_deferred> m_buffer;
 
       secure_vector<uint64_t> m_H;
       uint64_t m_T[2];
diff --git a/src/lib/hash/gost_3411/gost_3411.cpp b/src/lib/hash/gost_3411/gost_3411.cpp
index a9037aee6..083cfabf9 100644
--- a/src/lib/hash/gost_3411/gost_3411.cpp
+++ b/src/lib/hash/gost_3411/gost_3411.cpp
@@ -15,9 +15,8 @@ namespace Botan {
 /**
 * GOST 34.11 Constructor
 */
-GOST_34_11::GOST_34_11() : m_cipher(GOST_28147_89_Params("R3411_CryptoPro")), m_buffer(32), m_sum(32), m_hash(32) {
+GOST_34_11::GOST_34_11() : m_cipher(GOST_28147_89_Params("R3411_CryptoPro")), m_sum(32), m_hash(32) {
    m_count = 0;
-   m_position = 0;
 }
 
 void GOST_34_11::clear() {
@@ -25,7 +24,7 @@ void GOST_34_11::clear() {
    zeroise(m_sum);
    zeroise(m_hash);
    m_count = 0;
-   m_position = 0;
+   m_buffer.clear();
 }
 
 std::unique_ptr<HashFunction> GOST_34_11::copy_state() const {
@@ -36,28 +35,22 @@ std::unique_ptr<HashFunction> GOST_34_11::copy_state() const {
 * Hash additional inputs
 */
 void GOST_34_11::add_data(std::span<const uint8_t> input) {
-   m_count += input.size();
-
-   if(m_position) {
-      buffer_insert(m_buffer, m_position, input.data(), input.size());
+   BufferSlicer in(input);
 
-      if(m_position + input.size() >= hash_block_size()) {
-         compress_n(m_buffer.data(), 1);
-         input = input.last(input.size() - hash_block_size() + m_position);
-         m_position = 0;
+   while(!in.empty()) {
+      if(const auto one_block = m_buffer.handle_unaligned_data(in)) {
+         compress_n(one_block->data(), 1);
       }
-   }
 
-   BufferSlicer in(input);
-   const size_t full_blocks = in.remaining() / hash_block_size();
-
-   if(full_blocks) {
-      compress_n(in.take(full_blocks * hash_block_size()).data(), full_blocks);
+      if(m_buffer.in_alignment()) {
+         const auto [aligned_data, full_blocks] = m_buffer.aligned_data_to_process(in);
+         if(full_blocks > 0) {
+            compress_n(aligned_data.data(), full_blocks);
+         }
+      }
    }
 
-   const auto remaining = in.take(in.remaining());
-   buffer_insert(m_buffer, m_position, remaining.data(), remaining.size());
-   m_position += remaining.size();
+   m_count += input.size();
 }
 
 /**
@@ -208,9 +201,9 @@ void GOST_34_11::compress_n(const uint8_t input[], size_t blocks) {
 * Produce the final GOST 34.11 output
 */
 void GOST_34_11::final_result(std::span<uint8_t> out) {
-   if(m_position) {
-      clear_mem(m_buffer.data() + m_position, m_buffer.size() - m_position);
-      compress_n(m_buffer.data(), 1);
+   if(!m_buffer.in_alignment()) {
+      m_buffer.fill_up_with_zeros();
+      compress_n(m_buffer.consume().data(), 1);
    }
 
    secure_vector<uint8_t> length_buf(32);
diff --git a/src/lib/hash/gost_3411/gost_3411.h b/src/lib/hash/gost_3411/gost_3411.h
index 35f7af301..d58193ed6 100644
--- a/src/lib/hash/gost_3411/gost_3411.h
+++ b/src/lib/hash/gost_3411/gost_3411.h
@@ -9,6 +9,7 @@
 #define BOTAN_GOST_3411_H_
 
 #include <botan/hash.h>
+#include <botan/internal/alignment_buffer.h>
 #include <botan/internal/gost_28147.h>
 
 namespace Botan {
@@ -39,8 +40,8 @@ class GOST_34_11 final : public HashFunction {
       void final_result(std::span<uint8_t>) override;
 
       GOST_28147_89 m_cipher;
-      secure_vector<uint8_t> m_buffer, m_sum, m_hash;
-      size_t m_position;
+      AlignmentBuffer<uint8_t, 32> m_buffer;
+      secure_vector<uint8_t> m_sum, m_hash;
       uint64_t m_count;
 };
 
diff --git a/src/lib/hash/mdx_hash/mdx_hash.cpp b/src/lib/hash/mdx_hash/mdx_hash.cpp
index 3692b4c55..213ccffc3 100644
--- a/src/lib/hash/mdx_hash/mdx_hash.cpp
+++ b/src/lib/hash/mdx_hash/mdx_hash.cpp
@@ -50,6 +50,9 @@ void MDx_HashFunction::clear() {
 void MDx_HashFunction::add_data(std::span<const uint8_t> input) {
    const size_t block_len = static_cast<size_t>(1) << m_block_bits;
 
+   // TODO: Use AlignmentBuffer<> once MDx_HashFunction is not a base class
+   //       anymore. See: https://github.com/randombit/botan/pull/3550
+
    m_count += input.size();
 
    if(m_position) {
diff --git a/src/lib/hash/skein/skein_512.cpp b/src/lib/hash/skein/skein_512.cpp
index c29f50a9d..568bfba0b 100644
--- a/src/lib/hash/skein/skein_512.cpp
+++ b/src/lib/hash/skein/skein_512.cpp
@@ -19,9 +19,7 @@ Skein_512::Skein_512(size_t arg_output_bits, std::string_view arg_personalizatio
       m_personalization(arg_personalization),
       m_output_bits(arg_output_bits),
       m_threefish(std::make_unique<Threefish_512>()),
-      m_T(2),
-      m_buffer(64),
-      m_buf_pos(0) {
+      m_T(2) {
    if(m_output_bits == 0 || m_output_bits % 8 != 0 || m_output_bits > 512) {
       throw Invalid_Argument("Bad output bits size for Skein-512");
    }
@@ -46,13 +44,11 @@ std::unique_ptr<HashFunction> Skein_512::copy_state() const {
    copy->m_threefish->m_K = this->m_threefish->m_K;
    copy->m_T = this->m_T;
    copy->m_buffer = this->m_buffer;
-   copy->m_buf_pos = this->m_buf_pos;
    return copy;
 }
 
 void Skein_512::clear() {
-   zeroise(m_buffer);
-   m_buf_pos = 0;
+   m_buffer.clear();
 
    initial_block();
 }
@@ -120,41 +116,28 @@ void Skein_512::ubi_512(const uint8_t msg[], size_t msg_len) {
 }
 
 void Skein_512::add_data(std::span<const uint8_t> input) {
-   if(input.empty()) {
-      return;
-   }
-
-   if(m_buf_pos) {
-      buffer_insert(m_buffer, m_buf_pos, input.data(), input.size());
-      if(m_buf_pos + input.size() > 64) {
-         ubi_512(m_buffer.data(), m_buffer.size());
+   BufferSlicer in(input);
 
-         input = input.last(input.size() - 64 + m_buf_pos);
-         m_buf_pos = 0;
+   while(!in.empty()) {
+      if(const auto one_block = m_buffer.handle_unaligned_data(in)) {
+         ubi_512(one_block->data(), one_block->size());
       }
-   }
-
-   const size_t full_blocks = (input.size() - 1) / 64;
-   BufferSlicer in(input);
 
-   if(full_blocks) {
-      const auto blocks = in.take(64 * full_blocks);
-      ubi_512(blocks.data(), blocks.size());
+      if(m_buffer.in_alignment()) {
+         const auto [aligned_data, full_blocks] = m_buffer.aligned_data_to_process(in);
+         if(full_blocks > 0) {
+            ubi_512(aligned_data.data(), aligned_data.size());
+         }
+      }
    }
-
-   const auto remaining = in.take(in.remaining());
-   buffer_insert(m_buffer, m_buf_pos, remaining.data(), remaining.size());
-   m_buf_pos += remaining.size();
 }
 
 void Skein_512::final_result(std::span<uint8_t> out) {
    m_T[1] |= (static_cast<uint64_t>(1) << 63);  // final block flag
 
-   for(size_t i = m_buf_pos; i != m_buffer.size(); ++i) {
-      m_buffer[i] = 0;
-   }
-
-   ubi_512(m_buffer.data(), m_buf_pos);
+   const auto pos = m_buffer.elements_in_buffer();
+   m_buffer.fill_up_with_zeros();
+   ubi_512(m_buffer.consume().data(), pos);
 
    const uint8_t counter[8] = {0};
 
@@ -163,7 +146,6 @@ void Skein_512::final_result(std::span<uint8_t> out) {
 
    copy_out_vec_le(out.data(), m_output_bits / 8, m_threefish->m_K);
 
-   m_buf_pos = 0;
    initial_block();
 }
 
diff --git a/src/lib/hash/skein/skein_512.h b/src/lib/hash/skein/skein_512.h
index faa1a2c08..fc41c98c3 100644
--- a/src/lib/hash/skein/skein_512.h
+++ b/src/lib/hash/skein/skein_512.h
@@ -9,7 +9,9 @@
 #define BOTAN_SKEIN_512_H_
 
 #include <botan/hash.h>
+#include <botan/internal/alignment_buffer.h>
 #include <botan/internal/threefish_512.h>
+
 #include <memory>
 #include <string>
 
@@ -61,8 +63,7 @@ class Skein_512 final : public HashFunction {
 
       std::unique_ptr<Threefish_512> m_threefish;
       secure_vector<uint64_t> m_T;
-      secure_vector<uint8_t> m_buffer;
-      size_t m_buf_pos;
+      AlignmentBuffer<uint8_t, 64, AlignmentBufferFinalBlock::must_be_deferred> m_buffer;
 };
 
 }  // namespace Botan
diff --git a/src/lib/hash/streebog/streebog.cpp b/src/lib/hash/streebog/streebog.cpp
index 59b26dd6f..2a89d672d 100644
--- a/src/lib/hash/streebog/streebog.cpp
+++ b/src/lib/hash/streebog/streebog.cpp
@@ -22,8 +22,7 @@ std::unique_ptr<HashFunction> Streebog::copy_state() const {
    return std::make_unique<Streebog>(*this);
 }
 
-Streebog::Streebog(size_t output_bits) :
-      m_output_bits(output_bits), m_count(0), m_position(0), m_buffer(64), m_h(8), m_S(8) {
+Streebog::Streebog(size_t output_bits) : m_output_bits(output_bits), m_count(0), m_h(8), m_S(8) {
    if(output_bits != 256 && output_bits != 512) {
       throw Invalid_Argument(fmt("Streebog: Invalid output length {}", output_bits));
    }
@@ -40,8 +39,7 @@ std::string Streebog::name() const {
 */
 void Streebog::clear() {
    m_count = 0;
-   m_position = 0;
-   zeroise(m_buffer);
+   m_buffer.clear();
    zeroise(m_S);
 
    const uint64_t fill = (m_output_bits == 512) ? 0 : 0x0101010101010101;
@@ -52,46 +50,39 @@ void Streebog::clear() {
 * Update the hash
 */
 void Streebog::add_data(std::span<const uint8_t> input) {
-   const size_t block_size = m_buffer.size();
-
-   if(m_position) {
-      buffer_insert(m_buffer, m_position, input.data(), input.size());
+   BufferSlicer in(input);
 
-      if(m_position + input.size() >= block_size) {
-         compress(m_buffer.data());
+   while(!in.empty()) {
+      if(const auto one_block = m_buffer.handle_unaligned_data(in)) {
+         compress(one_block->data());
          m_count += 512;
-         input = input.last(input.size() - block_size + m_position);
-         m_position = 0;
       }
-   }
 
-   BufferSlicer in(input);
-   while(in.remaining() >= block_size) {
-      compress(in.take(block_size).data());
-      m_count += 512;
+      if(m_buffer.in_alignment()) {
+         while(const auto aligned_block = m_buffer.next_aligned_block_to_process(in)) {
+            compress(aligned_block->data());
+            m_count += 512;
+         }
+      }
    }
-
-   const auto remaining = in.take(in.remaining());
-   buffer_insert(m_buffer, m_position, remaining.data(), remaining.size());
-   m_position += remaining.size();
 }
 
 /*
 * Finalize a hash
 */
 void Streebog::final_result(std::span<uint8_t> output) {
-   m_buffer[m_position++] = 0x01;
+   const auto pos = m_buffer.elements_in_buffer();
 
-   if(m_position != m_buffer.size()) {
-      clear_mem(&m_buffer[m_position], m_buffer.size() - m_position);
-   }
+   const uint8_t padding = 0x01;
+   m_buffer.append({&padding, 1});
+   m_buffer.fill_up_with_zeros();
 
-   compress(m_buffer.data());
-   m_count += (m_position - 1) * 8;
+   compress(m_buffer.consume().data());
+   m_count += pos * 8;
 
-   zeroise(m_buffer);
-   store_le(m_count, m_buffer.data());
-   compress(m_buffer.data(), true);
+   m_buffer.fill_up_with_zeros();
+   store_le(m_count, m_buffer.directly_modify_first(sizeof(m_count)).data());
+   compress(m_buffer.consume().data(), true);
 
    compress_64(m_S.data(), true);
    // FIXME
diff --git a/src/lib/hash/streebog/streebog.h b/src/lib/hash/streebog/streebog.h
index 420ade1d9..9420ba3eb 100644
--- a/src/lib/hash/streebog/streebog.h
+++ b/src/lib/hash/streebog/streebog.h
@@ -10,6 +10,8 @@
 
 #include <botan/hash.h>
 
+#include <botan/internal/alignment_buffer.h>
+
 namespace Botan {
 
 /**
@@ -42,8 +44,7 @@ class Streebog final : public HashFunction {
    private:
       const size_t m_output_bits;
       uint64_t m_count;
-      size_t m_position;
-      secure_vector<uint8_t> m_buffer;
+      AlignmentBuffer<uint8_t, 64> m_buffer;
       secure_vector<uint64_t> m_h;
       secure_vector<uint64_t> m_S;
 };
diff --git a/src/lib/utils/alignment_buffer.h b/src/lib/utils/alignment_buffer.h
new file mode 100644
index 000000000..8f097883d
--- /dev/null
+++ b/src/lib/utils/alignment_buffer.h
@@ -0,0 +1,231 @@
+/*
+ * Alignment buffer helper
+ * (C) 2023 Jack Lloyd
+ *     2023 René Meusel - Rohde & Schwarz Cybersecurity
+ *
+ * Botan is released under the Simplified BSD License (see license.txt)
+ */
+
+#ifndef BOTAN_ALIGNMENT_BUFFER_H_
+#define BOTAN_ALIGNMENT_BUFFER_H_
+
+#include <botan/concepts.h>
+#include <botan/internal/stl_util.h>
+
+#include <array>
+#include <optional>
+#include <span>
+
+namespace Botan {
+
+/**
+ * Defines the strategy for handling the final block of input data in the
+ * handle_unaligned_data() method of the AlignmentBuffer<>.
+ *
+ * - is_not_special:   the final block is treated like any other block
+ * - must_be_deferred: the final block is not emitted while bulk processing (typically add_data())
+ *                     but is deferred until manually consumed (typically final_result())
+ *
+ * The AlignmentBuffer<> assumes data to be "the final block" if no further
+ * input data is available in the BufferSlicer<>. This might result in some
+ * performance overhead when using the must_be_deferred strategy.
+ */
+enum class AlignmentBufferFinalBlock : size_t {
+   is_not_special = 0,
+   must_be_deferred = 1,
+};
+
+/**
+ * @brief Alignment buffer helper
+ *
+ * Many algorithms have an intrinsic block size in which they consume input
+ * data. When streaming arbitrary data chunks to such algorithms we must store
+ * some data intermittently to honor the algorithm's alignment requirements.
+ *
+ * This helper encapsulates such an alignment buffer. The API of this class is
+ * designed to minimize user errors in the algorithm implementations. Therefore,
+ * it is strongly opinionated on its use case. Don't try to use it for anything
+ * but the described circumstance.
+ *
+ * @tparam T                     the element type of the internal buffer
+ * @tparam BLOCK_SIZE            the buffer size to use for the alignment buffer
+ * @tparam FINAL_BLOCK_STRATEGY  defines whether the final input data block is
+ *                               retained in handle_unaligned_data() and must be
+ *                               manually consumed
+ */
+template <typename T,
+          size_t BLOCK_SIZE,
+          AlignmentBufferFinalBlock FINAL_BLOCK_STRATEGY = AlignmentBufferFinalBlock::is_not_special>
+   requires(BLOCK_SIZE > 0)
+class AlignmentBuffer {
+   public:
+      AlignmentBuffer() : m_position(0) {}
+
+      ~AlignmentBuffer() { secure_scrub_memory(m_buffer.data(), m_buffer.size()); }
+
+      AlignmentBuffer(const AlignmentBuffer& other) = default;
+      AlignmentBuffer(AlignmentBuffer&& other) noexcept = default;
+      AlignmentBuffer& operator=(const AlignmentBuffer& other) = default;
+      AlignmentBuffer& operator=(AlignmentBuffer&& other) noexcept = default;
+
+      void clear() {
+         clear_mem(m_buffer.data(), m_buffer.size());
+         m_position = 0;
+      }
+
+      /**
+       * Fills the currently unused bytes of the buffer with zero bytes
+       */
+      void fill_up_with_zeros() {
+         clear_mem(&m_buffer[m_position], elements_until_alignment());
+         m_position = m_buffer.size();
+      }
+
+      /**
+       * Appends the provided @p elements to the buffer. The user has to make
+       * sure that @p elements fits in the remaining capacity of the buffer.
+       */
+      void append(std::span<const T> elements) {
+         BOTAN_ASSERT_NOMSG(elements.size() <= elements_until_alignment());
+         std::copy(elements.begin(), elements.end(), m_buffer.begin() + m_position);
+         m_position += elements.size();
+      }
+
+      /**
+       * Allows direct modification of the first @p elements in the buffer.
+       * This is a low-level accessor that neither takes the buffer's current
+       * capacity into account nor does it change the internal cursor.
+       * Beware not to overwrite unconsumed bytes.
+       */
+      std::span<T> directly_modify_first(size_t elements) {
+         BOTAN_ASSERT_NOMSG(size() >= elements);
+         return std::span(m_buffer).first(elements);
+      }
+
+      /**
+       * Allows direct modification of the last @p elements in the buffer.
+       * This is a low-level accessor that neither takes the buffer's current
+       * capacity into account nor does it change the internal cursor.
+       * Beware not to overwrite unconsumed bytes.
+       */
+      std::span<T> directly_modify_last(size_t elements) {
+         BOTAN_ASSERT_NOMSG(size() >= elements);
+         return std::span(m_buffer).last(elements);
+      }
+
+      /**
+       * Once the buffer reached alignment, this can be used to consume as many
+       * input bytes from the given @p slider as possible. The output always
+       * contains data elements that are a multiple of the intrinsic block size.
+       *
+       * @returns a view onto the aligned data from @p slicer and the number of
+       *          full blocks that are represented by this view.
+       */
+      [[nodiscard]] std::tuple<std::span<const uint8_t>, size_t> aligned_data_to_process(BufferSlicer& slicer) const {
+         BOTAN_ASSERT_NOMSG(in_alignment());
+
+         // When the final block is to be deferred, the last block must not be
+         // selected for processing if there is no (unaligned) extra input data.
+         const size_t defer = (defers_final_block()) ? 1 : 0;
+         const size_t full_blocks_to_process = (slicer.remaining() - defer) / m_buffer.size();
+         return {slicer.take(full_blocks_to_process * m_buffer.size()), full_blocks_to_process};
+      }
+
+      /**
+       * Once the buffer reached alignment, this can be used to consume full
+       * blocks from the input data represented by @p slicer.
+       *
+       * @returns a view onto the next full block from @p slicer or std::nullopt
+       *          if not enough data is available in @p slicer.
+       */
+      [[nodiscard]] std::optional<std::span<const uint8_t>> next_aligned_block_to_process(BufferSlicer& slicer) const {
+         BOTAN_ASSERT_NOMSG(in_alignment());
+
+         // When the final block is to be deferred, the last block must not be
+         // selected for processing if there is no (unaligned) extra input data.
+         const size_t defer = (defers_final_block()) ? 1 : 0;
+         if(slicer.remaining() < m_buffer.size() + defer) {
+            return std::nullopt;
+         }
+
+         return slicer.take(m_buffer.size());
+      }
+
+      /**
+       * Intermittently buffers potentially unaligned data provided in @p
+       * slicer. If the internal buffer already contains some elements, data is
+       * appended. Once a full block is collected, it is returned to the caller
+       * for processing.
+       *
+       * @param slicer the input data source to be (partially) consumed
+       * @returns a view onto a full block once enough data was collected, or
+       *          std::nullopt if no full block is available yet
+       */
+      [[nodiscard]] std::optional<std::span<const T>> handle_unaligned_data(BufferSlicer& slicer) {
+         // When the final block is to be deferred, we would need to store and
+         // hold a buffer that contains exactly one block until more data is
+         // passed or it is explicitly consumed.
+         const size_t defer = (defers_final_block()) ? 1 : 0;
+
+         if(in_alignment() && slicer.remaining() >= m_buffer.size() + defer) {
+            // We are currently in alignment and the passed-in data source
+            // contains enough data to benefit from aligned processing.
+            // Therefore, we don't copy anything into the intermittent buffer.
+            return std::nullopt;
+         }
+
+         // Fill the buffer with as much input data as needed to reach alignment
+         // or until the input source is depleted.
+         const auto elements_to_consume = std::min(m_buffer.size() - m_position, slicer.remaining());
+         append(slicer.take(elements_to_consume));
+
+         // If we collected enough data, we push out one full block. When
+         // deferring the final block is enabled, we additionally check that
+         // more input data is available to continue processing a consecutive
+         // block.
+         if(ready_to_consume() && (!defers_final_block() || !slicer.empty())) {
+            return consume();
+         } else {
+            return std::nullopt;
+         }
+      }
+
+      /**
+       * Explicitly consume the currently collected block. It is the caller's
+       * responsibility to ensure that the buffer is filled fully. After
+       * consumption, the buffer is cleared and ready to collect new data.
+       */
+      [[nodiscard]] std::span<const T> consume() {
+         BOTAN_ASSERT_NOMSG(ready_to_consume());
+         m_position = 0;
+         return m_buffer;
+      }
+
+      constexpr size_t size() const { return m_buffer.size(); }
+
+      size_t elements_in_buffer() const { return m_position; }
+
+      size_t elements_until_alignment() const { return m_buffer.size() - m_position; }
+
+      /**
+       * @returns true if the buffer is empty (i.e. contains no unaligned data)
+       */
+      bool in_alignment() const { return m_position == 0; }
+
+      /**
+       * @returns true if the buffer is full (i.e. a block is ready to be consumed)
+       */
+      bool ready_to_consume() const { return m_position == m_buffer.size(); }
+
+      constexpr bool defers_final_block() const {
+         return FINAL_BLOCK_STRATEGY == AlignmentBufferFinalBlock::must_be_deferred;
+      }
+
+   private:
+      std::array<T, BLOCK_SIZE> m_buffer;
+      size_t m_position;
+};
+
+}  // namespace Botan
+
+#endif
diff --git a/src/lib/utils/info.txt b/src/lib/utils/info.txt
index 4537caab9..20b874b7d 100644
--- a/src/lib/utils/info.txt
+++ b/src/lib/utils/info.txt
@@ -24,6 +24,7 @@ version.h
 </header:public>
 
 <header:internal>
+alignment_buffer.h
 bit_ops.h
 bswap.h
 calendar.h


Test patch:
diff --git a/src/tests/test_utils_buffer.cpp b/src/tests/test_utils_buffer.cpp
index ebbda67c9..9c824ec9c 100644
--- a/src/tests/test_utils_buffer.cpp
+++ b/src/tests/test_utils_buffer.cpp
@@ -7,12 +7,20 @@
 
 #include "tests.h"
 
+#include <botan/internal/alignment_buffer.h>
 #include <botan/internal/stl_util.h>
 
+#include <array>
+
 namespace Botan_Tests {
 
 namespace {
 
+template <typename T>
+std::vector<uint8_t> v(const T& container) {
+   return {container.begin(), container.end()};
+}
+
 using StrongBuffer = Botan::Strong<std::vector<uint8_t>, struct StrongBuffer_>;
 
 std::vector<Test::Result> test_buffer_slicer() {
@@ -148,7 +156,342 @@ std::vector<Test::Result> test_buffer_stuffer() {
    };
 }
 
-BOTAN_REGISTER_TEST_FN("utils", "buffer_utilities", test_buffer_slicer, test_buffer_stuffer);
+std::vector<Test::Result> test_alignment_buffer() {
+   std::array<uint8_t, 32> data = {1,  2,  3,  4,  5,  6,  7,  8,  9,  10, 11, 12, 13, 14, 15, 16,
+                                   17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32};
+   std::array<uint8_t, 16> first_half_data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+   std::array<uint8_t, 16> second_half_data = {17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32};
+
+   return {
+      CHECK("Fresh Alignment Buffer",
+            [](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32> b;
+               result.test_eq("size()", b.size(), 32);
+               result.test_eq("elements_in_buffer()", b.elements_in_buffer(), 0);
+               result.test_eq("elements_until_alignment()", b.elements_until_alignment(), 32);
+               result.confirm("in_alignment()", b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+            }),
+
+      CHECK("Fill Alignment Buffer",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32> b;
+
+               b.append(first_half_data);
+
+               result.test_eq("size()", b.size(), 32);
+               result.test_eq("elements_in_buffer()", b.elements_in_buffer(), 16);
+               result.test_eq("elements_until_alignment()", b.elements_until_alignment(), 16);
+               result.confirm("!in_alignment()", !b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+
+               b.append(second_half_data);
+
+               result.test_eq("size()", b.size(), 32);
+               result.test_eq("elements_in_buffer()", b.elements_in_buffer(), 32);
+               result.test_eq("elements_until_alignment()", b.elements_until_alignment(), 0);
+               result.confirm("!in_alignment()", !b.in_alignment());
+               result.confirm("ready_to_consume()", b.ready_to_consume());
+            }),
+
+      CHECK("Consume Alignment Buffer",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32> b;
+
+               b.append(data);
+
+               result.require("ready_to_consume()", b.ready_to_consume());
+               const auto out = b.consume();
+
+               result.test_eq("size()", b.size(), 32);
+               result.test_eq("elements_in_buffer()", b.elements_in_buffer(), 0);
+               result.test_eq("elements_until_alignment()", b.elements_until_alignment(), 32);
+               result.confirm("in_alignment()", b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+
+               result.test_is_eq("in == out", v(data), v(out));
+            }),
+
+      CHECK("Clear Alignment Buffer",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32> b;
+
+               b.append(first_half_data);
+
+               result.require("elements_in_buffer()", b.elements_in_buffer() == 16);
+               b.clear();
+
+               result.test_eq("size()", b.size(), 32);
+               result.test_eq("elements_in_buffer()", b.elements_in_buffer(), 0);
+               result.test_eq("elements_until_alignment()", b.elements_until_alignment(), 32);
+               result.confirm("in_alignment()", b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+            }),
+
+      CHECK("Add Zero-Padding to Alignment Buffer",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32> b;
+
+               b.append(first_half_data);
+
+               result.require("elements_in_buffer()", b.elements_in_buffer() == 16);
+               b.fill_up_with_zeros();
+
+               result.test_eq("size()", b.size(), 32);
+               result.test_eq("elements_in_buffer()", b.elements_in_buffer(), 32);
+               result.test_eq("elements_until_alignment()", b.elements_until_alignment(), 0);
+               result.confirm("!in_alignment()", !b.in_alignment());
+               result.confirm("ready_to_consume()", b.ready_to_consume());
+
+               const auto out = b.consume();
+
+               result.test_is_eq("prefix", v(out.first(16)), v(first_half_data));
+               result.test_is_eq("zero-padding", v(out.last(16)), std::vector<uint8_t>(16, 0));
+            }),
+
+      CHECK("Handle unaligned data in Alignment Buffer (no block-defer)",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32> b;
+
+               Botan::BufferSlicer first_half(first_half_data);
+               Botan::BufferSlicer second_half(second_half_data);
+
+               const auto r1 = b.handle_unaligned_data(first_half);
+               result.confirm("half a block is not returned", !r1.has_value());
+               result.confirm("first input is consumed", first_half.empty());
+
+               result.test_eq("elements_in_buffer()", b.elements_in_buffer(), 16);
+               result.test_eq("elements_until_alignment()", b.elements_until_alignment(), 16);
+               result.confirm("!in_alignment()", !b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+
+               const auto r2 = b.handle_unaligned_data(second_half);
+               result.require("second half completes block", r2.has_value());
+               result.confirm("second input is consumed", second_half.empty());
+
+               result.test_eq("elements_in_buffer()", b.elements_in_buffer(), 0);
+               result.test_eq("elements_until_alignment()", b.elements_until_alignment(), 32);
+               result.confirm("in_alignment()", b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+
+               result.test_is_eq("collected block is correct", v(r2.value()), v(data));
+            }),
+
+      CHECK("Aligned data is not buffered unneccesarily (no block-defer)",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32> b;
+
+               Botan::BufferSlicer full_block_1(data);
+               const auto r1 = b.handle_unaligned_data(full_block_1);
+               result.confirm("aligned data is not buffered", !r1.has_value());
+               result.confirm("in_alignment()", b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+               result.test_eq("aligned data is not consumed", full_block_1.remaining(), 32);
+
+               Botan::BufferSlicer half_block(first_half_data);
+               Botan::BufferSlicer full_block_2(data);
+               const auto r2 = b.handle_unaligned_data(half_block);
+               result.confirm("unaligned data is buffered", !r2.has_value());
+               result.confirm("!in_alignment()", !b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+               result.confirm("unaligned data is consumed", half_block.empty());
+
+               const auto r3 = b.handle_unaligned_data(full_block_2);
+               result.confirm("collected block is consumed", r3.has_value());
+               result.confirm("in_alignment()", b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+               result.test_eq("input is consumed until alignment", full_block_2.remaining(), 16);
+            }),
+
+      CHECK("Handle unaligned data in Alignment Buffer (with block-defer)",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32, Botan::AlignmentBufferFinalBlock::must_be_deferred> b;
+
+               Botan::BufferSlicer first_half(first_half_data);
+               Botan::BufferSlicer second_half(second_half_data);
+               Botan::BufferSlicer third_half(first_half_data);
+
+               const auto r1 = b.handle_unaligned_data(first_half);
+               result.confirm("half a block is not returned", !r1.has_value());
+               result.confirm("first input is consumed", first_half.empty());
+
+               result.test_eq("elements_in_buffer()", b.elements_in_buffer(), 16);
+               result.test_eq("elements_until_alignment()", b.elements_until_alignment(), 16);
+               result.confirm("!in_alignment()", !b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+
+               const auto r2 = b.handle_unaligned_data(second_half);
+               result.require("second half completes block but is not returned", !r2.has_value());
+               result.confirm("second input is consumed", second_half.empty());
+
+               result.test_eq("elements_in_buffer()", b.elements_in_buffer(), 32);
+               result.test_eq("elements_until_alignment()", b.elements_until_alignment(), 0);
+               result.confirm("!in_alignment()", !b.in_alignment());
+               result.confirm("ready_to_consume()", b.ready_to_consume());
+
+               const auto r3 = b.handle_unaligned_data(third_half);
+               result.require("extra data pushes out block", r3.has_value());
+               result.test_eq("third input is not consumed", third_half.remaining(), 16);
+
+               result.test_eq("elements_in_buffer()", b.elements_in_buffer(), 0);
+               result.test_eq("elements_until_alignment()", b.elements_until_alignment(), 32);
+               result.confirm("in_alignment()", b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+
+               result.test_is_eq("collected block is correct", v(r3.value()), v(data));
+            }),
+
+      CHECK("Aligned data is not buffered unneccesarily (with block-defer)",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32, Botan::AlignmentBufferFinalBlock::must_be_deferred> b;
+
+               Botan::BufferSlicer full_block_1(data);
+               const auto r1 = b.handle_unaligned_data(full_block_1);
+               result.confirm("exactly aligned data is buffered", !r1.has_value());
+               result.confirm("!in_alignment()", !b.in_alignment());
+               result.confirm("ready_to_consume()", b.ready_to_consume());
+               result.confirm("exactly aligned block is consumed", full_block_1.empty());
+
+               Botan::BufferSlicer empty_input({});
+               const auto r2 = b.handle_unaligned_data(empty_input);
+               result.require("empty input does not push out buffer", !r2.has_value());
+               result.confirm("!in_alignment()", !b.in_alignment());
+               result.confirm("ready_to_consume()", b.ready_to_consume());
+
+               const uint8_t extra_byte = 1;
+               Botan::BufferSlicer one_extra_byte({&extra_byte, 1});
+               const auto r3 = b.handle_unaligned_data(one_extra_byte);
+               result.require("more data pushes out buffer", r3.has_value());
+               result.confirm("in_alignment()", b.in_alignment());
+               result.confirm("!ready_to_consume()", !b.ready_to_consume());
+               result.test_eq("no input data is consumed", one_extra_byte.remaining(), 1);
+
+               result.test_is_eq("collected block is correct", v(r3.value()), v(data));
+            }),
+
+      CHECK("Aligned data passthrough (no block-defer)",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32> b;
+               result.require("buffer is in alignment", b.in_alignment());
+
+               Botan::BufferSlicer half_block(first_half_data);
+               const auto [s1, r1] = b.aligned_data_to_process(half_block);
+               result.confirm("not enough data for alignment processing", s1.empty());
+               result.test_eq("not enough data for alignment processing", r1, 0);
+               result.test_eq("(short) unaligned data is not consumed", half_block.remaining(), 16);
+
+               const auto more_than_one_block = Botan::concat_as<std::vector<uint8_t>>(data, first_half_data);
+               Botan::BufferSlicer one_and_a_half_block(more_than_one_block);
+               const auto [s2, r2] = b.aligned_data_to_process(one_and_a_half_block);
+               result.test_eq("data of one block for processing", s2.size(), 32);
+               result.test_eq("one block for processing", r2, 1);
+               result.test_is_eq(v(s2), v(data));
+               result.test_eq("(middle) unaligned data is not consumed", one_and_a_half_block.remaining(), 16);
+
+               const auto two_blocks_data = Botan::concat_as<std::vector<uint8_t>>(data, data);
+               Botan::BufferSlicer two_blocks(two_blocks_data);
+               const auto [s3, r3] = b.aligned_data_to_process(two_blocks);
+               result.test_eq("data of two block for processing", s3.size(), 64);
+               result.test_eq("two blocks for processing", r3, 2);
+               result.test_is_eq(v(s3), two_blocks_data);
+               result.test_eq("aligned data is fully consumed", two_blocks.remaining(), 0);
+            }),
+
+      CHECK("Aligned data blockwise (no block-defer)",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32> b;
+               result.require("buffer is in alignment", b.in_alignment());
+
+               Botan::BufferSlicer half_block(first_half_data);
+               const auto s1 = b.next_aligned_block_to_process(half_block);
+               result.confirm("not enough data for alignment processing", !s1.has_value());
+               result.test_eq("(short) unaligned data is not consumed", half_block.remaining(), 16);
+
+               const auto more_than_one_block = Botan::concat_as<std::vector<uint8_t>>(data, first_half_data);
+               Botan::BufferSlicer one_and_a_half_block(more_than_one_block);
+               const auto s2 = b.next_aligned_block_to_process(one_and_a_half_block);
+               result.require("one block for processing", s2.has_value());
+               result.test_eq("data of one block for processing", s2->size(), 32);
+               result.test_is_eq(v(s2.value()), v(data));
+               result.test_eq("(middle) unaligned data is not consumed", one_and_a_half_block.remaining(), 16);
+
+               const auto two_blocks_data = Botan::concat_as<std::vector<uint8_t>>(data, data);
+               Botan::BufferSlicer two_blocks(two_blocks_data);
+               const auto s3_1 = b.next_aligned_block_to_process(two_blocks);
+               result.require("first block for processing", s3_1.has_value());
+               result.test_eq("data of first block for processing", s3_1->size(), 32);
+               result.test_is_eq(v(s3_1.value()), v(data));
+               result.test_eq("first block is consumed", two_blocks.remaining(), 32);
+
+               const auto s3_2 = b.next_aligned_block_to_process(two_blocks);
+               result.require("second block for processing", s3_2.has_value());
+               result.test_eq("data of second block for processing", s3_2->size(), 32);
+               result.test_is_eq(v(s3_2.value()), v(data));
+               result.test_eq("second block is consumed", two_blocks.remaining(), 0);
+            }),
+
+      CHECK("Aligned data passthrough (with block-defer)",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32, Botan::AlignmentBufferFinalBlock::must_be_deferred> b;
+               result.require("buffer is in alignment", b.in_alignment());
+
+               Botan::BufferSlicer half_block(first_half_data);
+               const auto [s1, r1] = b.aligned_data_to_process(half_block);
+               result.confirm("not enough data for alignment processing", s1.empty());
+               result.test_eq("not enough data for alignment processing", r1, 0);
+               result.test_eq("(short) unaligned data is not consumed", half_block.remaining(), 16);
+
+               const auto more_than_one_block = Botan::concat_as<std::vector<uint8_t>>(data, first_half_data);
+               Botan::BufferSlicer one_and_a_half_block(more_than_one_block);
+               const auto [s2, r2] = b.aligned_data_to_process(one_and_a_half_block);
+               result.test_eq("data of one block for processing", s2.size(), 32);
+               result.test_eq("one block for processing", r2, 1);
+               result.test_is_eq(v(s2), v(data));
+               result.test_eq("(middle) unaligned data is not consumed", one_and_a_half_block.remaining(), 16);
+
+               const auto two_blocks_data = Botan::concat_as<std::vector<uint8_t>>(data, data);
+               Botan::BufferSlicer two_blocks(two_blocks_data);
+               const auto [s3, r3] = b.aligned_data_to_process(two_blocks);
+               result.test_eq("data of first block for processing", s3.size(), 32);
+               result.test_eq("one block for processing", r3, 1);
+               result.test_is_eq(v(s3), v(data));
+               result.test_eq("aligned data is partially consumed", two_blocks.remaining(), 32);
+            }),
+
+      CHECK("Aligned data blockwise (with block-defer)",
+            [=](auto& result) {
+               Botan::AlignmentBuffer<uint8_t, 32, Botan::AlignmentBufferFinalBlock::must_be_deferred> b;
+               result.require("buffer is in alignment", b.in_alignment());
+
+               Botan::BufferSlicer half_block(first_half_data);
+               const auto s1 = b.next_aligned_block_to_process(half_block);
+               result.confirm("not enough data for alignment processing", !s1.has_value());
+               result.test_eq("(short) unaligned data is not consumed", half_block.remaining(), 16);
+
+               const auto more_than_one_block = Botan::concat_as<std::vector<uint8_t>>(data, first_half_data);
+               Botan::BufferSlicer one_and_a_half_block(more_than_one_block);
+               const auto s2 = b.next_aligned_block_to_process(one_and_a_half_block);
+               result.require("one block for processing", s2.has_value());
+               result.test_eq("data of one block for processing", s2->size(), 32);
+               result.test_is_eq(v(s2.value()), v(data));
+               result.test_eq("(middle) unaligned data is not consumed", one_and_a_half_block.remaining(), 16);
+
+               const auto two_blocks_data = Botan::concat_as<std::vector<uint8_t>>(data, data);
+               Botan::BufferSlicer two_blocks(two_blocks_data);
+               const auto s3_1 = b.next_aligned_block_to_process(two_blocks);
+               result.require("first block for processing", s3_1.has_value());
+               result.test_eq("data of first block for processing", s3_1->size(), 32);
+               result.test_is_eq(v(s3_1.value()), v(data));
+               result.test_eq("first block is consumed", two_blocks.remaining(), 32);
+
+               const auto s3_2 = b.next_aligned_block_to_process(two_blocks);
+               result.confirm("second block is not passed through", !s3_2.has_value());
+               result.test_eq("second block is not consumed", two_blocks.remaining(), 32);
+            }),
+   };
+}
+
+BOTAN_REGISTER_TEST_FN("utils", "buffer_utilities", test_buffer_slicer, test_buffer_stuffer, test_alignment_buffer);
 
 }  // namespace