Repository: randombit/botan
Pull request #3707 (2023-09-19 15:59:56)

[std::span] {load,store}_{le,be}
### Pull Request Dependencies

* ~~#3715~~

### Description

This has two objectives:

1. **range-based overloads for big/little endian helpers**
    The lack of this has been annoying me for some time now. Using ranges facilitates maximum usage convenience while enabling opportunistic compile-time buffer size checks when the passed range has static length information (read: `std::array` or `std::span<T, 42>`). Otherwise runtime checks will be performed (`BOTAN_ARG_CHECK`).
2. **Reduce the overload complexity**
    ... by letting the compiler generate most of the generic code as `constexpr`. I didn't see any slow-downs (in the runtime of the test suite) after applying this.

Here's a godbolt (of a somewhat stripped-down version) to play around with that, if needed: https://godbolt.org/z/sxjhnbePo

Currently, this includes legacy (pointer-based) overloads of the existing helpers. Those just assert that the passed in pointers are big enough and wrap them into properly sized `std::span`s to forward the calls. In an ideal world we'd need to go through the code base and replace all those calls with statically checkable invocations.

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

coverage: 91.677% (-0.07%) from 91.749% when pulling **a5b2609be402ad6ad65e661085d18f7f827e8c3d on Rohde-Schwarz:span/loadstor** into **1f26e75d781745a7dbead57f10e0d31077a79709 on randombit:master**.

reneme: Rebased after #3711 fixed the CI build.
reneme: Lets postpone this for a moment. I'm hoping that the iOS/Android update will bring more tools to make this easier.
randombit: I'm going to either update the emscripten build (#3720) or just disable the emscripten CI build, at which point we should be able to rely on having most of C++20.

[*] Outside of `std::source_location`, and probably 20 other things that we just haven't hit yet...
reneme: Rebased after #3760 conflicted with the underlying #3715.
reneme: This will need a rebase. But let's review/merge #3715 first and get this done after.
FAlbertDev: I'll review this PR in the following days!

Patch:
diff --git a/src/lib/utils/concepts.h b/src/lib/utils/concepts.h
index 90ce49ce7..595d18614 100644
--- a/src/lib/utils/concepts.h
+++ b/src/lib/utils/concepts.h
@@ -33,6 +33,14 @@ struct is_strong_type<Strong<Ts...>> : std::true_type {};
 template <typename... Ts>
 constexpr bool is_strong_type_v = is_strong_type<std::remove_const_t<Ts>...>::value;
 
+template <typename T0, typename... Ts>
+struct all_same {
+      static constexpr bool value = (std::is_same_v<T0, Ts> && ...);
+};
+
+template <typename... Ts>
+static constexpr bool all_same_v = all_same<Ts...>::value;
+
 namespace ranges {
 
 /**
@@ -205,6 +213,10 @@ concept contiguous_strong_type = strong_type<T> && contiguous_container<T>;
 template <typename T>
 concept integral = std::is_integral_v<T>;
 
+// TODO: C++20 - replace with std::unsigned_integral
+template <typename T>
+concept unsigned_integral = std::is_integral_v<T> && std::is_unsigned_v<T>;
+
 }  // namespace concepts
 
 }  // namespace Botan
diff --git a/src/lib/utils/ghash/ghash.cpp b/src/lib/utils/ghash/ghash.cpp
index 38604afdb..8c3b1ed6c 100644
--- a/src/lib/utils/ghash/ghash.cpp
+++ b/src/lib/utils/ghash/ghash.cpp
@@ -13,6 +13,8 @@
 #include <botan/internal/ct_utils.h>
 #include <botan/internal/loadstor.h>
 
+#include <array>
+
 namespace Botan {
 
 std::string GHASH::provider() const {
@@ -177,9 +179,12 @@ void GHASH::add_final_block(secure_vector<uint8_t>& hash, size_t ad_len, size_t
    * stack buffer is fine here since the text len is public
    * and the length of the AD is probably not sensitive either.
    */
-   uint8_t final_block[GCM_BS];
-   store_be<uint64_t>(final_block, 8 * ad_len, 8 * text_len);
-   ghash_update(hash, {final_block, GCM_BS});
+   std::array<uint8_t, GCM_BS> final_block;
+
+   const uint64_t ad_bits = 8 * ad_len;
+   const uint64_t text_bits = 8 * text_len;
+   store_be(final_block, ad_bits, text_bits);
+   ghash_update(hash, final_block);
 }
 
 void GHASH::final(std::span<uint8_t> mac) {
diff --git a/src/lib/utils/loadstor.h b/src/lib/utils/loadstor.h
index f0e50a1c2..15bc7ff7d 100644
--- a/src/lib/utils/loadstor.h
+++ b/src/lib/utils/loadstor.h
@@ -9,6 +9,7 @@
 #ifndef BOTAN_LOAD_STORE_H_
 #define BOTAN_LOAD_STORE_H_
 
+#include <botan/concepts.h>
 #include <botan/mem_ops.h>
 #include <botan/types.h>
 #include <botan/internal/bswap.h>
@@ -82,6 +83,107 @@ inline constexpr uint64_t make_uint64(
            (static_cast<uint64_t>(i6) << 8) | (static_cast<uint64_t>(i7)));
 }
 
+/**
+* Load a big-endian unsigned integer
+* @param in_range a fixed-length span with some bytes
+* @return T loaded from in, as a big-endian value
+*/
+template <concepts::unsigned_integral T, ranges::contiguous_range<uint8_t> InR>
+inline constexpr T load_be(InR&& in_range) {
+   ranges::assert_exact_byte_length<sizeof(T)>(in_range);
+   std::span in{in_range};
+   if constexpr(sizeof(T) == 1) {
+      return static_cast<T>(in[0]);
+   } else {
+#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
+      return typecast_copy<T>(in);
+#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
+      return reverse_bytes(typecast_copy<T>(in));
+#else
+      return [&]<size_t... i>(std::index_sequence<i...>) {
+         return ((static_cast<T>(in[i]) << ((sizeof(T) - i - 1) * 8)) | ...);
+      }
+      (std::make_index_sequence<sizeof(T)>());
+#endif
+   }
+}
+
+/**
+* Load a little-endian unsigned integer
+* @param in_range a fixed-length span with some bytes
+* @return T loaded from in, as a little-endian value
+*/
+template <concepts::unsigned_integral T, ranges::contiguous_range<uint8_t> InR>
+inline constexpr T load_le(InR&& in_range) {
+   ranges::assert_exact_byte_length<sizeof(T)>(in_range);
+   std::span in{in_range};
+   if constexpr(sizeof(T) == 1) {
+      return static_cast<T>(in[0]);
+   } else {
+#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
+      return reverse_bytes(typecast_copy<T>(in));
+#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
+      return typecast_copy<T>(in);
+#else
+      return [&]<size_t... i>(std::index_sequence<i...>) {
+         return ((static_cast<T>(in[i]) << (i * 8)) | ...);
+      }
+      (std::make_index_sequence<sizeof(T)>());
+#endif
+   }
+}
+
+namespace details {
+
+/**
+* Given a statically sized range @p r of size 1, 2, 4, or 8, this finds the
+* corresponding unsigned integer type to hold the value of @p r. Note, that
+* this only maps the type, and does not perform any loading.
+*/
+template <ranges::contiguous_range<uint8_t> InR>
+consteval auto default_load_type(InR&& r) {
+   std::span s{r};
+   static_assert(decltype(s)::extent != std::dynamic_extent,
+                 "Cannot determine the output type based on a dynamically sized bytearray");
+   constexpr size_t e = decltype(s)::extent;
+
+   // clang-format off
+   using type =
+      std::conditional_t<e == 1, uint8_t,
+      std::conditional_t<e == 2, uint16_t,
+      std::conditional_t<e == 4, uint32_t,
+      std::conditional_t<e == 8, uint64_t, void>>>>;
+   // clang-format on
+
+   static_assert(
+      !std::is_void_v<type>,
+      "Cannot determine the output type based on a statically sized bytearray with length other than those: 1, 2, 4, 8");
+
+   return type{};
+}
+
+}  // namespace details
+
+/**
+* Load a little-endian unsigned integer, auto-detect the output type
+* @param in_range a statically-sized range with some bytes
+* @return T loaded from in, as a little-endian value
+*/
+template <ranges::contiguous_range<uint8_t> InR>
+inline constexpr auto load_le(InR&& in_range) {
+   return load_le<decltype(details::default_load_type(in_range)), InR>(std::forward<InR>(in_range));
+}
+
+/**
+* Load a big-endian unsigned integer, auto-detect the output type
+* @param in_range a statically-sized range with some bytes
+* @return T loaded from in, as a big-endian value
+*/
+template <ranges::contiguous_range<uint8_t> InR>
+inline constexpr auto load_be(InR&& in_range) {
+   return load_be<decltype(details::default_load_type(in_range)), InR>(std::forward<InR>(in_range));
+}
+
 /**
 * Load a big-endian word
 * @param in a pointer to some bytes
@@ -115,169 +217,87 @@ inline constexpr T load_le(const uint8_t in[], size_t off) {
 }
 
 /**
-* Load a big-endian uint16_t
+* Load a big-endian unsigned integer
 * @param in a pointer to some bytes
 * @param off an offset into the array
-* @return off'th uint16_t of in, as a big-endian value
+* @return off'th unsigned integer of in, as a big-endian value
 */
-template <>
-inline constexpr uint16_t load_be<uint16_t>(const uint8_t in[], size_t off) {
-   in += off * sizeof(uint16_t);
-
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   return typecast_copy<uint16_t>(in);
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   return reverse_bytes(typecast_copy<uint16_t>(in));
-#else
-   return make_uint16(in[0], in[1]);
-#endif
+template <concepts::unsigned_integral T>
+inline constexpr T load_be(const uint8_t in[], size_t off) {
+   // asserts that *in points to the correct amount of memory
+   return load_be<T>(std::span<const uint8_t, sizeof(T)>(in + off * sizeof(T), sizeof(T)));
 }
 
 /**
-* Load a little-endian uint16_t
+* Load a little-endian unsigned integer
 * @param in a pointer to some bytes
 * @param off an offset into the array
-* @return off'th uint16_t of in, as a little-endian value
+* @return off'th unsigned integer of in, as a little-endian value
 */
-template <>
-inline constexpr uint16_t load_le<uint16_t>(const uint8_t in[], size_t off) {
-   in += off * sizeof(uint16_t);
-
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   return reverse_bytes(typecast_copy<uint16_t>(in));
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   return typecast_copy<uint16_t>(in);
-#else
-   return make_uint16(in[1], in[0]);
-#endif
+template <concepts::unsigned_integral T>
+inline constexpr T load_le(const uint8_t in[], size_t off) {
+   // asserts that *in points to the correct amount of memory
+   return load_le<T>(std::span<const uint8_t, sizeof(T)>(in + off * sizeof(T), sizeof(T)));
 }
 
 /**
-* Load a big-endian uint32_t
-* @param in a pointer to some bytes
-* @param off an offset into the array
-* @return off'th uint32_t of in, as a big-endian value
+* Load many big-endian unsigned integers
+* @param in   a fixed-length span to some bytes
+* @param outs a arbitrary-length parameter list of unsigned integers to be loaded
 */
-template <>
-inline constexpr uint32_t load_be<uint32_t>(const uint8_t in[], size_t off) {
-   in += off * sizeof(uint32_t);
+template <ranges::contiguous_range<uint8_t> InR, concepts::unsigned_integral... Ts>
+   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
+inline constexpr void load_be(InR&& in, Ts&... outs) {
+   ranges::assert_exact_byte_length<(sizeof(Ts) + ...)>(in);
+   auto load_one = [off = 0]<typename T>(auto i, T& o) mutable {
+      o = load_be<T>(i.subspan(off).template first<sizeof(T)>());
+      off += sizeof(T);
+   };
 
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   return typecast_copy<uint32_t>(in);
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   return reverse_bytes(typecast_copy<uint32_t>(in));
-#else
-   return make_uint32(in[0], in[1], in[2], in[3]);
-#endif
+   (load_one(std::span{in}, outs), ...);
 }
 
 /**
-* Load a little-endian uint32_t
-* @param in a pointer to some bytes
-* @param off an offset into the array
-* @return off'th uint32_t of in, as a little-endian value
+* Load many little-endian unsigned integers
+* @param in   a fixed-length span to some bytes
+* @param outs a arbitrary-length parameter list of unsigned integers to be loaded
 */
-template <>
-inline constexpr uint32_t load_le<uint32_t>(const uint8_t in[], size_t off) {
-   in += off * sizeof(uint32_t);
+template <ranges::contiguous_range<uint8_t> InR, concepts::unsigned_integral... Ts>
+   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
+inline constexpr void load_le(InR&& in, Ts&... outs) {
+   ranges::assert_exact_byte_length<(sizeof(Ts) + ...)>(in);
+   auto load_one = [off = 0]<typename T>(auto i, T& o) mutable {
+      o = load_le<T>(i.subspan(off).template first<sizeof(T)>());
+      off += sizeof(T);
+   };
 
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   return reverse_bytes(typecast_copy<uint32_t>(in));
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   return typecast_copy<uint32_t>(in);
-#else
-   return make_uint32(in[3], in[2], in[1], in[0]);
-#endif
+   (load_one(std::span{in}, outs), ...);
 }
 
 /**
-* Load a big-endian uint64_t
-* @param in a pointer to some bytes
-* @param off an offset into the array
-* @return off'th uint64_t of in, as a big-endian value
+* Load many big-endian unsigned integers
+* @param in   a pointer to some bytes
+* @param outs a arbitrary-length parameter list of unsigned integers to be loaded
 */
-template <>
-inline constexpr uint64_t load_be<uint64_t>(const uint8_t in[], size_t off) {
-   in += off * sizeof(uint64_t);
-
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   return typecast_copy<uint64_t>(in);
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   return reverse_bytes(typecast_copy<uint64_t>(in));
-#else
-   return make_uint64(in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7]);
-#endif
+template <concepts::unsigned_integral... Ts>
+   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
+inline constexpr void load_be(const uint8_t in[], Ts&... outs) {
+   constexpr auto bytes = (sizeof(outs) + ...);
+   // asserts that *in points to the correct amount of memory
+   load_be(std::span<const uint8_t, bytes>(in, bytes), outs...);
 }
 
 /**
-* Load a little-endian uint64_t
-* @param in a pointer to some bytes
-* @param off an offset into the array
-* @return off'th uint64_t of in, as a little-endian value
+* Load many little-endian unsigned integers
+* @param in   a pointer to some bytes
+* @param outs a arbitrary-length parameter list of unsigned integers to be loaded
 */
-template <>
-inline constexpr uint64_t load_le<uint64_t>(const uint8_t in[], size_t off) {
-   in += off * sizeof(uint64_t);
-
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   return reverse_bytes(typecast_copy<uint64_t>(in));
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   return typecast_copy<uint64_t>(in);
-#else
-   return make_uint64(in[7], in[6], in[5], in[4], in[3], in[2], in[1], in[0]);
-#endif
-}
-
-/**
-* Load two little-endian words
-* @param in a pointer to some bytes
-* @param x0 where the first word will be written
-* @param x1 where the second word will be written
-*/
-template <typename T>
-inline constexpr void load_le(const uint8_t in[], T& x0, T& x1) {
-   x0 = load_le<T>(in, 0);
-   x1 = load_le<T>(in, 1);
-}
-
-/**
-* Load four little-endian words
-* @param in a pointer to some bytes
-* @param x0 where the first word will be written
-* @param x1 where the second word will be written
-* @param x2 where the third word will be written
-* @param x3 where the fourth word will be written
-*/
-template <typename T>
-inline constexpr void load_le(const uint8_t in[], T& x0, T& x1, T& x2, T& x3) {
-   x0 = load_le<T>(in, 0);
-   x1 = load_le<T>(in, 1);
-   x2 = load_le<T>(in, 2);
-   x3 = load_le<T>(in, 3);
-}
-
-/**
-* Load eight little-endian words
-* @param in a pointer to some bytes
-* @param x0 where the first word will be written
-* @param x1 where the second word will be written
-* @param x2 where the third word will be written
-* @param x3 where the fourth word will be written
-* @param x4 where the fifth word will be written
-* @param x5 where the sixth word will be written
-* @param x6 where the seventh word will be written
-* @param x7 where the eighth word will be written
-*/
-template <typename T>
-inline constexpr void load_le(const uint8_t in[], T& x0, T& x1, T& x2, T& x3, T& x4, T& x5, T& x6, T& x7) {
-   x0 = load_le<T>(in, 0);
-   x1 = load_le<T>(in, 1);
-   x2 = load_le<T>(in, 2);
-   x3 = load_le<T>(in, 3);
-   x4 = load_le<T>(in, 4);
-   x5 = load_le<T>(in, 5);
-   x6 = load_le<T>(in, 6);
-   x7 = load_le<T>(in, 7);
+template <concepts::unsigned_integral... Ts>
+   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
+inline constexpr void load_le(const uint8_t in[], Ts&... outs) {
+   constexpr auto bytes = (sizeof(outs) + ...);
+   // asserts that *in points to the correct amount of memory
+   load_le(std::span<const uint8_t, bytes>(in, bytes), outs...);
 }
 
 /**
@@ -304,58 +324,6 @@ inline constexpr void load_le(T out[], const uint8_t in[], size_t count) {
    }
 }
 
-/**
-* Load two big-endian words
-* @param in a pointer to some bytes
-* @param x0 where the first word will be written
-* @param x1 where the second word will be written
-*/
-template <typename T>
-inline constexpr void load_be(const uint8_t in[], T& x0, T& x1) {
-   x0 = load_be<T>(in, 0);
-   x1 = load_be<T>(in, 1);
-}
-
-/**
-* Load four big-endian words
-* @param in a pointer to some bytes
-* @param x0 where the first word will be written
-* @param x1 where the second word will be written
-* @param x2 where the third word will be written
-* @param x3 where the fourth word will be written
-*/
-template <typename T>
-inline constexpr void load_be(const uint8_t in[], T& x0, T& x1, T& x2, T& x3) {
-   x0 = load_be<T>(in, 0);
-   x1 = load_be<T>(in, 1);
-   x2 = load_be<T>(in, 2);
-   x3 = load_be<T>(in, 3);
-}
-
-/**
-* Load eight big-endian words
-* @param in a pointer to some bytes
-* @param x0 where the first word will be written
-* @param x1 where the second word will be written
-* @param x2 where the third word will be written
-* @param x3 where the fourth word will be written
-* @param x4 where the fifth word will be written
-* @param x5 where the sixth word will be written
-* @param x6 where the seventh word will be written
-* @param x7 where the eighth word will be written
-*/
-template <typename T>
-inline constexpr void load_be(const uint8_t in[], T& x0, T& x1, T& x2, T& x3, T& x4, T& x5, T& x6, T& x7) {
-   x0 = load_be<T>(in, 0);
-   x1 = load_be<T>(in, 1);
-   x2 = load_be<T>(in, 2);
-   x3 = load_be<T>(in, 3);
-   x4 = load_be<T>(in, 4);
-   x5 = load_be<T>(in, 5);
-   x6 = load_be<T>(in, 6);
-   x7 = load_be<T>(in, 7);
-}
-
 /**
 * Load a variable number of big-endian words
 * @param out the output array of words
@@ -382,219 +350,157 @@ inline constexpr void load_be(T out[], const uint8_t in[], size_t count) {
 }
 
 /**
-* Store a big-endian uint16_t
-* @param in the input uint16_t
-* @param out the byte array to write to
+* Store a big-endian unsigned integer
+* @param in the input unsigned integer
+* @param out_range the fixed-length span to write to
 */
-inline constexpr void store_be(uint16_t in, uint8_t out[2]) {
+template <concepts::unsigned_integral T, ranges::contiguous_output_range<uint8_t> OutR>
+inline constexpr void store_be(T in, OutR&& out_range) {
+   ranges::assert_exact_byte_length<sizeof(T)>(out_range);
+   std::span out{out_range};
+   if constexpr(sizeof(T) == 1) {
+      out[0] = static_cast<uint8_t>(in);
+   } else {
 #if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   typecast_copy(out, in);
+      typecast_copy(out, in);
 #elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   typecast_copy(out, reverse_bytes(in));
+      typecast_copy(out, reverse_bytes(in));
 #else
-   out[0] = get_byte<0>(in);
-   out[1] = get_byte<1>(in);
+      [&]<size_t... i>(std::index_sequence<i...>) {
+         ((out[i] = get_byte<i>(in)), ...);
+      }
+      (std::make_index_sequence<sizeof(T)>());
 #endif
+   }
 }
 
 /**
-* Store a little-endian uint16_t
-* @param in the input uint16_t
-* @param out the byte array to write to
+* Store a little-endian unsigned integer
+* @param in the input unsigned integer
+* @param out_range the fixed-length span to write to
 */
-inline constexpr void store_le(uint16_t in, uint8_t out[2]) {
+template <concepts::unsigned_integral T, ranges::contiguous_output_range<uint8_t> OutR>
+inline constexpr void store_le(T in, OutR&& out_range) {
+   ranges::assert_exact_byte_length<sizeof(T)>(out_range);
+   std::span out{out_range};
+   if constexpr(sizeof(T) == 1) {
+      out[0] = static_cast<uint8_t>(in);
+   } else {
 #if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   typecast_copy(out, reverse_bytes(in));
+      typecast_copy(out, reverse_bytes(in));
 #elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   typecast_copy(out, in);
+      typecast_copy(out, in);
 #else
-   out[0] = get_byte<1>(in);
-   out[1] = get_byte<0>(in);
+      [&]<size_t... i>(std::index_sequence<i...>) {
+         ((out[i] = get_byte<sizeof(T) - i - 1>(in)), ...);
+      }
+      (std::make_index_sequence<sizeof(T)>());
 #endif
+   }
 }
 
 /**
-* Store a big-endian uint32_t
-* @param in the input uint32_t
+* Store a big-endian unsigned integer
+* @param in the input unsigned integer
 * @param out the byte array to write to
 */
-inline constexpr void store_be(uint32_t in, uint8_t out[4]) {
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   typecast_copy(out, in);
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   typecast_copy(out, reverse_bytes(in));
-#else
-   out[0] = get_byte<0>(in);
-   out[1] = get_byte<1>(in);
-   out[2] = get_byte<2>(in);
-   out[3] = get_byte<3>(in);
-#endif
+template <concepts::unsigned_integral T>
+inline constexpr void store_be(T in, uint8_t out[sizeof(T)]) {
+   store_be(in, std::span<uint8_t, sizeof(T)>(out, sizeof(T)));
 }
 
 /**
-* Store a little-endian uint32_t
-* @param in the input uint32_t
+* Store a little-endian unsigned integer
+* @param in the input unsigned integer
 * @param out the byte array to write to
 */
-inline constexpr void store_le(uint32_t in, uint8_t out[4]) {
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   typecast_copy(out, reverse_bytes(in));
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   typecast_copy(out, in);
-#else
-   out[0] = get_byte<3>(in);
-   out[1] = get_byte<2>(in);
-   out[2] = get_byte<1>(in);
-   out[3] = get_byte<0>(in);
-#endif
+template <concepts::unsigned_integral T>
+inline constexpr void store_le(T in, uint8_t out[sizeof(T)]) {
+   store_le(in, std::span<uint8_t, sizeof(T)>(out, sizeof(T)));
 }
 
 /**
-* Store a big-endian uint64_t
-* @param in the input uint64_t
-* @param out the byte array to write to
+* Store many big-endian unsigned integers
+* @param out a fixed-length span to some bytes
+* @param ins a arbitrary-length parameter list of unsigned integers to be stored
 */
-inline constexpr void store_be(uint64_t in, uint8_t out[8]) {
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   typecast_copy(out, in);
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   typecast_copy(out, reverse_bytes(in));
-#else
-   out[0] = get_byte<0>(in);
-   out[1] = get_byte<1>(in);
-   out[2] = get_byte<2>(in);
-   out[3] = get_byte<3>(in);
-   out[4] = get_byte<4>(in);
-   out[5] = get_byte<5>(in);
-   out[6] = get_byte<6>(in);
-   out[7] = get_byte<7>(in);
-#endif
-}
+template <ranges::contiguous_output_range<uint8_t> OutR, concepts::unsigned_integral... Ts>
+   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
+inline constexpr void store_be(OutR&& out, Ts... ins) {
+   ranges::assert_exact_byte_length<(sizeof(Ts) + ...)>(out);
+   auto store_one = [off = 0]<typename T>(auto o, T i) mutable {
+      store_be<T>(i, o.subspan(off).template first<sizeof(T)>());
+      off += sizeof(T);
+   };
 
-/**
-* Store a little-endian uint64_t
-* @param in the input uint64_t
-* @param out the byte array to write to
-*/
-inline constexpr void store_le(uint64_t in, uint8_t out[8]) {
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-   typecast_copy(out, reverse_bytes(in));
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-   typecast_copy(out, in);
-#else
-   out[0] = get_byte<7>(in);
-   out[1] = get_byte<6>(in);
-   out[2] = get_byte<5>(in);
-   out[3] = get_byte<4>(in);
-   out[4] = get_byte<3>(in);
-   out[5] = get_byte<2>(in);
-   out[6] = get_byte<1>(in);
-   out[7] = get_byte<0>(in);
-#endif
+   (store_one(std::span{out}, ins), ...);
 }
 
 /**
-* Store two little-endian words
-* @param out the output byte array
-* @param x0 the first word
-* @param x1 the second word
+* Store many little-endian unsigned integers
+* @param out a fixed-length span to some bytes
+* @param ins a arbitrary-length parameter list of unsigned integers to be stored
 */
-template <typename T>
-inline constexpr void store_le(uint8_t out[], T x0, T x1) {
-   store_le(x0, out + (0 * sizeof(T)));
-   store_le(x1, out + (1 * sizeof(T)));
-}
+template <ranges::contiguous_output_range<uint8_t> OutR, concepts::unsigned_integral... Ts>
+   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
+inline constexpr void store_le(OutR&& out, Ts... ins) {
+   ranges::assert_exact_byte_length<(sizeof(Ts) + ...)>(out);
+   auto store_one = [off = 0]<typename T>(auto o, T i) mutable {
+      store_le<T>(i, o.subspan(off).template first<sizeof(T)>());
+      off += sizeof(T);
+   };
 
-/**
-* Store two big-endian words
-* @param out the output byte array
-* @param x0 the first word
-* @param x1 the second word
-*/
-template <typename T>
-inline constexpr void store_be(uint8_t out[], T x0, T x1) {
-   store_be(x0, out + (0 * sizeof(T)));
-   store_be(x1, out + (1 * sizeof(T)));
+   (store_one(std::span{out}, ins), ...);
 }
 
 /**
-* Store four little-endian words
-* @param out the output byte array
-* @param x0 the first word
-* @param x1 the second word
-* @param x2 the third word
-* @param x3 the fourth word
+* Store many big-endian unsigned integers
+* @param ins a pointer to some bytes to be written
+* @param out a arbitrary-length parameter list of unsigned integers to be stored
 */
-template <typename T>
-inline constexpr void store_le(uint8_t out[], T x0, T x1, T x2, T x3) {
-   store_le(x0, out + (0 * sizeof(T)));
-   store_le(x1, out + (1 * sizeof(T)));
-   store_le(x2, out + (2 * sizeof(T)));
-   store_le(x3, out + (3 * sizeof(T)));
+template <concepts::unsigned_integral... Ts>
+   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
+inline constexpr void store_be(uint8_t out[], Ts... ins) {
+   constexpr auto bytes = (sizeof(ins) + ...);
+   // asserts that *out points to the correct amount of memory
+   store_be(std::span<uint8_t, bytes>(out, bytes), ins...);
 }
 
 /**
-* Store four big-endian words
-* @param out the output byte array
-* @param x0 the first word
-* @param x1 the second word
-* @param x2 the third word
-* @param x3 the fourth word
+* Store many little-endian unsigned integers
+* @param ins a pointer to some bytes to be written
+* @param out a arbitrary-length parameter list of unsigned integers to be stored
 */
-template <typename T>
-inline constexpr void store_be(uint8_t out[], T x0, T x1, T x2, T x3) {
-   store_be(x0, out + (0 * sizeof(T)));
-   store_be(x1, out + (1 * sizeof(T)));
-   store_be(x2, out + (2 * sizeof(T)));
-   store_be(x3, out + (3 * sizeof(T)));
+template <concepts::unsigned_integral... Ts>
+   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
+inline constexpr void store_le(uint8_t out[], Ts... ins) {
+   constexpr auto bytes = (sizeof(ins) + ...);
+   // asserts that *out points to the correct amount of memory
+   store_le(std::span<uint8_t, bytes>(out, bytes), ins...);
 }
 
 /**
-* Store eight little-endian words
-* @param out the output byte array
-* @param x0 the first word
-* @param x1 the second word
-* @param x2 the third word
-* @param x3 the fourth word
-* @param x4 the fifth word
-* @param x5 the sixth word
-* @param x6 the seventh word
-* @param x7 the eighth word
+* Store a big-endian unsigned integer
+* @param in the input unsigned integer
+* @return a byte array holding the integer value in big-endian byte order
 */
-template <typename T>
-inline constexpr void store_le(uint8_t out[], T x0, T x1, T x2, T x3, T x4, T x5, T x6, T x7) {
-   store_le(x0, out + (0 * sizeof(T)));
-   store_le(x1, out + (1 * sizeof(T)));
-   store_le(x2, out + (2 * sizeof(T)));
-   store_le(x3, out + (3 * sizeof(T)));
-   store_le(x4, out + (4 * sizeof(T)));
-   store_le(x5, out + (5 * sizeof(T)));
-   store_le(x6, out + (6 * sizeof(T)));
-   store_le(x7, out + (7 * sizeof(T)));
+template <concepts::unsigned_integral T>
+inline constexpr auto store_be(T in) {
+   std::array<uint8_t, sizeof(T)> out;
+   store_be(in, out);
+   return out;
 }
 
 /**
-* Store eight big-endian words
-* @param out the output byte array
-* @param x0 the first word
-* @param x1 the second word
-* @param x2 the third word
-* @param x3 the fourth word
-* @param x4 the fifth word
-* @param x5 the sixth word
-* @param x6 the seventh word
-* @param x7 the eighth word
+* Store a little-endian unsigned integer
+* @param in the input unsigned integer
+* @return a byte array holding the integer value in little-endian byte order
 */
-template <typename T>
-inline constexpr void store_be(uint8_t out[], T x0, T x1, T x2, T x3, T x4, T x5, T x6, T x7) {
-   store_be(x0, out + (0 * sizeof(T)));
-   store_be(x1, out + (1 * sizeof(T)));
-   store_be(x2, out + (2 * sizeof(T)));
-   store_be(x3, out + (3 * sizeof(T)));
-   store_be(x4, out + (4 * sizeof(T)));
-   store_be(x5, out + (5 * sizeof(T)));
-   store_be(x6, out + (6 * sizeof(T)));
-   store_be(x7, out + (7 * sizeof(T)));
+template <concepts::unsigned_integral T>
+inline constexpr auto store_le(T in) {
+   std::array<uint8_t, sizeof(T)> out;
+   store_le(in, out);
+   return out;
 }
 
 template <typename T>


Test patch:
diff --git a/src/tests/test_utils.cpp b/src/tests/test_utils.cpp
index ae056009d..604bd782f 100644
--- a/src/tests/test_utils.cpp
+++ b/src/tests/test_utils.cpp
@@ -169,6 +169,44 @@ class Utility_Function_Tests final : public Text_Based_Test {
             result.test_is_eq<uint8_t>(out[7], 0xAB);
          }
 
+         std::array<uint8_t, 8> outarr;
+         uint16_t i0, i1, i2, i3;
+         Botan::store_be(in64, outarr);
+
+         Botan::load_be(outarr, i0, i1, i2, i3);
+         result.test_is_eq<uint16_t>(i0, 0xABCD);
+         result.test_is_eq<uint16_t>(i1, 0xEF01);
+         result.test_is_eq<uint16_t>(i2, 0x2345);
+         result.test_is_eq<uint16_t>(i3, 0x6789);
+
+         Botan::load_le(std::span{outarr}.first<6>(), i0, i1, i2);
+         result.test_is_eq<uint16_t>(i0, 0xCDAB);
+         result.test_is_eq<uint16_t>(i1, 0x01EF);
+         result.test_is_eq<uint16_t>(i2, 0x4523);
+         result.test_is_eq<uint16_t>(i3, 0x6789);  // remains unchanged
+
+         Botan::store_le(in64, outarr);
+
+         Botan::load_le(outarr, i0, i1, i2, i3);
+         result.test_is_eq<uint16_t>(i0, 0x6789);
+         result.test_is_eq<uint16_t>(i1, 0x2345);
+         result.test_is_eq<uint16_t>(i2, 0xEF01);
+         result.test_is_eq<uint16_t>(i3, 0xABCD);
+
+         Botan::load_be(std::span{outarr}.first<6>(), i0, i1, i2);
+         result.test_is_eq<uint16_t>(i0, 0x8967);
+         result.test_is_eq<uint16_t>(i1, 0x4523);
+         result.test_is_eq<uint16_t>(i2, 0x01EF);
+         result.test_is_eq<uint16_t>(i3, 0xABCD);  // remains unchanged
+
+         result.test_is_eq(in16, Botan::load_be(Botan::store_be(in16)));
+         result.test_is_eq(in32, Botan::load_be(Botan::store_be(in32)));
+         result.test_is_eq(in64, Botan::load_be(Botan::store_be(in64)));
+
+         result.test_is_eq(in16, Botan::load_le(Botan::store_le(in16)));
+         result.test_is_eq(in32, Botan::load_le(Botan::store_le(in32)));
+         result.test_is_eq(in64, Botan::load_le(Botan::store_le(in64)));
+
          return result;
       }
 };