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