Botan 3.6.0
Crypto and TLS for C&
pqcrystals_encoding.h
Go to the documentation of this file.
1/*
2 * PQ CRYSTALS Encoding Helpers
3 *
4 * Further changes
5 * (C) 2024 Jack Lloyd
6 * (C) 2024 René Meusel, Rohde & Schwarz Cybersecurity
7 *
8 * Botan is released under the Simplified BSD License (see license.txt)
9 */
10
11#ifndef BOTAN_PQ_CRYSTALS_ENCODING_H_
12#define BOTAN_PQ_CRYSTALS_ENCODING_H_
13
14#include <limits>
15#include <numeric>
16#include <span>
17
18#include <botan/internal/loadstor.h>
19#include <botan/internal/pqcrystals.h>
20#include <botan/internal/pqcrystals_helpers.h>
21#include <botan/internal/stl_util.h>
22
23#if defined(BOTAN_HAS_XOF)
24 #include <botan/xof.h>
25#endif
26namespace Botan::CRYSTALS {
27
28namespace detail {
29
30constexpr auto as_byte_source(BufferSlicer& slicer) {
31 return [&](std::span<uint8_t> out) { slicer.copy_into(out); };
32}
33
34#if defined(BOTAN_HAS_XOF)
35constexpr auto as_byte_source(Botan::XOF& xof) {
36 return [&](std::span<uint8_t> out) { xof.output(out); };
37}
38#endif
39
40} // namespace detail
41
42template <typename T>
43concept byte_source =
44 requires(T& t) { requires std::invocable<decltype(detail::as_byte_source(t)), std::span<uint8_t>>; };
45
46template <typename T, typename PolyCoeffT>
47concept coeff_map_fn = std::signed_integral<PolyCoeffT> && requires(T fn, PolyCoeffT coeff) {
48 { fn(coeff) } -> std::same_as<std::make_unsigned_t<PolyCoeffT>>;
49};
50
51template <typename T, typename PolyCoeffT>
53 std::signed_integral<PolyCoeffT> && requires(T fn, std::make_unsigned_t<PolyCoeffT> coeff_value) {
54 { fn(coeff_value) } -> std::same_as<PolyCoeffT>;
55 };
56
57/**
58 * Helper for base implementations of NIST FIPS 204, Algorithms 16-19 and
59 * NIST FIPS 203 Algorithms 5-6. It pre-computes generic values to bit-(un)pack
60 * polynomial coefficients at compile-time.
61 *
62 * The base implementations are also templated with the @p range parameter
63 * forcing the compiler to generate specialized code for each supported range.
64 */
65template <int32_t range, crystals_trait PolyTrait>
67 using T = typename PolyTrait::T;
68 using unsigned_T = std::make_unsigned_t<T>;
69 using sink_t = uint64_t;
70
71 static_assert(range <= std::numeric_limits<T>::max());
72
73 constexpr static size_t bits_in_collector = sizeof(sink_t) * 8;
74 constexpr static size_t bits_per_coeff = bitlen(range);
75 constexpr static size_t bits_per_pack = [] {
76 // Ensure that the bit-packing is byte-aligned and scale it
77 // to utilize the collector's bit-width as much as possible.
78 size_t smallest_aligned_pack = std::lcm(bits_per_coeff, size_t(8));
79 return (smallest_aligned_pack < bits_in_collector)
80 ? (bits_in_collector / smallest_aligned_pack) * smallest_aligned_pack
81 : smallest_aligned_pack;
82 }();
83 constexpr static size_t bytes_per_pack = bits_per_pack / 8;
84 constexpr static size_t coeffs_per_pack = bits_per_pack / bits_per_coeff;
85 constexpr static size_t collectors_per_pack = (bytes_per_pack + sizeof(sink_t) - 1) / sizeof(sink_t);
86 constexpr static size_t collector_bytes_per_pack = collectors_per_pack * sizeof(sink_t);
87 constexpr static sink_t value_mask = (1 << bits_per_coeff) - 1;
88
89 using collector_array = std::array<sink_t, collectors_per_pack>;
90 using collector_bytearray = std::array<uint8_t, collector_bytes_per_pack>;
91
92 static_assert(PolyTrait::N % coeffs_per_pack == 0);
93};
94
95/**
96 * Base implementation of NIST FIPS 203 Algorithm 5 (ByteEncode) and NIST
97 * FIPS 204 Algorithms 16 (SimpleBitPack) and 17 (BitPack).
98 *
99 * This takes a polynomial @p p and packs its coefficients into the buffer
100 * represented by @p stuffer. Optionally, the coefficients can be transformed
101 * using the @p map function before packing them. Kyber uses @p map to compress
102 * the coefficients as needed, Dilithium to transform coefficients to unsigned.
103 *
104 * The implementation assumes that the values returned from the custom @p map
105 * transformation are in the range [0, range]. No assumption is made about the
106 * value range of the coefficients in the polynomial @p p.
107 *
108 * Note that this bit-packing algorithm is inefficient if the bit-length of the
109 * coefficients is a multiple of 8. In that case, a byte-level encoding (that
110 * might need to take endianess into account) would be more efficient. However,
111 * neither Kyber nor Dilithium instantiate bit-packings with such a value range.
112 *
113 * @tparam range the upper bound of the coefficient range.
114 */
115template <int32_t range, crystals_trait PolyTrait, Domain D, coeff_map_fn<typename PolyTrait::T> MapFnT>
116constexpr void pack(const Polynomial<PolyTrait, D>& p, BufferStuffer& stuffer, MapFnT map) {
118
119 BOTAN_DEBUG_ASSERT(stuffer.remaining_capacity() >= p.size() * trait::bits_per_coeff / 8);
120
121 // Bit-packing example that shows a coefficients' bit-pack that spills across
122 // more than one 64-bit collectors. This illustrates the algorithm below.
123 //
124 // 0 64 128
125 // Collectors (64 bits): | collectors[0] | collectors[1] |
126 // | | |
127 // Coefficients (11 bits): | c[0] | c[1] | c[2] | c[3] | c[4] | c[5] | c[6] | c[7] | | | | | ...
128 // | | |
129 // | < byte-aligned coefficient pack > | < byte-aligned pad. > |
130 // | (one inner loop iteration) |
131 // 0 88 (divisible by 8)
132
133 for(size_t i = 0; i < p.size(); i += trait::coeffs_per_pack) {
134 // The collectors array is filled with bit-packed coefficients to produce
135 // a byte-aligned pack of coefficients. When coefficients fall onto the
136 // boundary of two collectors, their bits must be split.
137 typename trait::collector_array collectors = {0};
138 for(size_t j = 0, bit_offset = 0, c = 0; j < trait::coeffs_per_pack; ++j) {
139 // Transform p[i] via a custom map function (that may be a NOOP).
140 const typename trait::unsigned_T mapped_coeff = map(p[i + j]);
141 const auto coeff_value = static_cast<typename trait::sink_t>(mapped_coeff);
142
143 // pack() is called only on data produced by us. If the values returned
144 // by the map function are not in the range [0, range] we have a bug.
145 BOTAN_DEBUG_ASSERT(coeff_value <= range);
146
147 // Bit-pack the coefficient into the collectors array and keep track of
148 // the bit-offset within the current collector. Note that this might
149 // shift some high-bits of the coefficient out of the current collector.
150 collectors[c] |= coeff_value << bit_offset;
151 bit_offset += trait::bits_per_coeff;
152
153 // If the bit-offset now exceeds the collector's bit-width, we fill the
154 // next collector with the high-bits that didn't fit into the previous.
155 // The bit-offset is adjusted to now point into the new collector.
156 if(bit_offset > trait::bits_in_collector) {
157 bit_offset = bit_offset - trait::bits_in_collector;
158 collectors[++c] = coeff_value >> (trait::bits_per_coeff - bit_offset);
159 }
160 }
161
162 // One byte-aligned pack of bit-packed coefficients is now stored in the
163 // collectors and can be written to an output buffer. Note that we might
164 // have to remove some padding bytes of unused collector space.
165 const auto bytes = store_le(collectors);
166 stuffer.append(std::span{bytes}.template first<trait::bytes_per_pack>());
167 }
168}
169
170/**
171 * Base implementation of NIST FIPS 203 Algorithm 6 (ByteDecode) and NIST
172 * FIPS 204 Algorithms 18 (SimpleBitUnpack) and 19 (BitUnpack).
173 *
174 * This takes a byte sequence represented by @p byte_source and unpacks its
175 * coefficients into the polynomial @p p. Optionally, the coefficients can be
176 * transformed using the @p unmap function after unpacking them. Note that
177 * the @p unmap function must be able to deal with out-of-range values, as the
178 * input to `unpack()` may be untrusted data.
179 *
180 * Kyber uses @p unmap to decompress the coefficients as needed, Dilithium uses
181 * it to convert the coefficients back to signed integers.
182 *
183 * @tparam range the upper bound of the coefficient range.
184 */
185template <int32_t range,
186 byte_source ByteSourceT,
187 crystals_trait PolyTrait,
188 Domain D,
189 coeff_unmap_fn<typename PolyTrait::T> UnmapFnT>
190constexpr void unpack(Polynomial<PolyTrait, D>& p, ByteSourceT& byte_source, UnmapFnT unmap) {
192
193 auto get_bytes = detail::as_byte_source(byte_source);
194 typename trait::collector_bytearray bytes = {0};
195
196 // This is the inverse operation of the bit-packing algorithm above. Please
197 // refer to the comments there for a detailed explanation of the algorithm.
198 for(size_t i = 0; i < p.size(); i += trait::coeffs_per_pack) {
199 get_bytes(std::span{bytes}.template first<trait::bytes_per_pack>());
200 const auto collectors = load_le<typename trait::collector_array>(bytes);
201
202 for(size_t j = 0, bit_offset = 0, c = 0; j < trait::coeffs_per_pack; ++j) {
203 typename trait::sink_t coeff_value = collectors[c] >> bit_offset;
204 bit_offset += trait::bits_per_coeff;
205 if(bit_offset > trait::bits_in_collector) {
206 bit_offset = bit_offset - trait::bits_in_collector;
207 coeff_value |= collectors[++c] << (trait::bits_per_coeff - bit_offset);
208 }
209
210 // unpack() may be called on data produced by an untrusted party.
211 // The values passed into the unmap function may be out of range, hence
212 // it is acceptable for unmap to return an out-of-range value then.
213 //
214 // For that reason we cannot use BOTAN_ASSERT[_DEBUG] on the values.
215 p[i + j] = unmap(static_cast<typename trait::unsigned_T>(coeff_value & trait::value_mask));
216 }
217 }
218}
219
220/// Overload for packing polynomials with a NOOP map function
221template <int32_t range, crystals_trait PolyTrait, Domain D>
222constexpr void pack(const Polynomial<PolyTrait, D>& p, BufferStuffer& stuffer) {
223 using unsigned_T = std::make_unsigned_t<typename PolyTrait::T>;
224 pack<range>(p, stuffer, [](typename PolyTrait::T x) { return static_cast<unsigned_T>(x); });
225}
226
227/// Overload for unpacking polynomials with a NOOP unmap function
228template <int32_t range, byte_source ByteSourceT, crystals_trait PolyTrait, Domain D>
229constexpr void unpack(Polynomial<PolyTrait, D>& p, ByteSourceT& byte_source) {
230 using unsigned_T = std::make_unsigned_t<typename PolyTrait::T>;
231 unpack<range>(p, byte_source, [](unsigned_T x) { return static_cast<typename PolyTrait::T>(x); });
232}
233
234} // namespace Botan::CRYSTALS
235
236#endif
#define BOTAN_DEBUG_ASSERT(expr)
Definition assert.h:98
void copy_into(std::span< uint8_t > sink)
Definition stl_util.h:120
Helper class to ease in-place marshalling of concatenated fixed-length values.
Definition stl_util.h:142
constexpr void append(std::span< const uint8_t > buffer)
Definition stl_util.h:177
constexpr size_t remaining_capacity() const
Definition stl_util.h:189
constexpr size_t size() const
Definition pqcrystals.h:274
T output(size_t bytes)
Definition xof.h:155
int(* final)(unsigned char *, CTX *)
FE_25519 T
Definition ge.cpp:34
constexpr auto as_byte_source(BufferSlicer &slicer)
constexpr void unpack(Polynomial< PolyTrait, D > &p, ByteSourceT &byte_source, UnmapFnT unmap)
constexpr void pack(const Polynomial< PolyTrait, D > &p, BufferStuffer &stuffer, MapFnT map)
constexpr auto store_le(ParamTs &&... params)
Definition loadstor.h:764
constexpr auto load_le(ParamTs &&... params)
Definition loadstor.h:521
constexpr auto bitlen(size_t x)
static constexpr size_t coeffs_per_pack
static constexpr size_t collectors_per_pack
static constexpr size_t bits_per_pack
static constexpr size_t bits_in_collector
static constexpr size_t bits_per_coeff
std::array< sink_t, collectors_per_pack > collector_array
std::array< uint8_t, collector_bytes_per_pack > collector_bytearray
std::make_unsigned_t< T > unsigned_T
static constexpr size_t bytes_per_pack
static constexpr size_t collector_bytes_per_pack