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

randombit / botan / 20283898778

16 Dec 2025 09:52PM UTC coverage: 90.52% (+0.2%) from 90.36%
20283898778

Pull #5167

github

web-flow
Merge 795a38954 into 3d96b675e
Pull Request #5167: Changes to reduce unnecessary inclusions

101154 of 111748 relevant lines covered (90.52%)

12682929.61 hits per line

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

95.86
/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
#include <utility>
29

30
namespace Botan::Dilithium_Algos {
31

32
namespace {
33

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

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

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

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

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

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

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

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

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

120
   BOTAN_ASSERT_UNREACHABLE();
×
121
}
122

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

136
   BOTAN_ASSERT_UNREACHABLE();
×
137
}
138

139
#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
140

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

154
   BOTAN_ASSERT_UNREACHABLE();
×
155
}
156

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

166
#endif
167

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

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

195
   BOTAN_ASSERT_UNREACHABLE();
×
196
}
197

198
#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
199

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

213
   BOTAN_ASSERT_UNREACHABLE();
×
214
}
215

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

225
#endif
226

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

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

237
   uint8_t index = 0;
1,517✔
238
   for(const auto& p : h) {
10,625✔
239
      for(size_t i = 0; i < p.size(); ++i) {
2,340,756✔
240
         if(p[i] == 1) {
2,331,648✔
241
            bit_positions.append(static_cast<uint8_t>(i));
78,517✔
242
            ++index;
78,517✔
243
         }
244
      }
245
      offsets.append(index);
9,108✔
246
   }
247

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

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

259
   DilithiumPolyVec hint(mode.k());
8,324✔
260
   uint8_t index = 0;
8,324✔
261
   for(auto& p : hint) {
57,922✔
262
      const auto end_index = offsets.take_byte();
49,608✔
263

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

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

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

279
      // Set the specified bits in the polynomial
280
      for(const auto i : set_bits) {
468,969✔
281
         p[i] = 1;
419,371✔
282
      }
283

284
      index = end_index;
49,598✔
285
   }
286

287
   // Check that the remaining bit positions are all zero (strong unforgeability)
288
   uint8_t sum = 0;
8,314✔
289
   for(uint8_t b : bit_positions.take(bit_positions.remaining())) {
168,670✔
290
      sum |= b;
160,356✔
291
   }
292

293
   if(sum != 0) {
8,314✔
294
      return std::nullopt;
5✔
295
   }
296

297
   BOTAN_DEBUG_ASSERT(hint.ct_validate_value_range(0, 1));
8,309✔
298
   return hint;
8,309✔
299
}
8,324✔
300

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

321
   return Dilithium_Algos::power2round(t);
3,040✔
322
}
1,520✔
323

324
}  // namespace
325

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

335
   stuffer.append(rho);
6,123✔
336
   for(const auto& p : t1) {
42,873✔
337
      poly_pack_t1(p, stuffer);
73,500✔
338
   }
339

340
   BOTAN_ASSERT_NOMSG(stuffer.full());
6,123✔
341
   return pk;
6,123✔
342
}
×
343

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

353
   BufferSlicer slicer(pk);
1,746✔
354
   auto rho = slicer.copy<DilithiumSeedRho>(DilithiumConstants::SEED_RHO_BYTES);
1,746✔
355

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

362
   return {std::move(rho), std::move(t1)};
1,746✔
363
}
1,746✔
364

365
#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
366

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

377
   DilithiumSerializedPrivateKey serialization(mode.private_key_bytes());
1,306✔
378
   BufferStuffer stuffer(serialization);
1,306✔
379

380
   stuffer.append(pk->rho());
1,306✔
381
   stuffer.append(sk->signing_seed());
1,306✔
382
   stuffer.append(pk->tr());
1,306✔
383

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

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

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

396
   BOTAN_ASSERT_NOMSG(stuffer.full());
1,306✔
397
   CT::unpoison(serialization);
1,306✔
398

399
   return serialization;
1,306✔
400
}
1,306✔
401

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

414
   BOTAN_ASSERT_NOMSG(sk.size() == mode.private_key_bytes());
67✔
415

416
   BufferSlicer slicer(sk);
67✔
417

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

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

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

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

437
   BOTAN_ASSERT_NOMSG(slicer.empty());
67✔
438

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

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

445
   const auto A = expand_A(rho, mode);
67✔
446
   auto [t1, _] = compute_t1_and_t0(A, s1, s2);
67✔
447

448
   CT::unpoison(t1);  // part of the public key
67✔
449

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

460
   CT::unpoison(tr);  // hash of the public key
67✔
461

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

466
   CT::unpoison(*keypair.second);
67✔
467

468
   return keypair;
134✔
469
}
134✔
470

471
#endif
472

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

483
   stuffer.append(c);
1,517✔
484
   for(const auto& p : response) {
9,582✔
485
      poly_pack_gamma1(p, stuffer, mode);
8,065✔
486
   }
487
   hint_pack(hint, stuffer, mode);
1,517✔
488

489
   return sig;
1,517✔
490
}
×
491

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

500
   auto commitment_hash = slicer.copy<DilithiumCommitmentHash>(mode.commitment_hash_full_bytes());
8,324✔
501

502
   DilithiumPolyVec response(mode.l());
8,324✔
503
   for(auto& p : response) {
52,427✔
504
      poly_unpack_gamma1(p, slicer, mode);
44,103✔
505
   }
506
   BOTAN_ASSERT_NOMSG(slicer.remaining() == size_t(mode.omega()) + mode.k());
8,324✔
507

508
   auto hint = hint_unpack(slicer, mode);
8,324✔
509
   BOTAN_ASSERT_NOMSG(slicer.empty());
8,324✔
510
   if(!hint.has_value()) {
8,324✔
511
      return std::nullopt;
15✔
512
   }
513

514
   return std::make_tuple(std::move(commitment_hash), std::move(response), std::move(hint.value()));
8,309✔
515
}
8,339✔
516

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

524
   for(const auto& p : w1) {
103,118✔
525
      poly_pack_w1(p, stuffer, mode);
88,320✔
526
   }
527

528
   return commitment;
14,798✔
529
}
×
530

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

539
   DilithiumPoly c;
14,798✔
540
   uint64_t signs = load_le(bounded_xof.next<8>());
14,798✔
541
   for(size_t i = c.size() - mode.tau(); i < c.size(); ++i) {
742,111✔
542
      const auto j = bounded_xof.next_byte([i](uint8_t byte) { return byte <= i; });
808,245✔
543
      c[i] = c[j];
727,313✔
544
      c[j] = 1 - 2 * (signs & 1);
727,313✔
545
      signs >>= 1;
727,313✔
546
   }
547

548
   BOTAN_DEBUG_ASSERT(c.ct_validate_value_range(-1, 1));
14,798✔
549
   BOTAN_DEBUG_ASSERT(c.hamming_weight() == mode.tau());
14,798✔
550

551
   return c;
14,798✔
552
}
×
553

554
namespace {
555

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

570
   for(auto& coeff : p) {
43,114,320✔
571
      coeff =
85,893,120✔
572
         bounded_xof.next<3>([](const auto bytes) { return make_uint32(0, bytes[2], bytes[1], bytes[0]) & 0x7FFFFF; },
85,933,884✔
573
                             [](const uint32_t z) { return z < DilithiumConstants::Q; });
42,946,560✔
574
   }
575

576
   BOTAN_DEBUG_ASSERT(p.ct_validate_value_range(0, DilithiumConstants::Q - 1));
167,760✔
577
}
167,760✔
578

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

588
   if constexpr(eta == DilithiumConstants::DilithiumEta::_2) {
589
      if(CT::driveby_unpoison(b < 15)) {
3,018,330✔
590
         b = b - (205 * b >> 10) * 5;  // b = b mod 5
2,829,431✔
591
         return 2 - b;
2,829,431✔
592
      }
593
   } else if constexpr(eta == DilithiumConstants::DilithiumEta::_4) {
594
      if(CT::driveby_unpoison(b < 9)) {
2,473,838✔
595
         return 4 - b;
1,389,790✔
596
      }
597
   }
598

599
   return std::nullopt;
600
}
601

602
template <DilithiumConstants::DilithiumEta eta>
603
void sample_uniform_eta(DilithiumPoly& p, Botan::XOF& xof) {
16,456✔
604
   // A generator that returns the next coefficient sampled from the XOF. As the
605
   // sampling uses half-bytes, this keeps track of the additionally sampled
606
   // coefficient as needed.
607
   auto next_coeff = [bounded_xof = Bounded_XOF<DilithiumConstants::SAMPLE_POLY_FROM_XOF_BOUND>(xof),
4,229,192✔
608
                      stashed_coeff = std::optional<int32_t>{}]() mutable -> int32_t {
609
      if(auto stashed = std::exchange(stashed_coeff, std::nullopt)) {
8,425,472✔
610
         return *stashed;
4,212,736✔
611
      }
612

613
      BOTAN_DEBUG_ASSERT(!stashed_coeff.has_value());
2,746,084✔
614
      while(true) {
243,271✔
615
         const auto b = bounded_xof.next_byte();
5,492,168✔
616
         const auto z0 = coeff_from_halfbyte<eta>(b & 0x0F);
2,746,084✔
617
         const auto z1 = coeff_from_halfbyte<eta>(b >> 4);
4,856,274✔
618

619
         if(z0.has_value()) {
2,746,084✔
620
            stashed_coeff = z1;  // keep candidate z1 for the next invocation
2,109,031✔
621
            return *z0;
2,502,813✔
622
         } else if(z1.has_value()) {
637,053✔
623
            // z0 was invalid, z1 is valid, nothing to stash
624
            return *z1;
625
         }
626
      }
627
   };
628

629
   for(auto& coeff : p) {
4,229,192✔
630
      coeff = next_coeff();
4,212,736✔
631
   }
632
}
16,456✔
633

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

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

653
   // Rejection sampling is done. Secret polynomial can be repoisoned.
654
   CT::poison(p);
16,456✔
655

656
   BOTAN_DEBUG_ASSERT(p.ct_validate_value_range(-static_cast<int32_t>(mode.eta()), mode.eta()));
16,456✔
657
}
16,456✔
658

659
}  // namespace
660

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

671
   auto [rho, rhoprime, K] = sympriv.H(xi);
1,453✔
672
   CT::unpoison(rho);  // rho is public (seed for the public matrix A)
1,453✔
673

674
   const auto A = Dilithium_Algos::expand_A(rho, mode);
1,453✔
675
   auto [s1, s2] = Dilithium_Algos::expand_s(rhoprime, mode);
1,453✔
676
   auto [t1, t0] = Dilithium_Algos::compute_t1_and_t0(A, s1, s2);
1,453✔
677

678
   CT::unpoison(t1);  // part of the public key
1,453✔
679

680
   DilithiumInternalKeypair keypair{
1,453✔
681
      std::make_shared<Dilithium_PublicKeyInternal>(mode, std::move(rho), std::move(t1)),
1,453✔
682
      std::make_shared<Dilithium_PrivateKeyInternal>(
1,453✔
683
         std::move(mode), std::move(xi), std::move(K), std::move(s1), std::move(s2), std::move(t0)),
684
   };
1,453✔
685

686
   CT::unpoison(*keypair.second);
1,453✔
687

688
   return keypair;
1,453✔
689
}
2,906✔
690

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

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

715
   uint16_t nonce = 0;
1,453✔
716
   for(auto& p : s1) {
9,195✔
717
      sample_uniform_eta(rhoprime, p, nonce++, mode);
7,742✔
718
   }
719

720
   for(auto& p : s2) {
10,167✔
721
      sample_uniform_eta(rhoprime, p, nonce++, mode);
8,714✔
722
   }
723

724
   return result;
1,453✔
725
}
×
726

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

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

756
   auto result = std::make_pair(DilithiumPolyVec(vec.size()), DilithiumPolyVec(vec.size()));
1,520✔
757

758
   for(size_t i = 0; i < vec.size(); ++i) {
10,646✔
759
      for(size_t j = 0; j < vec[i].size(); ++j) {
2,345,382✔
760
         std::tie(result.first[i][j], result.second[i][j]) = power2round(vec[i][j]);
2,336,256✔
761
      }
762
   }
763

764
   return result;
1,520✔
765
}
766

767
namespace {
768

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

780
   if constexpr(gamma2 == DilithiumConstants::DilithiumGamma2::Qminus1DividedBy32) {
781
      r1 = (r1 * 1025 + (1 << 21)) >> 22;
17,710,080✔
782
      r1 &= 15;
17,710,080✔
783
   } else if constexpr(gamma2 == DilithiumConstants::DilithiumGamma2::Qminus1DividedBy88) {
784
      r1 = (r1 * 11275 + (1 << 23)) >> 24;
4,899,840✔
785
      r1 = is_negative_mask(43 - r1).if_not_set_return(r1);
4,899,840✔
786
   }
787

788
   int32_t r0 = r - r1 * 2 * gamma2;
22,609,920✔
789

790
   // reduce r0 mod q
791
   r0 -= is_negative_mask((DilithiumConstants::Q - 1) / 2 - r0).if_set_return(DilithiumConstants::Q);
8,084,992✔
792

793
   return {r1, r0};
8,084,992✔
794
}
795

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

804
   for(size_t i = 0; i < vec.size(); ++i) {
46,498✔
805
      for(size_t j = 0; j < vec[i].size(); ++j) {
10,236,310✔
806
         std::tie(result.first[i][j], result.second[i][j]) = decompose<gamma2>(vec[i][j]);
10,196,480✔
807
      }
808
   }
809

810
   return result;
6,668✔
811
}
812

813
}  // namespace
814

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

832
   BOTAN_ASSERT_UNREACHABLE();
×
833
}
834

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

848
   auto make_hint = [gamma2 = uint32_t(mode.gamma2()),
1,530✔
849
                     q_gamma2 = static_cast<uint32_t>(DilithiumConstants::Q) - uint32_t(mode.gamma2())](
1,530✔
850
                       int32_t c0, int32_t c1) -> CT::Choice {
851
      BOTAN_DEBUG_ASSERT(c0 >= 0);
2,350,080✔
852
      BOTAN_DEBUG_ASSERT(c1 >= 0);
2,350,080✔
853

854
      const uint32_t pc0 = static_cast<uint32_t>(c0);
2,350,080✔
855
      const uint32_t pc1 = static_cast<uint32_t>(c1);
2,350,080✔
856

857
      return (CT::Mask<uint32_t>::is_gt(pc0, gamma2) & CT::Mask<uint32_t>::is_lte(pc0, q_gamma2) &
2,350,080✔
858
              ~(CT::Mask<uint32_t>::is_equal(pc0, q_gamma2) & CT::Mask<uint32_t>::is_zero(pc1)))
2,350,080✔
859
         .as_choice();
2,350,080✔
860
   };
1,530✔
861

862
   DilithiumPolyVec hint(r.size());
1,530✔
863

864
   for(size_t i = 0; i < r.size(); ++i) {
10,710✔
865
      for(size_t j = 0; j < r[i].size(); ++j) {
2,359,260✔
866
         hint[i][j] = static_cast<int>(make_hint(z[i][j], r[i][j]).as_bool());
2,350,080✔
867
      }
868
   }
869

870
   BOTAN_DEBUG_ASSERT(hint.ct_validate_value_range(0, 1));
1,530✔
871

872
   return hint;
1,530✔
873
}
874

875
namespace {
876

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

885
   auto modulo_m = [](int32_t r1) -> int32_t {
418,909✔
886
      BOTAN_DEBUG_ASSERT(r1 >= -static_cast<decltype(r1)>(m) && r1 <= static_cast<decltype(r1)>(m));
418,909✔
887
      return (r1 + m) % m;
418,909✔
888
   };
889

890
   auto use_hint = [&modulo_m](bool hint, int32_t r) -> int32_t {
12,421,570✔
891
      auto [r1, r0] = decompose<gamma2>(r);
12,413,440✔
892

893
      if(!hint) {
12,413,440✔
894
         return r1;
895
      }
896

897
      if(r0 > 0) {
418,909✔
898
         return modulo_m(r1 + 1);
208,823✔
899
      } else {
900
         return modulo_m(r1 - 1);
210,086✔
901
      }
902
   };
903

904
   for(size_t i = 0; i < vec.size(); ++i) {
56,620✔
905
      for(size_t j = 0; j < vec[i].size(); ++j) {
12,461,930✔
906
         vec[i][j] = use_hint(hints[i][j], vec[i][j]);
12,413,440✔
907
      }
908
   }
909
}
8,130✔
910

911
}  // namespace
912

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

925
   using Gamma2 = DilithiumConstants::DilithiumGamma2;
8,130✔
926
   switch(mode.gamma2()) {
8,130✔
927
      case Gamma2::Qminus1DividedBy32:
5,407✔
928
         use_hint_on_coefficients<Gamma2::Qminus1DividedBy32>(hints, vec);
5,407✔
929
         break;
5,407✔
930
      case Gamma2::Qminus1DividedBy88:
2,723✔
931
         use_hint_on_coefficients<Gamma2::Qminus1DividedBy88>(hints, vec);
2,723✔
932
         break;
2,723✔
933
   }
934

935
   BOTAN_DEBUG_ASSERT(vec.ct_validate_value_range(0, (DilithiumConstants::Q - 1) / (2 * mode.gamma2())));
8,130✔
936
}
8,130✔
937

938
bool infinity_norm_within_bound(const DilithiumPolyVec& vec, size_t bound) {
20,584✔
939
   BOTAN_DEBUG_ASSERT(bound <= (DilithiumConstants::Q - 1) / 8);
20,584✔
940

941
   // It is ok to leak which coefficient violates the bound as the probability
942
   // for each coefficient is independent of secret data but we must not leak
943
   // the sign of the centralized representative.
944
   for(const auto& p : vec) {
114,173✔
945
      for(auto c : p) {
24,695,314✔
946
         const auto abs_c = c - is_negative_mask(c).if_set_return(2 * c);
24,601,725✔
947
         if(CT::driveby_unpoison(abs_c >= bound)) {
24,601,725✔
948
            return false;
20,584✔
949
         }
950
      }
951
   }
952

953
   return true;
954
}
955

956
}  // 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