Botan 3.6.0
Crypto and TLS for C&
kyber_algos.cpp
Go to the documentation of this file.
1/*
2 * Crystals Kyber Internal Algorithms
3 * Based on the public domain reference implementation by the
4 * designers (https://github.com/pq-crystals/kyber)
5 *
6 * Further changes
7 * (C) 2021-2024 Jack Lloyd
8 * (C) 2021-2022 Manuel Glaser and Michael Boric, Rohde & Schwarz Cybersecurity
9 * (C) 2021-2022 René Meusel and Hannes Rantzsch, neXenio GmbH
10 * (C) 2024 René Meusel, Rohde & Schwarz Cybersecurity
11 *
12 * Botan is released under the Simplified BSD License (see license.txt)
13 */
14
15#include <botan/internal/kyber_algos.h>
16
17#include <botan/internal/kyber_helpers.h>
18#include <botan/internal/kyber_keys.h>
19#include <botan/internal/loadstor.h>
20#include <botan/internal/pqcrystals_encoding.h>
21#include <botan/internal/pqcrystals_helpers.h>
22
23#include <utility>
24
26
27namespace {
28
29/**
30 * NIST FIPS 203, Algorithm 5 (ByteEncode) for d < 12 in combination with
31 * Formula 4.7 (Compress)
32 */
33template <size_t d>
34 requires(d < 12)
35void poly_compress_and_encode(BufferStuffer& bs, const KyberPoly& p) {
36 CRYSTALS::pack<(1 << d) - 1>(p, bs, compress<d>);
37}
38
39/**
40 * NIST FIPS 203, Algorithm 5 (ByteEncode) for d == 12
41 */
42void byte_encode(BufferStuffer& bs, const KyberPolyNTT& p) {
44}
45
46/**
47 * NIST FIPS 203, Algorithm 6 (ByteDecode) for d < 12 in combination with
48 * Formula 4.8 (Decompress)
49 */
50template <size_t d>
51 requires(d < 12)
52void poly_decode_and_decompress(KyberPoly& p, BufferSlicer& bs) {
53 CRYSTALS::unpack<(1 << d) - 1>(p, bs, decompress<d>);
54}
55
56/**
57 * NIST FIPS 203, Algorithm 6 (ByteDecode) for d == 12
58 */
59void byte_decode(KyberPolyNTT& p, BufferSlicer& bs) {
61
62 if(!p.ct_validate_value_range(0, KyberConstants::Q - 1)) {
63 throw Decoding_Error("Decoded polynomial coefficients out of range");
64 }
65}
66
67/**
68 * NIST FIPS 203, Algorithm 7 (SampleNTT)
69 *
70 * Note that this assumes that the XOF has been initialized with the correct
71 * seed + two bytes of indices prior to invoking this function.
72 * See sample_matrix() below.
73 */
74void sample_ntt_uniform(KyberPolyNTT& p, XOF& xof) {
75 // A generator that returns the next coefficient sampled from the XOF. As the
76 // sampling uses half-bytes, this keeps track of the additionally sampled
77 // coefficient as needed.
78 auto sample = [stashed_coeff = std::optional<uint16_t>{},
79 bounded_xof =
81 auto lowerthan_q = [](uint32_t d) -> std::optional<uint16_t> {
82 if(d < KyberConstants::Q) {
83 return static_cast<uint16_t>(d);
84 } else {
85 return std::nullopt;
86 }
87 };
88
89 if(auto stashed = std::exchange(stashed_coeff, std::nullopt)) {
90 return *stashed; // value retained from a previous invocation
91 }
92
93 while(true) {
94 const auto [d1, d2] = bounded_xof.next<3>([&](const auto bytes) {
95 const auto x = load_le3(bytes);
96 return std::pair{lowerthan_q(x & 0x0FFF), lowerthan_q(x >> 12)};
97 });
98
99 if(d1.has_value()) {
100 stashed_coeff = d2; // keep candidate d2 for the next invocation
101 return *d1;
102 } else if(d2.has_value()) {
103 // d1 was invalid, d2 is valid, nothing to stash
104 return *d2;
105 }
106 }
107 };
108
109 for(auto& coeff : p) {
110 coeff = sample();
111 }
112}
113
114/**
115 * NIST FIPS 203, Algorithm 8 (SamplePolyCBD)
116 *
117 * Implementations for eta = 2 and eta = 3 are provided separately as template
118 * specializations below.
119 */
120template <KyberConstants::KyberEta eta>
121void sample_poly_cbd(KyberPoly& poly, StrongSpan<const KyberSamplingRandomness> randomness);
122
123/**
124 * NIST FIPS 203, Algorithm 8 (SamplePolyCBD) for eta = 2
125 */
126template <>
127void sample_poly_cbd<KyberConstants::KyberEta::_2>(KyberPoly& poly,
129 BufferSlicer bs(randomness);
130
131 for(size_t i = 0; i < poly.size() / 8; ++i) {
132 const uint32_t t = Botan::load_le(bs.take<4>());
133
134 // SWAR (SIMD within a Register) trick: calculate 16 2-bit-sums in parallel
135 constexpr uint32_t operand_bitmask = 0b01010101010101010101010101010101;
136
137 // clang-format off
138 const uint32_t d = ((t >> 0) & operand_bitmask) +
139 ((t >> 1) & operand_bitmask);
140 // clang-format on
141
142 for(size_t j = 0; j < 8; ++j) {
143 const int16_t a = (d >> (4 * j + 0)) & 0x3;
144 const int16_t b = (d >> (4 * j + 2)) & 0x3;
145 poly[8 * i + j] = a - b;
146 }
147 }
148
150}
151
152/**
153 * NIST FIPS 203, Algorithm 8 (SamplePolyCBD) for eta = 3
154 */
155template <>
156void sample_poly_cbd<KyberConstants::KyberEta::_3>(KyberPoly& poly,
158 BufferSlicer bs(randomness);
159
160 for(size_t i = 0; i < poly.size() / 4; ++i) {
161 const uint32_t t = load_le3(bs.take<3>());
162
163 // SWAR (SIMD within a Register) trick: calculate 8 3-bit-sums in parallel
164 constexpr uint32_t operand_bitmask = 0b00000000001001001001001001001001;
165
166 // clang-format off
167 const uint32_t d = ((t >> 0) & operand_bitmask) +
168 ((t >> 1) & operand_bitmask) +
169 ((t >> 2) & operand_bitmask);
170 // clang-format on
171
172 for(size_t j = 0; j < 4; ++j) {
173 const int16_t a = (d >> (6 * j + 0)) & 0x7;
174 const int16_t b = (d >> (6 * j + 3)) & 0x7;
175 poly[4 * i + j] = a - b;
176 }
177 }
178
180}
181
182} // namespace
183
184void encode_polynomial_vector(std::span<uint8_t> out, const KyberPolyVecNTT& vec) {
185 BufferStuffer bs(out);
186 for(auto& v : vec) {
187 byte_encode(bs, v);
188 }
190}
191
192KyberPolyVecNTT decode_polynomial_vector(std::span<const uint8_t> a, const KyberConstants& mode) {
193 KyberPolyVecNTT vec(mode.k());
194
195 BufferSlicer bs(a);
196 for(auto& p : vec) {
197 byte_decode(p, bs);
198 }
200
201 return vec;
202}
203
205 BOTAN_ASSERT(msg.size() == KyberConstants::N / 8, "message length must be N/8 bytes");
206 KyberPoly r;
207 BufferSlicer bs(msg);
208 poly_decode_and_decompress<1>(r, bs);
209 return r;
210}
211
213 KyberMessage result(p.size() / 8);
214 BufferStuffer bs(result);
215 poly_compress_and_encode<1>(bs, p);
216 return result;
217}
218
219namespace {
220
221template <size_t d>
222void polyvec_compress_and_encode(BufferStuffer& sink, const KyberPolyVec& polyvec) {
223 for(const auto& p : polyvec) {
224 poly_compress_and_encode<d>(sink, p);
225 }
226}
227
228void compress_polyvec(std::span<uint8_t> out, const KyberPolyVec& pv, const KyberConstants& mode) {
229 BufferStuffer bs(out);
230
231 switch(mode.d_u()) {
233 polyvec_compress_and_encode<10>(bs, pv);
234 BOTAN_ASSERT_NOMSG(bs.full());
235 return;
237 polyvec_compress_and_encode<11>(bs, pv);
238 BOTAN_ASSERT_NOMSG(bs.full());
239 return;
240 }
241
243}
244
245void compress_poly(std::span<uint8_t> out, const KyberPoly& p, const KyberConstants& mode) {
246 BufferStuffer bs(out);
247
248 switch(mode.d_v()) {
250 poly_compress_and_encode<4>(bs, p);
251 BOTAN_ASSERT_NOMSG(bs.full());
252 return;
254 poly_compress_and_encode<5>(bs, p);
255 BOTAN_ASSERT_NOMSG(bs.full());
256 return;
257 }
258
260}
261
262template <size_t d>
263void polyvec_decode_and_decompress(KyberPolyVec& polyvec, BufferSlicer& source) {
264 for(auto& p : polyvec) {
265 poly_decode_and_decompress<d>(p, source);
266 }
267}
268
269KyberPolyVec decompress_polynomial_vector(std::span<const uint8_t> buffer, const KyberConstants& mode) {
270 BOTAN_ASSERT(buffer.size() == mode.polynomial_vector_compressed_bytes(),
271 "unexpected length of compressed polynomial vector");
272
273 KyberPolyVec r(mode.k());
274 BufferSlicer bs(buffer);
275
276 switch(mode.d_u()) {
278 polyvec_decode_and_decompress<10>(r, bs);
279 BOTAN_ASSERT_NOMSG(bs.empty());
280 return r;
282 polyvec_decode_and_decompress<11>(r, bs);
283 BOTAN_ASSERT_NOMSG(bs.empty());
284 return r;
285 }
286
288}
289
290KyberPoly decompress_polynomial(std::span<const uint8_t> buffer, const KyberConstants& mode) {
291 BOTAN_ASSERT(buffer.size() == mode.polynomial_compressed_bytes(), "unexpected length of compressed polynomial");
292
293 KyberPoly r;
294 BufferSlicer bs(buffer);
295
296 switch(mode.d_v()) {
298 poly_decode_and_decompress<4>(r, bs);
299 BOTAN_ASSERT_NOMSG(bs.empty());
300 return r;
302 poly_decode_and_decompress<5>(r, bs);
303 BOTAN_ASSERT_NOMSG(bs.empty());
304 return r;
305 }
306
308}
309
310} // namespace
311
312/**
313 * NIST FIPS 203, Algorithms 16 (ML-KEM.KeyGen_internal), and
314 * 13 (K-PKE.KeyGen)
315 *
316 * In contrast to the specification, the expansion of rho and sigma is inlined
317 * with the actual PKE key generation. The sampling loops spelled out in
318 * FIPS 203 are hidden in the sample_* functions. The keys are kept in memory
319 * without serialization, which is deferred until requested.
320 */
322 BOTAN_ARG_CHECK(seed.d.has_value(), "Cannot expand keypair without the full private seed");
323 const auto& d = seed.d.value();
324
325 CT::poison(d);
326 auto [rho, sigma] = mode.symmetric_primitives().G(d, mode);
327 CT::unpoison(rho); // rho is public (seed for the public matrix A)
328
329 // Algorithm 13 (K-PKE.KeyGen) ----------------
330
331 auto A = Kyber_Algos::sample_matrix(rho, false /* not transposed */, mode);
332
333 // The nonce N is handled internally by the PolynomialSampler
335 auto s = ntt(ps.sample_polynomial_vector_cbd_eta1());
336 const auto e = ntt(ps.sample_polynomial_vector_cbd_eta1());
337
338 auto t = montgomery(A * s);
339 t += e;
340 t.reduce();
341
342 // End Algorithm 13 ---------------------------
343
344 CT::unpoison_all(d, t, s);
345
346 return {
347 std::make_shared<Kyber_PublicKeyInternal>(mode, std::move(t), std::move(rho)),
348 std::make_shared<Kyber_PrivateKeyInternal>(std::move(mode), std::move(s), std::move(seed)),
349 };
350}
351
353 const KyberPolyVec& u,
354 const KyberPoly& v,
355 const KyberConstants& m_mode) {
356 BufferStuffer bs(out);
357 compress_polyvec(bs.next(m_mode.polynomial_vector_compressed_bytes()), u, m_mode);
358 compress_poly(bs.next(m_mode.polynomial_compressed_bytes()), v, m_mode);
360}
361
363 const KyberConstants& mode) {
364 const size_t pvb = mode.polynomial_vector_compressed_bytes();
365 const size_t pcb = mode.polynomial_compressed_bytes();
366
367 // FIPS 203, Section 7.3 check 1 "Ciphertext type check"
368 if(ct.size() != pvb + pcb) {
369 throw Decoding_Error("Kyber: unexpected ciphertext length");
370 }
371
372 BufferSlicer bs(ct);
373 auto pv = bs.take(pvb);
374 auto p = bs.take(pcb);
376
377 return {decompress_polynomial_vector(pv, mode), decompress_polynomial(p, mode)};
378}
379
381 BOTAN_ASSERT(seed.size() == KyberConstants::SEED_BYTES, "unexpected seed size");
382
383 KyberPolyMat mat(mode.k(), mode.k());
384
385 for(uint8_t i = 0; i < mode.k(); ++i) {
386 for(uint8_t j = 0; j < mode.k(); ++j) {
387 const auto pos = (transposed) ? std::tuple(i, j) : std::tuple(j, i);
388 sample_ntt_uniform(mat[i][j], mode.symmetric_primitives().XOF(seed, pos));
389 }
390 }
391
392 return mat;
393}
394
395/**
396 * NIST FIPS 203, Algorithm 8 (SamplePolyCBD)
397 *
398 * The actual implementation is above. This just dispatches to the correct
399 * specialization based on the eta of the chosen mode.
400 */
403 const KyberSamplingRandomness& randomness) {
404 switch(eta) {
406 return sample_poly_cbd<KyberConstants::KyberEta::_2>(poly, randomness);
408 return sample_poly_cbd<KyberConstants::KyberEta::_3>(poly, randomness);
409 }
410
412}
413
414} // namespace Botan::Kyber_Algos
#define BOTAN_ASSERT_NOMSG(expr)
Definition assert.h:59
#define BOTAN_ARG_CHECK(expr, msg)
Definition assert.h:29
#define BOTAN_ASSERT(expr, assertion_made)
Definition assert.h:50
#define BOTAN_ASSERT_UNREACHABLE()
Definition assert.h:137
bool empty() const
Definition stl_util.h:129
std::span< const uint8_t > take(const size_t count)
Definition stl_util.h:98
Helper class to ease in-place marshalling of concatenated fixed-length values.
Definition stl_util.h:142
constexpr std::span< uint8_t > next(size_t bytes)
Definition stl_util.h:150
constexpr bool full() const
Definition stl_util.h:187
constexpr size_t size() const
Definition pqcrystals.h:274
size_t polynomial_vector_compressed_bytes() const
byte length of an encoded compressed polynomial vector
static constexpr T N
number of coefficients in a polynomial
size_t polynomial_compressed_bytes() const
byte length of an encoded compressed polynomial
static constexpr T Q
modulus
static constexpr size_t SEED_BYTES
Kyber_Symmetric_Primitives & symmetric_primitives() const
KyberPolyVec sample_polynomial_vector_cbd_eta1()
Definition kyber_algos.h:70
Botan::XOF & XOF(StrongSpan< const KyberSeedRho > seed, std::tuple< uint8_t, uint8_t > matrix_position) const
std::pair< KyberSeedRho, KyberSeedSigma > G(StrongSpan< const KyberSeedRandomness > seed, const KyberConstants &mode) const
decltype(auto) size() const noexcept(noexcept(this->m_span.size()))
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 void unpoison_all(Ts &&... ts)
Definition ct_utils.h:201
constexpr void unpoison(const T *p, size_t n)
Definition ct_utils.h:64
constexpr void poison(const T *p, size_t n)
Definition ct_utils.h:53
constexpr std::make_unsigned_t< KyberConstants::T > compress(KyberConstants::T x)
void encode_polynomial_vector(std::span< uint8_t > out, const KyberPolyVecNTT &vec)
KyberMessage polynomial_to_message(const KyberPoly &p)
KyberPolyMat sample_matrix(StrongSpan< const KyberSeedRho > seed, bool transposed, const KyberConstants &mode)
uint32_t load_le3(std::span< const uint8_t, 3 > in)
void compress_ciphertext(StrongSpan< KyberCompressedCiphertext > out, const KyberPolyVec &u, const KyberPoly &v, const KyberConstants &m_mode)
KyberInternalKeypair expand_keypair(KyberPrivateKeySeed seed, KyberConstants mode)
KyberPoly polynomial_from_message(StrongSpan< const KyberMessage > msg)
void sample_polynomial_from_cbd(KyberPoly &poly, KyberConstants::KyberEta eta, const KyberSamplingRandomness &randomness)
std::pair< KyberPolyVec, KyberPoly > decompress_ciphertext(StrongSpan< const KyberCompressedCiphertext > ct, const KyberConstants &mode)
constexpr KyberConstants::T decompress(std::make_unsigned_t< KyberConstants::T > x)
KyberPolyVecNTT decode_polynomial_vector(std::span< const uint8_t > a, const KyberConstants &mode)
constexpr T rho(T x)
Definition rotate.h:51
constexpr T sigma(T x)
Definition rotate.h:43
Botan::CRYSTALS::Polynomial< KyberPolyTraits, Botan::CRYSTALS::Domain::Normal > KyberPoly
Definition kyber_types.h:29
detail::Bounded_XOF< XOF &, bound > Bounded_XOF
constexpr auto load_le(ParamTs &&... params)
Definition loadstor.h:521
std::pair< std::shared_ptr< Kyber_PublicKeyInternal >, std::shared_ptr< Kyber_PrivateKeyInternal > > KyberInternalKeypair
Definition kyber_types.h:73
const SIMD_8x32 & b
Botan::CRYSTALS::PolynomialVector< KyberPolyTraits, Botan::CRYSTALS::Domain::Normal > KyberPolyVec
Definition kyber_types.h:30
std::optional< KyberSeedRandomness > d
Definition kyber_types.h:80