• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

randombit / botan / 19012754211

02 Nov 2025 01:10PM UTC coverage: 90.677% (+0.006%) from 90.671%
19012754211

push

github

web-flow
Merge pull request #5137 from randombit/jack/clang-tidy-includes

Remove various unused includes flagged by clang-tidy misc-include-cleaner

100457 of 110786 relevant lines covered (90.68%)

12189873.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.84
/src/lib/pubkey/dilithium/dilithium_common/dilithium_algos.cpp
1
/*
2
 * Crystals Dilithium Internal Algorithms (aka. "Auxiliary Functions")
3
 *
4
 * This implements the auxiliary functions of the Crystals Dilithium signature
5
 * scheme as specified in NIST FIPS 204, Chapter 7.
6
 *
7
 * Some implementations are based on the public domain reference implementation
8
 * by the designers (https://github.com/pq-crystals/dilithium)
9
 *
10
 * (C) 2021-2024 Jack Lloyd
11
 * (C) 2021-2022 Manuel Glaser and Michael Boric, Rohde & Schwarz Cybersecurity
12
 * (C) 2021-2022 René Meusel and Hannes Rantzsch, neXenio GmbH
13
 * (C) 2024 Fabian Albert and René Meusel, Rohde & Schwarz Cybersecurity
14
 *
15
 * Botan is released under the Simplified BSD License (see license.txt)
16
 */
17

18
#include <botan/internal/dilithium_algos.h>
19

20
#include <botan/internal/bit_ops.h>
21
#include <botan/internal/ct_utils.h>
22
#include <botan/internal/dilithium_keys.h>
23
#include <botan/internal/dilithium_symmetric_primitives.h>
24
#include <botan/internal/loadstor.h>
25
#include <botan/internal/pqcrystals_encoding.h>
26
#include <botan/internal/pqcrystals_helpers.h>
27
#include <botan/internal/stl_util.h>
28

29
#include <utility>
30

31
namespace Botan::Dilithium_Algos {
32

33
namespace {
34

35
/**
36
 * Returns an all-one mask if @p x is negative, otherwise an all-zero mask.
37
 */
38
template <std::signed_integral T>
39
constexpr auto is_negative_mask(T x) {
47,492,449✔
40
   using unsigned_T = std::make_unsigned_t<T>;
41
   return CT::Mask<unsigned_T>::expand_top_bit(static_cast<unsigned_T>(x));
47,492,449✔
42
}
43

44
template <DilithiumConstants::T b>
45
constexpr std::make_unsigned_t<DilithiumConstants::T> map_range(DilithiumConstants::T c) {
7,865,344✔
46
   // NIST FIPS 204, Algorithm 17 (BitPack)
47
   //   c is in range [-a, b] and must be mapped to [0, a + b] as follows:
48
   BOTAN_DEBUG_ASSERT(b - c >= 0);
7,865,344✔
49
   return b - c;
7,865,344✔
50
}
51

52
template <DilithiumConstants::T b>
53
constexpr DilithiumConstants::T unmap_range(std::make_unsigned_t<DilithiumConstants::T> c) {
20,743,424✔
54
   // NIST FIPS 204, Algorithm 19 (BitUnpack)
55
   //   c is in range [0, a + b] and must be mapped to [-a, b] as follows:
56
   return static_cast<DilithiumConstants::T>(b - c);
20,743,424✔
57
}
58

59
template <DilithiumConstants::T a, DilithiumConstants::T b, CRYSTALS::crystals_trait PolyTrait, CRYSTALS::Domain D>
60
constexpr void poly_pack(const CRYSTALS::Polynomial<PolyTrait, D>& p, BufferStuffer& stuffer) {
156,372✔
61
   if constexpr(a == 0) {
62
      // If `a` is 0, we assume SimpleBitPack (Algorithm 16) where the
63
      // coefficients are in the range [0, b].
64
      CRYSTALS::pack<b>(p, stuffer);
125,648✔
65
   } else {
66
      // Otherwise, for BitPack (Algorithm 17), we must map the coefficients to
67
      // positive values as they are in the range [-a, b].
68
      CRYSTALS::pack<a + b>(p, stuffer, map_range<b>);
30,724✔
69
   }
70
}
111,774✔
71

72
template <DilithiumConstants::T a,
73
          DilithiumConstants::T b,
74
          CRYSTALS::byte_source ByteSourceT,
75
          CRYSTALS::crystals_trait PolyTrait,
76
          CRYSTALS::Domain D>
77
constexpr void poly_unpack(CRYSTALS::Polynomial<PolyTrait, D>& p, ByteSourceT& get_bytes, bool check_range = false) {
91,511✔
78
   if constexpr(a == 0) {
79
      // If `a` is 0, we assume SimpleBitUnpack (Algorithm 18) where the
80
      // coefficients are in the range [0, b].
81
      CRYSTALS::unpack<b>(p, get_bytes);
10,482✔
82
   } else {
83
      // Otherwise, BitUnpack (Algorithm  19) must map the unpacked coefficients
84
      // to the range [-a, b].
85
      CRYSTALS::unpack<a + b>(p, get_bytes, unmap_range<b>);
81,029✔
86
   }
87

88
   // `check_range` should only be enabled if the requested range is not fully
89
   // covered by the encodeable range, i.e |range| is not a power of 2.
90
   BOTAN_DEBUG_ASSERT(!check_range ||
91,511✔
91
                      (a >= 0 && b >= 0 && !is_power_of_2(static_cast<uint64_t>(b) - static_cast<uint64_t>(a) + 1)));
92

93
   if(check_range && !p.ct_validate_value_range(-a, b)) {
91,511✔
94
      throw Decoding_Error("Decoded polynomial coefficients out of range");
×
95
   }
96
}
91,511✔
97

98
/**
99
 * NIST FIPS 204, Algorithm 16 (SimpleBitPack)
100
 * (for a = 2^(bitlen(q-1)-d) - 1)
101
 */
102
void poly_pack_t1(const DilithiumPoly& p, BufferStuffer& stuffer) {
36,756✔
103
   constexpr auto b = (1 << (bitlen(DilithiumConstants::Q - 1) - DilithiumConstants::D)) - 1;
36,756✔
104
   poly_pack<0, b>(p, stuffer);
73,512✔
105
}
36,756✔
106

107
/**
108
 * NIST FIPS 204, Algorithm 16 (SimpleBitPack)
109
 * (for a = (q-1)/(2*gamma2-1))
110
 */
111
void poly_pack_w1(const DilithiumPoly& p, BufferStuffer& stuffer, const DilithiumConstants& mode) {
88,892✔
112
   using Gamma2 = DilithiumConstants::DilithiumGamma2;
88,892✔
113
   auto calculate_b = [](auto gamma2) { return ((DilithiumConstants::Q - 1) / (2 * gamma2)) - 1; };
88,892✔
114
   switch(mode.gamma2()) {
88,892✔
115
      case Gamma2::Qminus1DividedBy88:
19,452✔
116
         return poly_pack<0, calculate_b(Gamma2::Qminus1DividedBy88)>(p, stuffer);
19,452✔
117
      case Gamma2::Qminus1DividedBy32:
69,440✔
118
         return poly_pack<0, calculate_b(Gamma2::Qminus1DividedBy32)>(p, stuffer);
69,440✔
119
   }
120

121
   BOTAN_ASSERT_UNREACHABLE();
×
122
}
123

124
/**
125
 * NIST FIPS 204, Algorithm 17 (BitPack)
126
 * (for a = -gamma1 - 1, b = gamma1)
127
 */
128
void poly_pack_gamma1(const DilithiumPoly& p, BufferStuffer& stuffer, const DilithiumConstants& mode) {
8,070✔
129
   using Gamma1 = DilithiumConstants::DilithiumGamma1;
8,070✔
130
   switch(mode.gamma1()) {
8,070✔
131
      case Gamma1::ToThe17th:
1,904✔
132
         return poly_pack<Gamma1::ToThe17th - 1, Gamma1::ToThe17th>(p, stuffer);
1,904✔
133
      case Gamma1::ToThe19th:
6,166✔
134
         return poly_pack<Gamma1::ToThe19th - 1, Gamma1::ToThe19th>(p, stuffer);
6,166✔
135
   }
136

137
   BOTAN_ASSERT_UNREACHABLE();
×
138
}
139

140
#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
141

142
/**
143
 * NIST FIPS 204, Algorithm 17 (BitPack)
144
 * (for a = -eta, b = eta)
145
 */
146
void poly_pack_eta(const DilithiumPoly& p, BufferStuffer& stuffer, const DilithiumConstants& mode) {
14,812✔
147
   using Eta = DilithiumConstants::DilithiumEta;
14,812✔
148
   switch(mode.eta()) {
14,812✔
149
      case Eta::_2:
10,027✔
150
         return poly_pack<Eta::_2, Eta::_2>(p, stuffer);
10,027✔
151
      case Eta::_4:
4,785✔
152
         return poly_pack<Eta::_4, Eta::_4>(p, stuffer);
4,785✔
153
   }
154

155
   BOTAN_ASSERT_UNREACHABLE();
×
156
}
157

158
/**
159
 * NIST FIPS 204, Algorithm 17 (BitPack)
160
 * (for a = -2^(d-1) - 1, b = 2^(d-1))
161
 */
162
void poly_pack_t0(const DilithiumPoly& p, BufferStuffer& stuffer) {
7,842✔
163
   constexpr auto TwoToTheDminus1 = 1 << (DilithiumConstants::D - 1);
7,842✔
164
   poly_pack<TwoToTheDminus1 - 1, TwoToTheDminus1>(p, stuffer);
15,684✔
165
}
7,842✔
166

167
#endif
168

169
/**
170
 * NIST FIPS 204, Algorithm 18 (SimpleBitUnpack)
171
 * (for a = 2^(bitlen(q-1)-d) - 1)
172
 */
173
void poly_unpack_t1(DilithiumPoly& p, BufferSlicer& slicer) {
10,482✔
174
   constexpr auto b = (1 << (bitlen(DilithiumConstants::Q - 1) - DilithiumConstants::D)) - 1;
10,482✔
175
   // The range of valid output coefficients [0, b] fully covers the encodeable
176
   // range. Hence, no range check is needed despite this being exposed to
177
   // potentially untrusted serialized public keys.
178
   static_assert(b >= 0 && is_power_of_2(static_cast<uint32_t>(b) + 1));
10,482✔
179
   poly_unpack<0, b>(p, slicer);
10,482✔
180
}
10,482✔
181

182
/**
183
 * NIST FIPS 204, Algorithm 19 (BitUnpack)
184
 * (for a = -gamma1 - 1, b = gamma1)
185
 */
186
template <typename ByteSourceT>
187
void poly_unpack_gamma1(DilithiumPoly& p, ByteSourceT& byte_source, const DilithiumConstants& mode) {
79,840✔
188
   using Gamma1 = DilithiumConstants::DilithiumGamma1;
189
   switch(mode.gamma1()) {
79,840✔
190
      case Gamma1::ToThe17th:
19,716✔
191
         return poly_unpack<Gamma1::ToThe17th - 1, Gamma1::ToThe17th>(p, byte_source);
19,716✔
192
      case Gamma1::ToThe19th:
60,124✔
193
         return poly_unpack<Gamma1::ToThe19th - 1, Gamma1::ToThe19th>(p, byte_source);
60,124✔
194
   }
195

196
   BOTAN_ASSERT_UNREACHABLE();
×
197
}
198

199
#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
200

201
/**
202
 * NIST FIPS 204, Algorithm 19 (BitUnpack)
203
 * (for a = -eta, b = eta)
204
 */
205
void poly_unpack_eta(DilithiumPoly& p, BufferSlicer& slicer, const DilithiumConstants& mode, bool check_range = false) {
777✔
206
   using Eta = DilithiumConstants::DilithiumEta;
777✔
207
   switch(mode.eta()) {
777✔
208
      case Eta::_2:
535✔
209
         return poly_unpack<Eta::_2, Eta::_2>(p, slicer, check_range);
535✔
210
      case Eta::_4:
242✔
211
         return poly_unpack<Eta::_4, Eta::_4>(p, slicer, check_range);
242✔
212
   }
213

214
   BOTAN_ASSERT_UNREACHABLE();
×
215
}
216

217
/**
218
 * NIST FIPS 204, Algorithm 19 (BitUnpack)
219
 * (for a = -2^(d-1) - 1, b = 2^(d-1))
220
 */
221
void poly_unpack_t0(DilithiumPoly& p, BufferSlicer& slicer) {
412✔
222
   constexpr auto TwoToTheDminus1 = 1 << (DilithiumConstants::D - 1);
412✔
223
   poly_unpack<TwoToTheDminus1 - 1, TwoToTheDminus1>(p, slicer);
412✔
224
}
412✔
225

226
#endif
227

228
/**
229
 * NIST FIPS 204, Algorithm 20 (HintBitPack)
230
 */
231
void hint_pack(const DilithiumPolyVec& h, BufferStuffer& stuffer, const DilithiumConstants& mode) {
1,518✔
232
   BOTAN_ASSERT_NOMSG(h.size() == mode.k());
1,518✔
233
   BOTAN_DEBUG_ASSERT(h.ct_validate_value_range(0, 1));
1,518✔
234

235
   BufferStuffer bit_positions(stuffer.next(mode.omega()));
1,518✔
236
   BufferStuffer offsets(stuffer.next(mode.k()));
1,518✔
237

238
   uint8_t index = 0;
1,518✔
239
   for(const auto& p : h) {
10,630✔
240
      for(size_t i = 0; i < p.size(); ++i) {
2,341,784✔
241
         if(p[i] == 1) {
2,332,672✔
242
            bit_positions.append(static_cast<uint8_t>(i));
78,751✔
243
            ++index;
78,751✔
244
         }
245
      }
246
      offsets.append(index);
9,112✔
247
   }
248

249
   // Fill the remaining bit positions with zeros
250
   bit_positions.append(0, bit_positions.remaining_capacity());
1,518✔
251
}
1,518✔
252

253
/**
254
 * NIST FIPS 204, Algorithm 21 (HintBitUnpack)
255
 */
256
std::optional<DilithiumPolyVec> hint_unpack(BufferSlicer& slicer, const DilithiumConstants& mode) {
8,328✔
257
   BufferSlicer bit_positions(slicer.take(mode.omega()));
8,328✔
258
   BufferSlicer offsets(slicer.take(mode.k()));
8,328✔
259

260
   DilithiumPolyVec hint(mode.k());
8,328✔
261
   uint8_t index = 0;
8,328✔
262
   for(auto& p : hint) {
57,950✔
263
      const auto end_index = offsets.take_byte();
49,632✔
264

265
      // Check the bounds of the end index for this polynomial
266
      if(end_index < index || end_index > mode.omega()) {
49,632✔
267
         return std::nullopt;
10✔
268
      }
269

270
      const auto set_bits = bit_positions.take(end_index - index);
49,629✔
271

272
      // Check that the set bit positions are ordered (strong unforgeability)
273
      // TODO: explicitly add a test for this, Whycheproof perhaps?
274
      for(size_t i = 1; i < set_bits.size(); ++i) {
420,946✔
275
         if(set_bits[i] <= set_bits[i - 1]) {
371,324✔
276
            return std::nullopt;
7✔
277
         }
278
      }
279

280
      // Set the specified bits in the polynomial
281
      for(const auto i : set_bits) {
469,096✔
282
         p[i] = 1;
419,474✔
283
      }
284

285
      index = end_index;
49,622✔
286
   }
287

288
   // Check that the remaining bit positions are all zero (strong unforgeability)
289
   const auto remaining = bit_positions.take(bit_positions.remaining());
8,318✔
290
   if(!std::all_of(remaining.begin(), remaining.end(), [](auto b) { return b == 0; })) {
8,318✔
291
      return std::nullopt;
5✔
292
   }
293

294
   BOTAN_DEBUG_ASSERT(hint.ct_validate_value_range(0, 1));
8,313✔
295
   return hint;
8,313✔
296
}
8,328✔
297

298
/**
299
 * NIST FIPS 204, Algorithm 6, lines 5-7 (ML-DSA.KeyGen_internal)
300
 *
301
 * We have to expose this independently to derive the public key from the
302
 * private key when loading a key pair from a serialized private key. This
303
 * is needed because of Botan's design decision to let the private key
304
 * class inherit from the public key class.
305
 *
306
 * TODO(Botan4): This should be refactored after PrivateKey does not inherit
307
 *               from PublicKey anymore.
308
 */
309
std::pair<DilithiumPolyVec, DilithiumPolyVec> compute_t1_and_t0(const DilithiumPolyMatNTT& A,
1,521✔
310
                                                                const DilithiumPolyVec& s1,
311
                                                                const DilithiumPolyVec& s2) {
312
   auto t_hat = A * ntt(s1.clone());
1,521✔
313
   t_hat.reduce();
1,521✔
314
   auto t = inverse_ntt(std::move(t_hat));
1,521✔
315
   t += s2;
1,521✔
316
   t.conditional_add_q();
1,521✔
317

318
   return Dilithium_Algos::power2round(t);
3,042✔
319
}
1,521✔
320

321
}  // namespace
322

323
/**
324
 * NIST FIPS 204, Algorithm 22 (pkEncode)
325
 */
326
DilithiumSerializedPublicKey encode_public_key(StrongSpan<const DilithiumSeedRho> rho,
6,124✔
327
                                               const DilithiumPolyVec& t1,
328
                                               const DilithiumConstants& mode) {
329
   DilithiumSerializedPublicKey pk(mode.public_key_bytes());
6,124✔
330
   BufferStuffer stuffer(pk);
6,124✔
331

332
   stuffer.append(rho);
6,124✔
333
   for(const auto& p : t1) {
42,880✔
334
      poly_pack_t1(p, stuffer);
73,512✔
335
   }
336

337
   BOTAN_ASSERT_NOMSG(stuffer.full());
6,124✔
338
   return pk;
6,124✔
339
}
×
340

341
/**
342
 * NIST FIPS 204, Algorithm 23 (pkDecode)
343
 */
344
std::pair<DilithiumSeedRho, DilithiumPolyVec> decode_public_key(StrongSpan<const DilithiumSerializedPublicKey> pk,
1,746✔
345
                                                                const DilithiumConstants& mode) {
346
   if(pk.size() != mode.public_key_bytes()) {
1,746✔
347
      throw Decoding_Error("Dilithium: Invalid public key length");
×
348
   }
349

350
   BufferSlicer slicer(pk);
1,746✔
351
   auto rho = slicer.copy<DilithiumSeedRho>(DilithiumConstants::SEED_RHO_BYTES);
1,746✔
352

353
   DilithiumPolyVec t1(mode.k());
1,746✔
354
   for(auto& p : t1) {
12,228✔
355
      poly_unpack_t1(p, slicer);
20,964✔
356
   }
357
   BOTAN_ASSERT_NOMSG(slicer.empty());
1,746✔
358

359
   return {std::move(rho), std::move(t1)};
1,746✔
360
}
1,746✔
361

362
#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
363

364
/**
365
 * NIST FIPS 204, Algorithm 24 (skEncode)
366
 */
367
DilithiumSerializedPrivateKey encode_keypair(const DilithiumInternalKeypair& keypair) {
1,306✔
368
   const auto& [pk, sk] = keypair;
1,306✔
369
   BOTAN_ASSERT_NONNULL(pk);
1,306✔
370
   BOTAN_ASSERT_NONNULL(sk);
1,306✔
371
   const auto& mode = sk->mode();
1,306✔
372
   auto scope = CT::scoped_poison(*sk);
1,306✔
373

374
   DilithiumSerializedPrivateKey serialization(mode.private_key_bytes());
1,306✔
375
   BufferStuffer stuffer(serialization);
1,306✔
376

377
   stuffer.append(pk->rho());
1,306✔
378
   stuffer.append(sk->signing_seed());
1,306✔
379
   stuffer.append(pk->tr());
1,306✔
380

381
   for(const auto& p : sk->s1()) {
8,276✔
382
      poly_pack_eta(p, stuffer, mode);
6,970✔
383
   }
384

385
   for(const auto& p : sk->s2()) {
9,148✔
386
      poly_pack_eta(p, stuffer, mode);
7,842✔
387
   }
388

389
   for(const auto& p : sk->t0()) {
9,148✔
390
      poly_pack_t0(p, stuffer);
15,684✔
391
   }
392

393
   BOTAN_ASSERT_NOMSG(stuffer.full());
1,306✔
394
   CT::unpoison(serialization);
1,306✔
395

396
   return serialization;
1,306✔
397
}
1,306✔
398

399
/**
400
 * NIST FIPS 204, Algorithm 25 (skDecode)
401
 *
402
 * Because Botan's Private_Key class inherits from Public_Key, we have to
403
 * derive the public key from the private key here.
404
 *
405
 * TODO(Botan4): This should be refactored after PrivateKey does not inherit
406
 *               from PublicKey anymore.
407
 */
408
DilithiumInternalKeypair decode_keypair(StrongSpan<const DilithiumSerializedPrivateKey> sk, DilithiumConstants mode) {
67✔
409
   auto scope = CT::scoped_poison(sk);
67✔
410

411
   BOTAN_ASSERT_NOMSG(sk.size() == mode.private_key_bytes());
67✔
412

413
   BufferSlicer slicer(sk);
67✔
414

415
   auto rho = slicer.copy<DilithiumSeedRho>(DilithiumConstants::SEED_RHO_BYTES);
67✔
416
   auto K = slicer.copy<DilithiumSigningSeedK>(DilithiumConstants::SEED_SIGNING_KEY_BYTES);
67✔
417
   auto tr = slicer.copy<DilithiumHashedPublicKey>(mode.public_key_hash_bytes());
67✔
418

419
   DilithiumPolyVec s1(mode.l());
67✔
420
   for(auto& p : s1) {
432✔
421
      poly_unpack_eta(p, slicer, mode, true /* check decoded value range */);
365✔
422
   }
423

424
   DilithiumPolyVec s2(mode.k());
67✔
425
   for(auto& p : s2) {
479✔
426
      poly_unpack_eta(p, slicer, mode, true /* check decoded value range */);
412✔
427
   }
428

429
   DilithiumPolyVec t0(mode.k());
67✔
430
   for(auto& p : t0) {
479✔
431
      poly_unpack_t0(p, slicer);
824✔
432
   }
433

434
   BOTAN_ASSERT_NOMSG(slicer.empty());
67✔
435

436
   // Currently, Botan's Private_Key class inherits from Public_Key, forcing us
437
   // to derive the public key from the private key here.
438
   // TODO(Botan4): Reconsider once PrivateKey/PublicKey issue is tackled.
439

440
   CT::unpoison(rho);  // rho is public (used in rejection sampling of matrix A)
67✔
441

442
   const auto A = expand_A(rho, mode);
67✔
443
   auto [t1, _] = compute_t1_and_t0(A, s1, s2);
67✔
444

445
   CT::unpoison(t1);  // part of the public key
67✔
446

447
   DilithiumInternalKeypair keypair{
67✔
448
      std::make_shared<Dilithium_PublicKeyInternal>(mode, std::move(rho), std::move(t1)),
67✔
449
      std::make_shared<Dilithium_PrivateKeyInternal>(std::move(mode),
67✔
450
                                                     std::nullopt,  // decoding cannot recover the private seed
451
                                                     std::move(K),
452
                                                     std::move(s1),
453
                                                     std::move(s2),
454
                                                     std::move(t0)),
455
   };
67✔
456

457
   CT::unpoison(tr);  // hash of the public key
67✔
458

459
   if(keypair.first->tr() != tr) {
67✔
460
      throw Decoding_Error("Calculated dilithium public key hash does not match the one stored in the private key");
×
461
   }
462

463
   CT::unpoison(*keypair.second);
67✔
464

465
   return keypair;
134✔
466
}
134✔
467

468
#endif
469

470
/**
471
 * NIST FIPS 204, Algorithm 26 (sigEncode)
472
 */
473
DilithiumSerializedSignature encode_signature(StrongSpan<const DilithiumCommitmentHash> c,
1,518✔
474
                                              const DilithiumPolyVec& response,
475
                                              const DilithiumPolyVec& hint,
476
                                              const DilithiumConstants& mode) {
477
   DilithiumSerializedSignature sig(mode.signature_bytes());
1,518✔
478
   BufferStuffer stuffer(sig);
1,518✔
479

480
   stuffer.append(c);
1,518✔
481
   for(const auto& p : response) {
9,588✔
482
      poly_pack_gamma1(p, stuffer, mode);
8,070✔
483
   }
484
   hint_pack(hint, stuffer, mode);
1,518✔
485

486
   return sig;
1,518✔
487
}
×
488

489
/**
490
 * NIST FIPS 204, Algorithm 27 (sigDecode)
491
 */
492
std::optional<std::tuple<DilithiumCommitmentHash, DilithiumPolyVec, DilithiumPolyVec>> decode_signature(
8,328✔
493
   StrongSpan<const DilithiumSerializedSignature> sig, const DilithiumConstants& mode) {
494
   BufferSlicer slicer(sig);
8,328✔
495
   BOTAN_ASSERT_NOMSG(slicer.remaining() == mode.signature_bytes());
8,328✔
496

497
   auto commitment_hash = slicer.copy<DilithiumCommitmentHash>(mode.commitment_hash_full_bytes());
8,328✔
498

499
   DilithiumPolyVec response(mode.l());
8,328✔
500
   for(auto& p : response) {
52,453✔
501
      poly_unpack_gamma1(p, slicer, mode);
44,125✔
502
   }
503
   BOTAN_ASSERT_NOMSG(slicer.remaining() == size_t(mode.omega()) + mode.k());
8,328✔
504

505
   auto hint = hint_unpack(slicer, mode);
8,328✔
506
   BOTAN_ASSERT_NOMSG(slicer.empty());
8,328✔
507
   if(!hint.has_value()) {
8,328✔
508
      return std::nullopt;
15✔
509
   }
510

511
   return std::make_tuple(std::move(commitment_hash), std::move(response), std::move(hint.value()));
8,313✔
512
}
8,343✔
513

514
/**
515
 * NIST FIPS 204, Algorithm 28 (w1Encode)
516
 */
517
DilithiumSerializedCommitment encode_commitment(const DilithiumPolyVec& w1, const DilithiumConstants& mode) {
14,920✔
518
   DilithiumSerializedCommitment commitment(mode.serialized_commitment_bytes());
14,920✔
519
   BufferStuffer stuffer(commitment);
14,920✔
520

521
   for(const auto& p : w1) {
103,812✔
522
      poly_pack_w1(p, stuffer, mode);
88,892✔
523
   }
524

525
   return commitment;
14,920✔
526
}
×
527

528
/**
529
 * NIST FIPS 204, Algorithm 29 (SampleInBall)
530
 */
531
DilithiumPoly sample_in_ball(StrongSpan<const DilithiumCommitmentHash> seed, const DilithiumConstants& mode) {
14,920✔
532
   // This generator resembles the while loop in the spec.
533
   auto& xof = mode.symmetric_primitives().H(seed);
14,920✔
534
   auto bounded_xof = Bounded_XOF<DilithiumConstants::SAMPLE_IN_BALL_XOF_BOUND + 8>(xof);
14,920✔
535

536
   DilithiumPoly c;
14,920✔
537
   uint64_t signs = load_le(bounded_xof.next<8>());
14,920✔
538
   for(size_t i = c.size() - mode.tau(); i < c.size(); ++i) {
747,409✔
539
      const auto j = bounded_xof.next_byte([i](uint8_t byte) { return byte <= i; });
814,251✔
540
      c[i] = c[j];
732,489✔
541
      c[j] = 1 - 2 * (signs & 1);
732,489✔
542
      signs >>= 1;
732,489✔
543
   }
544

545
   BOTAN_DEBUG_ASSERT(c.ct_validate_value_range(-1, 1));
14,920✔
546
   BOTAN_DEBUG_ASSERT(c.hamming_weight() == mode.tau());
14,920✔
547

548
   return c;
14,920✔
549
}
×
550

551
namespace {
552

553
/**
554
 * NIST FIPS 204, Algorithm 30 (RejNTTPoly)
555
 */
556
void sample_ntt_uniform(StrongSpan<const DilithiumSeedRho> rho,
167,790✔
557
                        DilithiumPolyNTT& p,
558
                        uint16_t nonce,
559
                        const DilithiumConstants& mode) {
560
   /**
561
    * A generator that returns the next coefficient sampled from the XOF,
562
    * according to: NIST FIPS 204, Algorithm 14 (CoeffFromThreeBytes).
563
    */
564
   auto& xof = mode.symmetric_primitives().H(rho, nonce);
167,790✔
565
   auto bounded_xof = Bounded_XOF<DilithiumConstants::SAMPLE_NTT_POLY_FROM_XOF_BOUND>(xof);
167,790✔
566

567
   for(auto& coeff : p) {
43,122,030✔
568
      coeff =
85,908,480✔
569
         bounded_xof.next<3>([](const auto bytes) { return make_uint32(0, bytes[2], bytes[1], bytes[0]) & 0x7FFFFF; },
85,949,657✔
570
                             [](const uint32_t z) { return z < DilithiumConstants::Q; });
42,954,240✔
571
   }
572

573
   BOTAN_DEBUG_ASSERT(p.ct_validate_value_range(0, DilithiumConstants::Q - 1));
167,790✔
574
}
167,790✔
575

576
/**
577
 * NIST FIPS 204, Algorithm 15 (CoeffFromHalfByte)
578
 *
579
 * Magic numbers for (b mod 5) are taken from the reference implementation.
580
 */
581
template <DilithiumConstants::DilithiumEta eta>
582
std::optional<int32_t> coeff_from_halfbyte(uint8_t b) {
5,496,244✔
583
   BOTAN_DEBUG_ASSERT(b < 16);
5,496,244✔
584

585
   if constexpr(eta == DilithiumConstants::DilithiumEta::_2) {
586
      if(CT::driveby_unpoison(b < 15)) {
3,019,048✔
587
         b = b - (205 * b >> 10) * 5;  // b = b mod 5
2,829,436✔
588
         return 2 - b;
2,829,436✔
589
      }
590
   } else if constexpr(eta == DilithiumConstants::DilithiumEta::_4) {
591
      if(CT::driveby_unpoison(b < 9)) {
2,477,196✔
592
         return 4 - b;
1,392,560✔
593
      }
594
   }
595

596
   return std::nullopt;
597
}
598

599
template <DilithiumConstants::DilithiumEta eta>
600
void sample_uniform_eta(DilithiumPoly& p, Botan::XOF& xof) {
16,467✔
601
   // A generator that returns the next coefficient sampled from the XOF. As the
602
   // sampling uses half-bytes, this keeps track of the additionally sampled
603
   // coefficient as needed.
604
   auto next_coeff = [bounded_xof = Bounded_XOF<DilithiumConstants::SAMPLE_POLY_FROM_XOF_BOUND>(xof),
4,232,019✔
605
                      stashed_coeff = std::optional<int32_t>{}]() mutable -> int32_t {
606
      if(auto stashed = std::exchange(stashed_coeff, std::nullopt)) {
8,431,104✔
607
         return *stashed;
4,215,552✔
608
      }
609

610
      BOTAN_DEBUG_ASSERT(!stashed_coeff.has_value());
2,748,122✔
611
      while(true) {
243,223✔
612
         const auto b = bounded_xof.next_byte();
5,496,244✔
613
         const auto z0 = coeff_from_halfbyte<eta>(b & 0x0F);
2,748,122✔
614
         const auto z1 = coeff_from_halfbyte<eta>(b >> 4);
4,859,704✔
615

616
         if(z0.has_value()) {
2,748,122✔
617
            stashed_coeff = z1;  // keep candidate z1 for the next invocation
2,110,414✔
618
            return *z0;
2,504,899✔
619
         } else if(z1.has_value()) {
637,708✔
620
            // z0 was invalid, z1 is valid, nothing to stash
621
            return *z1;
622
         }
623
      }
624
   };
625

626
   for(auto& coeff : p) {
4,232,019✔
627
      coeff = next_coeff();
4,215,552✔
628
   }
629
}
16,467✔
630

631
/**
632
 * NIST FIPS 204, Algorithm 31 (RejBoundedPoly)
633
 */
634
void sample_uniform_eta(StrongSpan<const DilithiumSeedRhoPrime> rhoprime,
16,467✔
635
                        DilithiumPoly& p,
636
                        uint16_t nonce,
637
                        const DilithiumConstants& mode) {
638
   using Eta = DilithiumConstants::DilithiumEta;
16,467✔
639

640
   auto& xof = mode.symmetric_primitives().H(rhoprime, nonce);
16,467✔
641
   switch(mode.eta()) {
16,467✔
642
      case Eta::_2:
11,033✔
643
         sample_uniform_eta<Eta::_2>(p, xof);
11,033✔
644
         break;
11,033✔
645
      case Eta::_4:
5,434✔
646
         sample_uniform_eta<Eta::_4>(p, xof);
5,434✔
647
         break;
5,434✔
648
   }
649

650
   // Rejection sampling is done. Secret polynomial can be repoisoned.
651
   CT::poison(p);
16,467✔
652

653
   BOTAN_DEBUG_ASSERT(p.ct_validate_value_range(-static_cast<int32_t>(mode.eta()), mode.eta()));
16,467✔
654
}
16,467✔
655

656
}  // namespace
657

658
/**
659
 * NIST FIPS 204, Algorithm 6 (ML-DSA.KeyGen_internal)
660
 *
661
 * Lines 5-7 are extracted into a separate function, see above. The key
662
 * encoding is deferred until the user explicitly invokes the encoding.
663
 */
664
DilithiumInternalKeypair expand_keypair(DilithiumSeedRandomness xi, DilithiumConstants mode) {
1,454✔
665
   const auto& sympriv = mode.symmetric_primitives();
1,454✔
666
   CT::poison(xi);
1,454✔
667

668
   auto [rho, rhoprime, K] = sympriv.H(xi);
1,454✔
669
   CT::unpoison(rho);  // rho is public (seed for the public matrix A)
1,454✔
670

671
   const auto A = Dilithium_Algos::expand_A(rho, mode);
1,454✔
672
   auto [s1, s2] = Dilithium_Algos::expand_s(rhoprime, mode);
1,454✔
673
   auto [t1, t0] = Dilithium_Algos::compute_t1_and_t0(A, s1, s2);
1,454✔
674

675
   CT::unpoison(t1);  // part of the public key
1,454✔
676

677
   DilithiumInternalKeypair keypair{
1,454✔
678
      std::make_shared<Dilithium_PublicKeyInternal>(mode, std::move(rho), std::move(t1)),
1,454✔
679
      std::make_shared<Dilithium_PrivateKeyInternal>(
1,454✔
680
         std::move(mode), std::move(xi), std::move(K), std::move(s1), std::move(s2), std::move(t0)),
681
   };
1,454✔
682

683
   CT::unpoison(*keypair.second);
1,454✔
684

685
   return keypair;
1,454✔
686
}
2,908✔
687

688
/**
689
 * NIST FIPS 204, Algorithm 32 (ExpandA)
690
 *
691
 * Note that the actual concatenation of rho, s and r is done downstream
692
 * in the sampling function.
693
 */
694
DilithiumPolyMatNTT expand_A(StrongSpan<const DilithiumSeedRho> rho, const DilithiumConstants& mode) {
4,948✔
695
   DilithiumPolyMatNTT A(mode.k(), mode.l());
4,948✔
696
   for(uint8_t r = 0; r < mode.k(); ++r) {
34,654✔
697
      for(uint8_t s = 0; s < mode.l(); ++s) {
197,496✔
698
         sample_ntt_uniform(rho, A[r][s], load_le(std::array{s, r}), mode);
167,790✔
699
      }
700
   }
701
   return A;
4,948✔
702
}
×
703

704
/**
705
 * NIST FIPS 204, Algorithm 33 (ExpandS)
706
 */
707
std::pair<DilithiumPolyVec, DilithiumPolyVec> expand_s(StrongSpan<const DilithiumSeedRhoPrime> rhoprime,
1,454✔
708
                                                       const DilithiumConstants& mode) {
709
   auto result = std::make_pair(DilithiumPolyVec(mode.l()), DilithiumPolyVec(mode.k()));
1,454✔
710
   auto& [s1, s2] = result;
1,454✔
711

712
   uint16_t nonce = 0;
1,454✔
713
   for(auto& p : s1) {
9,201✔
714
      sample_uniform_eta(rhoprime, p, nonce++, mode);
7,747✔
715
   }
716

717
   for(auto& p : s2) {
10,174✔
718
      sample_uniform_eta(rhoprime, p, nonce++, mode);
8,720✔
719
   }
720

721
   return result;
1,454✔
722
}
×
723

724
/**
725
 * NIST FIPS 204, Algorithm 34 (ExpandMask)
726
 */
727
DilithiumPolyVec expand_mask(StrongSpan<const DilithiumSeedRhoPrime> rhoprime,
6,783✔
728
                             uint16_t nonce,
729
                             const DilithiumConstants& mode) {
730
   DilithiumPolyVec s(mode.l());
6,783✔
731
   for(auto& p : s) {
42,498✔
732
      auto& xof = mode.symmetric_primitives().H(rhoprime, nonce++);
35,715✔
733
      poly_unpack_gamma1(p, xof, mode);
35,715✔
734
   }
735
   return s;
6,783✔
736
}
×
737

738
/**
739
 * NIST FIPS 204, Algorithm 35 (Power2Round)
740
 *
741
 * In contrast to the spec, this function takes a polynomial vector and
742
 * performs the power2round operation on each coefficient in the vector.
743
 * The actual Algorithm 35 as specified is actually just the inner lambda.
744
 */
745
std::pair<DilithiumPolyVec, DilithiumPolyVec> power2round(const DilithiumPolyVec& vec) {
1,521✔
746
   // This procedure is taken verbatim from Dilithium's reference implementation.
747
   auto power2round = [d = DilithiumConstants::D](int32_t r) -> std::pair<int32_t, int32_t> {
2,339,313✔
748
      const int32_t r1 = (r + (1 << (d - 1)) - 1) >> d;
2,337,792✔
749
      const int32_t r0 = r - (r1 << d);
2,337,792✔
750
      return {r1, r0};
2,337,792✔
751
   };
752

753
   auto result = std::make_pair(DilithiumPolyVec(vec.size()), DilithiumPolyVec(vec.size()));
1,521✔
754

755
   for(size_t i = 0; i < vec.size(); ++i) {
10,653✔
756
      for(size_t j = 0; j < vec[i].size(); ++j) {
2,346,924✔
757
         std::tie(result.first[i][j], result.second[i][j]) = power2round(vec[i][j]);
2,337,792✔
758
      }
759
   }
760

761
   return result;
1,521✔
762
}
763

764
namespace {
765

766
/**
767
 * NIST FIPS 204, Algorithm 36 (Decompose)
768
 *
769
 * The implementation is adapted from the verbatim reference implementation using
770
 * the magic numbers that depend on the values of Q and gamma2 and ensure that
771
 * this operation is done in constant-time.
772
 */
773
template <DilithiumConstants::DilithiumGamma2 gamma2>
774
std::pair<int32_t, int32_t> decompose(int32_t r) {
22,756,352✔
775
   int32_t r1 = (r + 127) >> 7;
22,756,352✔
776

777
   if constexpr(gamma2 == DilithiumConstants::DilithiumGamma2::Qminus1DividedBy32) {
778
      r1 = (r1 * 1025 + (1 << 21)) >> 22;
17,776,640✔
779
      r1 &= 15;
17,776,640✔
780
   } else if constexpr(gamma2 == DilithiumConstants::DilithiumGamma2::Qminus1DividedBy88) {
781
      r1 = (r1 * 11275 + (1 << 23)) >> 24;
4,979,712✔
782
      r1 = is_negative_mask(43 - r1).if_not_set_return(r1);
4,979,712✔
783
   }
784

785
   int32_t r0 = r - r1 * 2 * gamma2;
22,756,352✔
786

787
   // reduce r0 mod q
788
   r0 -= is_negative_mask((DilithiumConstants::Q - 1) / 2 - r0).if_set_return(DilithiumConstants::Q);
8,142,848✔
789

790
   return {r1, r0};
8,142,848✔
791
}
792

793
/**
794
 * This is templated on all possible values of gamma2 to allow for compile-time
795
 * optimization given the statically known value of gamma2.
796
 */
797
template <DilithiumConstants::DilithiumGamma2 gamma2>
798
std::pair<DilithiumPolyVec, DilithiumPolyVec> decompose_all_coefficients(const DilithiumPolyVec& vec) {
6,783✔
799
   auto result = std::make_pair(DilithiumPolyVec(vec.size()), DilithiumPolyVec(vec.size()));
6,783✔
800

801
   for(size_t i = 0; i < vec.size(); ++i) {
47,143✔
802
      for(size_t j = 0; j < vec[i].size(); ++j) {
10,372,520✔
803
         std::tie(result.first[i][j], result.second[i][j]) = decompose<gamma2>(vec[i][j]);
10,332,160✔
804
      }
805
   }
806

807
   return result;
6,783✔
808
}
809

810
}  // namespace
811

812
/**
813
 * NIST FIPS 204, Algorithm 36 (Decompose) on a polynomial vector
814
 *
815
 * Algorithms 37 (HighBits) and 38 (LowBits) are not implemented explicitly,
816
 * simply use the first (HighBits) and second (LowBits) element of the result.
817
 */
818
std::pair<DilithiumPolyVec, DilithiumPolyVec> decompose(const DilithiumPolyVec& vec, const DilithiumConstants& mode) {
6,783✔
819
   using Gamma2 = DilithiumConstants::DilithiumGamma2;
6,783✔
820
   switch(mode.gamma2()) {
6,783✔
821
      case Gamma2::Qminus1DividedBy32:
4,645✔
822
         return decompose_all_coefficients<Gamma2::Qminus1DividedBy32>(vec);
4,645✔
823
         break;
2,138✔
824
      case Gamma2::Qminus1DividedBy88:
2,138✔
825
         return decompose_all_coefficients<Gamma2::Qminus1DividedBy88>(vec);
2,138✔
826
         break;
×
827
   }
828

829
   BOTAN_ASSERT_UNREACHABLE();
×
830
}
831

832
/**
833
 * NIST FIPS 204, Algorithm 39 (MakeHint)
834
 *
835
 * MakeHint is specified per value in FIPS 204. This implements the algorithm
836
 * for the entire polynomial vector. The specified algorithm is equivalent to
837
 * the inner lambda.
838
 *
839
 * TODO: This is taken from the reference implementation. We should implement it
840
 *       as specified in the spec, and see if that has any performance impact.
841
 */
842
DilithiumPolyVec make_hint(const DilithiumPolyVec& z, const DilithiumPolyVec& r, const DilithiumConstants& mode) {
1,531✔
843
   BOTAN_DEBUG_ASSERT(z.size() == r.size());
1,531✔
844

845
   auto make_hint = [gamma2 = uint32_t(mode.gamma2()),
1,531✔
846
                     q_gamma2 = static_cast<uint32_t>(DilithiumConstants::Q) - uint32_t(mode.gamma2())](
1,531✔
847
                       int32_t c0, int32_t c1) -> CT::Choice {
848
      BOTAN_DEBUG_ASSERT(c0 >= 0);
2,351,104✔
849
      BOTAN_DEBUG_ASSERT(c1 >= 0);
2,351,104✔
850

851
      const uint32_t pc0 = static_cast<uint32_t>(c0);
2,351,104✔
852
      const uint32_t pc1 = static_cast<uint32_t>(c1);
2,351,104✔
853

854
      return (CT::Mask<uint32_t>::is_gt(pc0, gamma2) & CT::Mask<uint32_t>::is_lte(pc0, q_gamma2) &
2,351,104✔
855
              ~(CT::Mask<uint32_t>::is_equal(pc0, q_gamma2) & CT::Mask<uint32_t>::is_zero(pc1)))
2,351,104✔
856
         .as_choice();
2,351,104✔
857
   };
1,531✔
858

859
   DilithiumPolyVec hint(r.size());
1,531✔
860

861
   for(size_t i = 0; i < r.size(); ++i) {
10,715✔
862
      for(size_t j = 0; j < r[i].size(); ++j) {
2,360,288✔
863
         hint[i][j] = static_cast<int>(make_hint(z[i][j], r[i][j]).as_bool());
2,351,104✔
864
      }
865
   }
866

867
   BOTAN_DEBUG_ASSERT(hint.ct_validate_value_range(0, 1));
1,531✔
868

869
   return hint;
1,531✔
870
}
871

872
namespace {
873

874
/**
875
 * This is templated on all possible values of gamma2 to allow for compile-time
876
 * optimization given the statically known value of gamma2.
877
 */
878
template <DilithiumConstants::DilithiumGamma2 gamma2>
879
void use_hint_on_coefficients(const DilithiumPolyVec& hints, DilithiumPolyVec& vec) {
8,137✔
880
   constexpr auto m = (DilithiumConstants::Q - 1) / (2 * gamma2);
8,137✔
881

882
   auto modulo_m = [](int32_t r1) -> int32_t {
418,994✔
883
      BOTAN_DEBUG_ASSERT(r1 >= -static_cast<decltype(r1)>(m) && r1 <= static_cast<decltype(r1)>(m));
418,994✔
884
      return (r1 + m) % m;
418,994✔
885
   };
886

887
   auto use_hint = [&modulo_m](bool hint, int32_t r) -> int32_t {
12,432,329✔
888
      auto [r1, r0] = decompose<gamma2>(r);
12,424,192✔
889

890
      if(!hint) {
12,424,192✔
891
         return r1;
892
      }
893

894
      if(r0 > 0) {
418,994✔
895
         return modulo_m(r1 + 1);
209,497✔
896
      } else {
897
         return modulo_m(r1 - 1);
209,497✔
898
      }
899
   };
900

901
   for(size_t i = 0; i < vec.size(); ++i) {
56,669✔
902
      for(size_t j = 0; j < vec[i].size(); ++j) {
12,472,724✔
903
         vec[i][j] = use_hint(hints[i][j], vec[i][j]);
12,424,192✔
904
      }
905
   }
906
}
8,137✔
907

908
}  // namespace
909

910
/**
911
 * NIST FIPS 204, Algorithm 40 (UseHint)
912
 *
913
 * UseHint is specified per value in FIPS 204. This implements the algorithm
914
 * for the entire polynomial vector. The specified algorithm is equivalent to
915
 * the inner lambdas of 'use_hint_with_coefficients'.
916
 */
917
void use_hint(DilithiumPolyVec& vec, const DilithiumPolyVec& hints, const DilithiumConstants& mode) {
8,137✔
918
   BOTAN_DEBUG_ASSERT(hints.size() == vec.size());
8,137✔
919
   BOTAN_DEBUG_ASSERT(hints.ct_validate_value_range(0, 1));
8,137✔
920
   BOTAN_DEBUG_ASSERT(vec.ct_validate_value_range(0, DilithiumConstants::Q - 1));
8,137✔
921

922
   using Gamma2 = DilithiumConstants::DilithiumGamma2;
8,137✔
923
   switch(mode.gamma2()) {
8,137✔
924
      case Gamma2::Qminus1DividedBy32:
5,412✔
925
         use_hint_on_coefficients<Gamma2::Qminus1DividedBy32>(hints, vec);
5,412✔
926
         break;
5,412✔
927
      case Gamma2::Qminus1DividedBy88:
2,725✔
928
         use_hint_on_coefficients<Gamma2::Qminus1DividedBy88>(hints, vec);
2,725✔
929
         break;
2,725✔
930
   }
931

932
   BOTAN_DEBUG_ASSERT(vec.ct_validate_value_range(0, (DilithiumConstants::Q - 1) / (2 * mode.gamma2())));
8,137✔
933
}
8,137✔
934

935
bool infinity_norm_within_bound(const DilithiumPolyVec& vec, size_t bound) {
20,764✔
936
   BOTAN_DEBUG_ASSERT(bound <= (DilithiumConstants::Q - 1) / 8);
20,764✔
937

938
   // It is ok to leak which coefficient violates the bound as the probability
939
   // for each coefficient is independent of secret data but we must not leak
940
   // the sign of the centralized representative.
941
   for(const auto& p : vec) {
114,822✔
942
      for(auto c : p) {
24,830,155✔
943
         const auto abs_c = c - is_negative_mask(c).if_set_return(2 * c);
24,736,097✔
944
         if(CT::driveby_unpoison(abs_c >= bound)) {
24,736,097✔
945
            return false;
20,764✔
946
         }
947
      }
948
   }
949

950
   return true;
951
}
952

953
}  // namespace Botan::Dilithium_Algos
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc