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