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

randombit / botan / 11345152100

15 Oct 2024 11:31AM UTC coverage: 91.146% (+0.02%) from 91.131%
11345152100

Pull #4270

github

web-flow
Merge 041fb0a91 into 7dd590598
Pull Request #4270: PQC: ML-DSA

90747 of 99562 relevant lines covered (91.15%)

9029457.78 hits per line

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

94.87
/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/fmt.h>
25
#include <botan/internal/loadstor.h>
26
#include <botan/internal/pqcrystals_encoding.h>
27
#include <botan/internal/pqcrystals_helpers.h>
28
#include <botan/internal/stl_util.h>
29

30
#include <utility>
31

32
namespace Botan::Dilithium_Algos {
33

34
namespace {
35

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

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

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

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

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

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

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

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

108
/**
109
 * NIST FIPS 204, Algorithm 16 (SimpleBitPack)
110
 * (for a = (q-1)/(2*gamma2-1))
111
 */
112
void poly_pack_w1(const DilithiumPoly& p, BufferStuffer& stuffer, const DilithiumConstants& mode) {
66,488✔
113
   using Gamma2 = DilithiumConstants::DilithiumGamma2;
66,488✔
114
   auto calculate_b = [](auto gamma2) { return ((DilithiumConstants::Q - 1) / (2 * gamma2)) - 1; };
66,488✔
115
   switch(mode.gamma2()) {
66,488✔
116
      case Gamma2::Qminus1DevidedBy88:
13,952✔
117
         return poly_pack<0, calculate_b(Gamma2::Qminus1DevidedBy88)>(p, stuffer);
13,952✔
118
      case Gamma2::Qminus1DevidedBy32:
52,536✔
119
         return poly_pack<0, calculate_b(Gamma2::Qminus1DevidedBy32)>(p, stuffer);
52,536✔
120
   }
121

122
   BOTAN_ASSERT_UNREACHABLE();
×
123
}
124

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

138
   BOTAN_ASSERT_UNREACHABLE();
×
139
}
140

141
#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
142

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

156
   BOTAN_ASSERT_UNREACHABLE();
×
157
}
158

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

168
#endif
169

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

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

197
   BOTAN_ASSERT_UNREACHABLE();
×
198
}
199

200
#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
201

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

215
   BOTAN_ASSERT_UNREACHABLE();
×
216
}
217

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

227
#endif
228

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

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

239
   uint8_t index = 0;
1,515✔
240
   for(const auto& p : h) {
10,611✔
241
      for(size_t i = 0; i < p.size(); ++i) {
2,337,672✔
242
         if(p[i] == 1) {
2,328,576✔
243
            bit_positions.append(static_cast<uint8_t>(i));
78,525✔
244
            ++index;
78,525✔
245
         }
246
      }
247
      offsets.append(index);
9,096✔
248
   }
249

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

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

261
   DilithiumPolyVec hint(mode.k());
4,450✔
262
   uint8_t index = 0;
4,450✔
263
   for(auto& p : hint) {
31,146✔
264
      const auto end_index = offsets.take_byte();
26,696✔
265

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

271
      const auto set_bits = bit_positions.take(end_index - index);
26,696✔
272

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

281
      // Set the specified bits in the polynomial
282
      for(const auto i : set_bits) {
260,273✔
283
         p[i] = 1;
233,577✔
284
      }
285

286
      index = end_index;
26,696✔
287
   }
288

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

295
   BOTAN_DEBUG_ASSERT(hint.ct_validate_value_range(0, 1));
4,450✔
296
   return hint;
4,450✔
297
}
4,450✔
298

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

319
   return Dilithium_Algos::power2round(t);
3,036✔
320
}
1,518✔
321

322
}  // namespace
323

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

333
   stuffer.append(rho);
5,900✔
334
   for(const auto& p : t1) {
41,324✔
335
      poly_pack_t1(p, stuffer);
70,848✔
336
   }
337

338
   BOTAN_ASSERT_NOMSG(stuffer.full());
5,900✔
339
   return pk;
5,900✔
340
}
×
341

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

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

354
   DilithiumPolyVec t1(mode.k());
1,525✔
355
   for(auto& p : t1) {
10,689✔
356
      poly_unpack_t1(p, slicer);
18,328✔
357
   }
358
   BOTAN_ASSERT_NOMSG(slicer.empty());
1,525✔
359

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

363
#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
364

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

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

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

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

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

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

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

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

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

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

414
   BufferSlicer slicer(sk);
67✔
415

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

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

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

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

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

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

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

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

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

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

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

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

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

466
   return keypair;
134✔
467
}
134✔
468

469
#endif
470

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

481
   stuffer.append(c);
1,515✔
482
   for(const auto& p : response) {
9,570✔
483
      poly_pack_gamma1(p, stuffer, mode);
8,055✔
484
   }
485
   hint_pack(hint, stuffer, mode);
1,515✔
486

487
   return sig;
1,515✔
488
}
×
489

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

498
   auto commitment_hash = slicer.copy<DilithiumCommitmentHash>(mode.commitment_hash_full_bytes());
4,450✔
499

500
   DilithiumPolyVec response(mode.l());
4,450✔
501
   for(auto& p : response) {
28,159✔
502
      poly_unpack_gamma1(p, slicer, mode);
23,709✔
503
   }
504
   BOTAN_ASSERT_NOMSG(slicer.remaining() == mode.omega() + mode.k());
4,450✔
505

506
   auto hint = hint_unpack(slicer, mode);
4,450✔
507
   BOTAN_ASSERT_NOMSG(slicer.empty());
4,450✔
508
   if(!hint.has_value()) {
4,450✔
509
      return std::nullopt;
×
510
   }
511

512
   return std::make_tuple(std::move(commitment_hash), std::move(response), std::move(hint.value()));
4,450✔
513
}
4,450✔
514

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

522
   for(const auto& p : w1) {
77,582✔
523
      poly_pack_w1(p, stuffer, mode);
66,488✔
524
   }
525

526
   return commitment;
11,094✔
527
}
×
528

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

537
   DilithiumPoly c;
11,094✔
538
   uint64_t signs = load_le(bounded_xof.next<8>());
11,094✔
539
   for(size_t i = c.size() - mode.tau(); i < c.size(); ++i) {
557,770✔
540
      const auto j = bounded_xof.next_byte([i](uint8_t byte) { return byte <= i; });
607,728✔
541
      c[i] = c[j];
546,676✔
542
      c[j] = 1 - 2 * (signs & 1);
546,676✔
543
      signs >>= 1;
546,676✔
544
   }
545

546
   BOTAN_DEBUG_ASSERT(c.ct_validate_value_range(-1, 1));
11,094✔
547
   BOTAN_DEBUG_ASSERT(c.hamming_weight() == mode.tau());
11,094✔
548

549
   return c;
11,094✔
550
}
×
551

552
namespace {
553

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

568
   for(auto& coeff : p) {
41,200,698✔
569
      coeff =
82,080,768✔
570
         bounded_xof.next<3>([](const auto bytes) { return make_uint32(0, bytes[2], bytes[1], bytes[0]) & 0x7FFFFF; },
82,120,162✔
571
                             [](const uint32_t z) { return z < DilithiumConstants::Q; });
41,040,384✔
572
   }
573

574
   BOTAN_DEBUG_ASSERT(p.ct_validate_value_range(0, DilithiumConstants::Q - 1));
160,314✔
575
}
160,314✔
576

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

586
   if constexpr(eta == DilithiumConstants::DilithiumEta::_2) {
587
      if(CT::driveby_unpoison(b < 15)) {
3,014,346✔
588
         b = b - (205 * b >> 10) * 5;  // b = b mod 5
2,825,367✔
589
         return 2 - b;
2,825,367✔
590
      }
591
   } else if constexpr(eta == DilithiumConstants::DilithiumEta::_4) {
592
      if(CT::driveby_unpoison(b < 9)) {
2,471,872✔
593
         return 4 - b;
1,389,802✔
594
      }
595
   }
596

597
   return std::nullopt;
598
}
599

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

611
      BOTAN_DEBUG_ASSERT(!stashed_coeff.has_value());
2,743,109✔
612
      while(true) {
242,457✔
613
         const auto b = bounded_xof.next_byte();
5,486,218✔
614
         const auto z0 = coeff_from_halfbyte<eta>(b & 0x0F);
2,743,109✔
615
         const auto z1 = coeff_from_halfbyte<eta>(b >> 4);
4,851,203✔
616

617
         if(z0.has_value()) {
2,743,109✔
618
            stashed_coeff = z1;  // keep candidate z1 for the next invocation
2,107,075✔
619
            return *z0;
2,500,652✔
620
         } else if(z1.has_value()) {
636,034✔
621
            // z0 was invalid, z1 is valid, nothing to stash
622
            return *z1;
623
         }
624
      }
625
   };
626

627
   for(auto& coeff : p) {
4,225,080✔
628
      coeff = next_coeff();
4,208,640✔
629
   }
630
}
16,440✔
631

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

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

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

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

657
}  // namespace
658

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

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

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

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

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

684
   CT::unpoison(*keypair.second);
1,451✔
685

686
   return keypair;
1,451✔
687
};
2,902✔
688

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

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

713
   uint16_t nonce = 0;
1,451✔
714
   for(auto& p : s1) {
9,185✔
715
      sample_uniform_eta(rhoprime, p, nonce++, mode);
7,734✔
716
   }
717

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

722
   return result;
1,451✔
723
}
×
724

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

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

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

756
   for(size_t i = 0; i < vec.size(); ++i) {
10,636✔
757
      for(size_t j = 0; j < vec[i].size(); ++j) {
2,343,326✔
758
         std::tie(result.first[i][j], result.second[i][j]) = power2round(vec[i][j]);
2,334,208✔
759
      }
760
   }
761

762
   return result;
1,518✔
763
}
764

765
namespace {
766

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

778
   if constexpr(gamma2 == DilithiumConstants::DilithiumGamma2::Qminus1DevidedBy32) {
779
      r1 = (r1 * 1025 + (1 << 21)) >> 22;
13,449,216✔
780
      r1 &= 15;
13,449,216✔
781
   } else if constexpr(gamma2 == DilithiumConstants::DilithiumGamma2::Qminus1DevidedBy88) {
782
      r1 = (r1 * 11275 + (1 << 23)) >> 24;
3,571,712✔
783
      r1 = is_negative_mask(43 - r1).if_not_set_return(r1);
3,571,712✔
784
   }
785

786
   int32_t r0 = r - r1 * 2 * gamma2;
17,020,928✔
787

788
   // reduce r0 mod q
789
   r0 -= is_negative_mask((DilithiumConstants::Q - 1) / 2 - r0).if_set_return(DilithiumConstants::Q);
8,113,152✔
790

791
   return {r1, r0};
8,113,152✔
792
}
793

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

802
   for(size_t i = 0; i < vec.size(); ++i) {
46,436✔
803
      for(size_t j = 0; j < vec[i].size(); ++j) {
10,226,544✔
804
         std::tie(result.first[i][j], result.second[i][j]) = decompose<gamma2>(vec[i][j]);
10,186,752✔
805
      }
806
   }
807

808
   return result;
6,644✔
809
}
810

811
}  // namespace
812

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

830
   BOTAN_ASSERT_UNREACHABLE();
×
831
}
832

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

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

852
      const uint32_t pc0 = static_cast<uint32_t>(c0);
2,348,544✔
853
      const uint32_t pc1 = static_cast<uint32_t>(c1);
2,348,544✔
854

855
      return (CT::Mask<uint32_t>::is_gt(pc0, gamma2) & CT::Mask<uint32_t>::is_lte(pc0, q_gamma2) &
2,348,544✔
856
              ~(CT::Mask<uint32_t>::is_equal(pc0, q_gamma2) & CT::Mask<uint32_t>::is_zero(pc1)))
2,348,544✔
857
         .as_choice();
2,348,544✔
858
   };
1,529✔
859

860
   DilithiumPolyVec hint(r.size());
1,529✔
861

862
   for(size_t i = 0; i < r.size(); ++i) {
10,703✔
863
      for(size_t j = 0; j < r[i].size(); ++j) {
2,357,718✔
864
         hint[i][j] = make_hint(z[i][j], r[i][j]).as_bool();
2,348,544✔
865
      }
866
   }
867

868
   BOTAN_DEBUG_ASSERT(hint.ct_validate_value_range(0, 1));
1,529✔
869

870
   return hint;
1,529✔
871
}
872

873
namespace {
874

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

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

888
   auto use_hint = [&modulo_m](bool hint, int32_t r) -> int32_t {
6,838,626✔
889
      auto [r1, r0] = decompose<gamma2>(r);
6,834,176✔
890

891
      if(!hint) {
6,834,176✔
892
         return r1;
893
      }
894

895
      if(r0 > 0) {
233,577✔
896
         return modulo_m(r1 + 1);
116,610✔
897
      } else {
898
         return modulo_m(r1 - 1);
116,967✔
899
      }
900
   };
901

902
   for(size_t i = 0; i < vec.size(); ++i) {
31,146✔
903
      for(size_t j = 0; j < vec[i].size(); ++j) {
6,860,872✔
904
         vec[i][j] = use_hint(hints[i][j], vec[i][j]);
6,834,176✔
905
      }
906
   }
907
}
4,450✔
908

909
}  // namespace
910

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

923
   using Gamma2 = DilithiumConstants::DilithiumGamma2;
4,450✔
924
   switch(mode.gamma2()) {
4,450✔
925
      case Gamma2::Qminus1DevidedBy32:
2,987✔
926
         use_hint_on_coefficients<Gamma2::Qminus1DevidedBy32>(hints, vec);
2,987✔
927
         break;
2,987✔
928
      case Gamma2::Qminus1DevidedBy88:
1,463✔
929
         use_hint_on_coefficients<Gamma2::Qminus1DevidedBy88>(hints, vec);
1,463✔
930
         break;
1,463✔
931
   }
932

933
   BOTAN_DEBUG_ASSERT(vec.ct_validate_value_range(0, (DilithiumConstants::Q - 1) / (2 * mode.gamma2())));
4,450✔
934
}
4,450✔
935

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

939
   // It is ok to leak which coefficient violates the bound as the probability
940
   // for each coefficient is independent of secret data but we must not leak
941
   // the sign of the centralized representative.
942
   for(const auto& p : vec) {
90,788✔
943
      for(auto c : p) {
19,688,961✔
944
         const auto abs_c = c - is_negative_mask(c).if_set_return(2 * c);
19,614,841✔
945
         if(CT::driveby_unpoison(abs_c >= bound)) {
19,614,841✔
946
            return false;
16,668✔
947
         }
948
      }
949
   }
950

951
   return true;
952
}
953

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

© 2025 Coveralls, Inc