Coverage Report

Created: 2025-04-11 06:34

/src/botan/src/lib/pubkey/kyber/kyber_common/kyber_algos.cpp
Line
Count
Source (jump to first uncovered line)
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
25
namespace Botan::Kyber_Algos {
26
27
namespace {
28
29
/**
30
 * NIST FIPS 203, Algorithm 5 (ByteEncode) for d < 12 in combination with
31
 * Formula 4.7 (Compress)
32
 */
33
template <size_t d>
34
   requires(d < 12)
35
0
void poly_compress_and_encode(BufferStuffer& bs, const KyberPoly& p) {
36
0
   CRYSTALS::pack<(1 << d) - 1>(p, bs, compress<d>);
37
0
}
Unexecuted instantiation: kyber_algos.cpp:_ZN5Botan11Kyber_Algos12_GLOBAL__N_124poly_compress_and_encodeILm10EQltT_Li12EEEvRNS_13BufferStufferERKNS_8CRYSTALS10PolynomialINS_15KyberPolyTraitsELNS5_6DomainE0EEE
Unexecuted instantiation: kyber_algos.cpp:_ZN5Botan11Kyber_Algos12_GLOBAL__N_124poly_compress_and_encodeILm11EQltT_Li12EEEvRNS_13BufferStufferERKNS_8CRYSTALS10PolynomialINS_15KyberPolyTraitsELNS5_6DomainE0EEE
Unexecuted instantiation: kyber_algos.cpp:_ZN5Botan11Kyber_Algos12_GLOBAL__N_124poly_compress_and_encodeILm4EQltT_Li12EEEvRNS_13BufferStufferERKNS_8CRYSTALS10PolynomialINS_15KyberPolyTraitsELNS5_6DomainE0EEE
Unexecuted instantiation: kyber_algos.cpp:_ZN5Botan11Kyber_Algos12_GLOBAL__N_124poly_compress_and_encodeILm5EQltT_Li12EEEvRNS_13BufferStufferERKNS_8CRYSTALS10PolynomialINS_15KyberPolyTraitsELNS5_6DomainE0EEE
Unexecuted instantiation: kyber_algos.cpp:_ZN5Botan11Kyber_Algos12_GLOBAL__N_124poly_compress_and_encodeILm1EQltT_Li12EEEvRNS_13BufferStufferERKNS_8CRYSTALS10PolynomialINS_15KyberPolyTraitsELNS5_6DomainE0EEE
38
39
/**
40
 * NIST FIPS 203, Algorithm 5 (ByteEncode) for d == 12
41
 */
42
0
void byte_encode(BufferStuffer& bs, const KyberPolyNTT& p) {
43
0
   CRYSTALS::pack<KyberConstants::Q - 1>(p, bs);
44
0
}
45
46
/**
47
 * NIST FIPS 203, Algorithm 6 (ByteDecode) for d < 12 in combination with
48
 * Formula 4.8 (Decompress)
49
 */
50
template <size_t d>
51
   requires(d < 12)
52
0
void poly_decode_and_decompress(KyberPoly& p, BufferSlicer& bs) {
53
0
   CRYSTALS::unpack<(1 << d) - 1>(p, bs, decompress<d>);
54
0
}
Unexecuted instantiation: kyber_algos.cpp:_ZN5Botan11Kyber_Algos12_GLOBAL__N_126poly_decode_and_decompressILm10EQltT_Li12EEEvRNS_8CRYSTALS10PolynomialINS_15KyberPolyTraitsELNS3_6DomainE0EEERNS_12BufferSlicerE
Unexecuted instantiation: kyber_algos.cpp:_ZN5Botan11Kyber_Algos12_GLOBAL__N_126poly_decode_and_decompressILm11EQltT_Li12EEEvRNS_8CRYSTALS10PolynomialINS_15KyberPolyTraitsELNS3_6DomainE0EEERNS_12BufferSlicerE
Unexecuted instantiation: kyber_algos.cpp:_ZN5Botan11Kyber_Algos12_GLOBAL__N_126poly_decode_and_decompressILm4EQltT_Li12EEEvRNS_8CRYSTALS10PolynomialINS_15KyberPolyTraitsELNS3_6DomainE0EEERNS_12BufferSlicerE
Unexecuted instantiation: kyber_algos.cpp:_ZN5Botan11Kyber_Algos12_GLOBAL__N_126poly_decode_and_decompressILm5EQltT_Li12EEEvRNS_8CRYSTALS10PolynomialINS_15KyberPolyTraitsELNS3_6DomainE0EEERNS_12BufferSlicerE
Unexecuted instantiation: kyber_algos.cpp:_ZN5Botan11Kyber_Algos12_GLOBAL__N_126poly_decode_and_decompressILm1EQltT_Li12EEEvRNS_8CRYSTALS10PolynomialINS_15KyberPolyTraitsELNS3_6DomainE0EEERNS_12BufferSlicerE
55
56
/**
57
 * NIST FIPS 203, Algorithm 6 (ByteDecode) for d == 12
58
 */
59
0
void byte_decode(KyberPolyNTT& p, BufferSlicer& bs) {
60
0
   CRYSTALS::unpack<KyberConstants::Q - 1>(p, bs);
61
62
0
   if(!p.ct_validate_value_range(0, KyberConstants::Q - 1)) {
63
0
      throw Decoding_Error("Decoded polynomial coefficients out of range");
64
0
   }
65
0
}
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
 */
74
0
void 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
0
   auto sample = [stashed_coeff = std::optional<uint16_t>{},
79
0
                  bounded_xof =
80
0
                     Bounded_XOF<KyberConstants::SAMPLE_NTT_POLY_FROM_XOF_BOUND>(xof)]() mutable -> uint16_t {
81
0
      auto lowerthan_q = [](uint32_t d) -> std::optional<uint16_t> {
82
0
         if(d < KyberConstants::Q) {
83
0
            return static_cast<uint16_t>(d);
84
0
         } else {
85
0
            return std::nullopt;
86
0
         }
87
0
      };
88
89
0
      if(auto stashed = std::exchange(stashed_coeff, std::nullopt)) {
90
0
         return *stashed;  // value retained from a previous invocation
91
0
      }
92
93
0
      while(true) {
94
0
         const auto [d1, d2] = bounded_xof.next<3>([&](const auto bytes) {
95
0
            const auto x = load_le3(bytes);
96
0
            return std::pair{lowerthan_q(x & 0x0FFF), lowerthan_q(x >> 12)};
97
0
         });
98
99
0
         if(d1.has_value()) {
100
0
            stashed_coeff = d2;  // keep candidate d2 for the next invocation
101
0
            return *d1;
102
0
         } else if(d2.has_value()) {
103
            // d1 was invalid, d2 is valid, nothing to stash
104
0
            return *d2;
105
0
         }
106
0
      }
107
0
   };
108
109
0
   for(auto& coeff : p) {
110
0
      coeff = sample();
111
0
   }
112
0
}
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
 */
120
template <KyberConstants::KyberEta eta>
121
void sample_poly_cbd(KyberPoly& poly, StrongSpan<const KyberSamplingRandomness> randomness);
122
123
/**
124
 * NIST FIPS 203, Algorithm 8 (SamplePolyCBD) for eta = 2
125
 */
126
template <>
127
void sample_poly_cbd<KyberConstants::KyberEta::_2>(KyberPoly& poly,
128
0
                                                   StrongSpan<const KyberSamplingRandomness> randomness) {
129
0
   BufferSlicer bs(randomness);
130
131
0
   for(size_t i = 0; i < poly.size() / 8; ++i) {
132
0
      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
0
      constexpr uint32_t operand_bitmask = 0b01010101010101010101010101010101;
136
137
      // clang-format off
138
0
      const uint32_t d = ((t >> 0) & operand_bitmask) +
139
0
                         ((t >> 1) & operand_bitmask);
140
      // clang-format on
141
142
0
      for(size_t j = 0; j < 8; ++j) {
143
0
         const int16_t a = (d >> (4 * j + 0)) & 0x3;
144
0
         const int16_t b = (d >> (4 * j + 2)) & 0x3;
145
0
         poly[8 * i + j] = a - b;
146
0
      }
147
0
   }
148
149
0
   BOTAN_ASSERT_NOMSG(bs.empty());
150
0
}
151
152
/**
153
 * NIST FIPS 203, Algorithm 8 (SamplePolyCBD) for eta = 3
154
 */
155
template <>
156
void sample_poly_cbd<KyberConstants::KyberEta::_3>(KyberPoly& poly,
157
0
                                                   StrongSpan<const KyberSamplingRandomness> randomness) {
158
0
   BufferSlicer bs(randomness);
159
160
0
   for(size_t i = 0; i < poly.size() / 4; ++i) {
161
0
      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
0
      constexpr uint32_t operand_bitmask = 0b00000000001001001001001001001001;
165
166
      // clang-format off
167
0
      const uint32_t d = ((t >> 0) & operand_bitmask) +
168
0
                         ((t >> 1) & operand_bitmask) +
169
0
                         ((t >> 2) & operand_bitmask);
170
      // clang-format on
171
172
0
      for(size_t j = 0; j < 4; ++j) {
173
0
         const int16_t a = (d >> (6 * j + 0)) & 0x7;
174
0
         const int16_t b = (d >> (6 * j + 3)) & 0x7;
175
0
         poly[4 * i + j] = a - b;
176
0
      }
177
0
   }
178
179
0
   BOTAN_ASSERT_NOMSG(bs.empty());
180
0
}
181
182
}  // namespace
183
184
0
void encode_polynomial_vector(std::span<uint8_t> out, const KyberPolyVecNTT& vec) {
185
0
   BufferStuffer bs(out);
186
0
   for(auto& v : vec) {
187
0
      byte_encode(bs, v);
188
0
   }
189
0
   BOTAN_ASSERT_NOMSG(bs.full());
190
0
}
191
192
0
KyberPolyVecNTT decode_polynomial_vector(std::span<const uint8_t> a, const KyberConstants& mode) {
193
0
   KyberPolyVecNTT vec(mode.k());
194
195
0
   BufferSlicer bs(a);
196
0
   for(auto& p : vec) {
197
0
      byte_decode(p, bs);
198
0
   }
199
0
   BOTAN_ASSERT_NOMSG(bs.empty());
200
201
0
   return vec;
202
0
}
203
204
0
KyberPoly polynomial_from_message(StrongSpan<const KyberMessage> msg) {
205
0
   BOTAN_ASSERT(msg.size() == KyberConstants::N / 8, "message length must be N/8 bytes");
206
0
   KyberPoly r;
207
0
   BufferSlicer bs(msg);
208
0
   poly_decode_and_decompress<1>(r, bs);
209
0
   return r;
210
0
}
211
212
0
KyberMessage polynomial_to_message(const KyberPoly& p) {
213
0
   KyberMessage result(p.size() / 8);
214
0
   BufferStuffer bs(result);
215
0
   poly_compress_and_encode<1>(bs, p);
216
0
   return result;
217
0
}
218
219
namespace {
220
221
template <size_t d>
222
0
void polyvec_compress_and_encode(BufferStuffer& sink, const KyberPolyVec& polyvec) {
223
0
   for(const auto& p : polyvec) {
224
0
      poly_compress_and_encode<d>(sink, p);
225
0
   }
226
0
}
Unexecuted instantiation: kyber_algos.cpp:void Botan::Kyber_Algos::(anonymous namespace)::polyvec_compress_and_encode<10ul>(Botan::BufferStuffer&, Botan::CRYSTALS::PolynomialVector<Botan::KyberPolyTraits, (Botan::CRYSTALS::Domain)0> const&)
Unexecuted instantiation: kyber_algos.cpp:void Botan::Kyber_Algos::(anonymous namespace)::polyvec_compress_and_encode<11ul>(Botan::BufferStuffer&, Botan::CRYSTALS::PolynomialVector<Botan::KyberPolyTraits, (Botan::CRYSTALS::Domain)0> const&)
227
228
0
void compress_polyvec(std::span<uint8_t> out, const KyberPolyVec& pv, const KyberConstants& mode) {
229
0
   BufferStuffer bs(out);
230
231
0
   switch(mode.d_u()) {
232
0
      case KyberConstants::KyberDu::_10:
233
0
         polyvec_compress_and_encode<10>(bs, pv);
234
0
         BOTAN_ASSERT_NOMSG(bs.full());
235
0
         return;
236
0
      case KyberConstants::KyberDu::_11:
237
0
         polyvec_compress_and_encode<11>(bs, pv);
238
0
         BOTAN_ASSERT_NOMSG(bs.full());
239
0
         return;
240
0
   }
241
242
0
   BOTAN_ASSERT_UNREACHABLE();
243
0
}
244
245
0
void compress_poly(std::span<uint8_t> out, const KyberPoly& p, const KyberConstants& mode) {
246
0
   BufferStuffer bs(out);
247
248
0
   switch(mode.d_v()) {
249
0
      case KyberConstants::KyberDv::_4:
250
0
         poly_compress_and_encode<4>(bs, p);
251
0
         BOTAN_ASSERT_NOMSG(bs.full());
252
0
         return;
253
0
      case KyberConstants::KyberDv::_5:
254
0
         poly_compress_and_encode<5>(bs, p);
255
0
         BOTAN_ASSERT_NOMSG(bs.full());
256
0
         return;
257
0
   }
258
259
0
   BOTAN_ASSERT_UNREACHABLE();
260
0
}
261
262
template <size_t d>
263
0
void polyvec_decode_and_decompress(KyberPolyVec& polyvec, BufferSlicer& source) {
264
0
   for(auto& p : polyvec) {
265
0
      poly_decode_and_decompress<d>(p, source);
266
0
   }
267
0
}
Unexecuted instantiation: kyber_algos.cpp:void Botan::Kyber_Algos::(anonymous namespace)::polyvec_decode_and_decompress<10ul>(Botan::CRYSTALS::PolynomialVector<Botan::KyberPolyTraits, (Botan::CRYSTALS::Domain)0>&, Botan::BufferSlicer&)
Unexecuted instantiation: kyber_algos.cpp:void Botan::Kyber_Algos::(anonymous namespace)::polyvec_decode_and_decompress<11ul>(Botan::CRYSTALS::PolynomialVector<Botan::KyberPolyTraits, (Botan::CRYSTALS::Domain)0>&, Botan::BufferSlicer&)
268
269
0
KyberPolyVec decompress_polynomial_vector(std::span<const uint8_t> buffer, const KyberConstants& mode) {
270
0
   BOTAN_ASSERT(buffer.size() == mode.polynomial_vector_compressed_bytes(),
271
0
                "unexpected length of compressed polynomial vector");
272
273
0
   KyberPolyVec r(mode.k());
274
0
   BufferSlicer bs(buffer);
275
276
0
   switch(mode.d_u()) {
277
0
      case KyberConstants::KyberDu::_10:
278
0
         polyvec_decode_and_decompress<10>(r, bs);
279
0
         BOTAN_ASSERT_NOMSG(bs.empty());
280
0
         return r;
281
0
      case KyberConstants::KyberDu::_11:
282
0
         polyvec_decode_and_decompress<11>(r, bs);
283
0
         BOTAN_ASSERT_NOMSG(bs.empty());
284
0
         return r;
285
0
   }
286
287
0
   BOTAN_ASSERT_UNREACHABLE();
288
0
}
289
290
0
KyberPoly decompress_polynomial(std::span<const uint8_t> buffer, const KyberConstants& mode) {
291
0
   BOTAN_ASSERT(buffer.size() == mode.polynomial_compressed_bytes(), "unexpected length of compressed polynomial");
292
293
0
   KyberPoly r;
294
0
   BufferSlicer bs(buffer);
295
296
0
   switch(mode.d_v()) {
297
0
      case KyberConstants::KyberDv::_4:
298
0
         poly_decode_and_decompress<4>(r, bs);
299
0
         BOTAN_ASSERT_NOMSG(bs.empty());
300
0
         return r;
301
0
      case KyberConstants::KyberDv::_5:
302
0
         poly_decode_and_decompress<5>(r, bs);
303
0
         BOTAN_ASSERT_NOMSG(bs.empty());
304
0
         return r;
305
0
   }
306
307
0
   BOTAN_ASSERT_UNREACHABLE();
308
0
}
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
 */
321
0
KyberInternalKeypair expand_keypair(KyberPrivateKeySeed seed, KyberConstants mode) {
322
0
   BOTAN_ARG_CHECK(seed.d.has_value(), "Cannot expand keypair without the full private seed");
323
0
   const auto& d = seed.d.value();
324
325
0
   CT::poison(d);
326
0
   auto [rho, sigma] = mode.symmetric_primitives().G(d, mode);
327
0
   CT::unpoison(rho);  // rho is public (seed for the public matrix A)
328
329
   // Algorithm 13 (K-PKE.KeyGen) ----------------
330
331
0
   auto A = Kyber_Algos::sample_matrix(rho, false /* not transposed */, mode);
332
333
   // The nonce N is handled internally by the PolynomialSampler
334
0
   Kyber_Algos::PolynomialSampler ps(sigma, mode);
335
0
   auto s = ntt(ps.sample_polynomial_vector_cbd_eta1());
336
0
   const auto e = ntt(ps.sample_polynomial_vector_cbd_eta1());
337
338
0
   auto t = montgomery(A * s);
339
0
   t += e;
340
0
   t.reduce();
341
342
   // End Algorithm 13 ---------------------------
343
344
0
   CT::unpoison_all(d, t, s);
345
346
0
   return {
347
0
      std::make_shared<Kyber_PublicKeyInternal>(mode, std::move(t), std::move(rho)),
348
0
      std::make_shared<Kyber_PrivateKeyInternal>(std::move(mode), std::move(s), std::move(seed)),
349
0
   };
350
0
}
351
352
void compress_ciphertext(StrongSpan<KyberCompressedCiphertext> out,
353
                         const KyberPolyVec& u,
354
                         const KyberPoly& v,
355
0
                         const KyberConstants& m_mode) {
356
0
   BufferStuffer bs(out);
357
0
   compress_polyvec(bs.next(m_mode.polynomial_vector_compressed_bytes()), u, m_mode);
358
0
   compress_poly(bs.next(m_mode.polynomial_compressed_bytes()), v, m_mode);
359
0
   BOTAN_ASSERT_NOMSG(bs.full());
360
0
}
361
362
std::pair<KyberPolyVec, KyberPoly> decompress_ciphertext(StrongSpan<const KyberCompressedCiphertext> ct,
363
0
                                                         const KyberConstants& mode) {
364
0
   const size_t pvb = mode.polynomial_vector_compressed_bytes();
365
0
   const size_t pcb = mode.polynomial_compressed_bytes();
366
367
   // FIPS 203, Section 7.3 check 1 "Ciphertext type check"
368
0
   if(ct.size() != pvb + pcb) {
369
0
      throw Decoding_Error("Kyber: unexpected ciphertext length");
370
0
   }
371
372
0
   BufferSlicer bs(ct);
373
0
   auto pv = bs.take(pvb);
374
0
   auto p = bs.take(pcb);
375
0
   BOTAN_ASSERT_NOMSG(bs.empty());
376
377
0
   return {decompress_polynomial_vector(pv, mode), decompress_polynomial(p, mode)};
378
0
}
379
380
0
KyberPolyMat sample_matrix(StrongSpan<const KyberSeedRho> seed, bool transposed, const KyberConstants& mode) {
381
0
   BOTAN_ASSERT(seed.size() == KyberConstants::SEED_BYTES, "unexpected seed size");
382
383
0
   KyberPolyMat mat(mode.k(), mode.k());
384
385
0
   for(uint8_t i = 0; i < mode.k(); ++i) {
386
0
      for(uint8_t j = 0; j < mode.k(); ++j) {
387
0
         const auto pos = (transposed) ? std::tuple(i, j) : std::tuple(j, i);
388
0
         sample_ntt_uniform(mat[i][j], mode.symmetric_primitives().XOF(seed, pos));
389
0
      }
390
0
   }
391
392
0
   return mat;
393
0
}
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
 */
401
void sample_polynomial_from_cbd(KyberPoly& poly,
402
                                KyberConstants::KyberEta eta,
403
0
                                const KyberSamplingRandomness& randomness) {
404
0
   switch(eta) {
405
0
      case KyberConstants::KyberEta::_2:
406
0
         return sample_poly_cbd<KyberConstants::KyberEta::_2>(poly, randomness);
407
0
      case KyberConstants::KyberEta::_3:
408
0
         return sample_poly_cbd<KyberConstants::KyberEta::_3>(poly, randomness);
409
0
   }
410
411
0
   BOTAN_ASSERT_UNREACHABLE();
412
0
}
413
414
}  // namespace Botan::Kyber_Algos