Botan 3.11.0
Crypto and TLS for C&
dilithium_algos.cpp
Go to the documentation of this file.
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/buffer_slicer.h>
22#include <botan/internal/buffer_stuffer.h>
23#include <botan/internal/ct_utils.h>
24#include <botan/internal/dilithium_keys.h>
25#include <botan/internal/dilithium_symmetric_primitives.h>
26#include <botan/internal/loadstor.h>
27#include <botan/internal/pqcrystals_encoding.h>
28#include <botan/internal/pqcrystals_helpers.h>
29#include <utility>
30
32
33namespace {
34
35/**
36 * Returns an all-one mask if @p x is negative, otherwise an all-zero mask.
37 */
38template <std::signed_integral T>
39constexpr auto is_negative_mask(T x) {
40 using unsigned_T = std::make_unsigned_t<T>;
41 return CT::Mask<unsigned_T>::expand_top_bit(static_cast<unsigned_T>(x));
42}
43
44template <DilithiumConstants::T b>
45constexpr std::make_unsigned_t<DilithiumConstants::T> map_range(DilithiumConstants::T c) {
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);
49 return b - c;
50}
51
52template <DilithiumConstants::T b>
53constexpr DilithiumConstants::T unmap_range(std::make_unsigned_t<DilithiumConstants::T> c) {
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);
57}
58
59template <DilithiumConstants::T a, DilithiumConstants::T b, CRYSTALS::crystals_trait PolyTrait, CRYSTALS::Domain D>
60constexpr void poly_pack(const CRYSTALS::Polynomial<PolyTrait, D>& p, BufferStuffer& stuffer) {
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);
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>);
69 }
70}
71
72template <DilithiumConstants::T a,
74 CRYSTALS::byte_source ByteSourceT,
77constexpr void poly_unpack(CRYSTALS::Polynomial<PolyTrait, D>& p, ByteSourceT& get_bytes, bool check_range = false) {
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);
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>);
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 (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)) {
94 throw Decoding_Error("Decoded polynomial coefficients out of range");
95 }
96}
97
98/**
99 * NIST FIPS 204, Algorithm 16 (SimpleBitPack)
100 * (for a = 2^(bitlen(q-1)-d) - 1)
101 */
102void poly_pack_t1(const DilithiumPoly& p, BufferStuffer& stuffer) {
103 constexpr auto b = (1 << (bitlen(DilithiumConstants::Q - 1) - DilithiumConstants::D)) - 1;
104 poly_pack<0, b>(p, stuffer);
105}
106
107/**
108 * NIST FIPS 204, Algorithm 16 (SimpleBitPack)
109 * (for a = (q-1)/(2*gamma2-1))
110 */
111void poly_pack_w1(const DilithiumPoly& p, BufferStuffer& stuffer, const DilithiumConstants& mode) {
113 auto calculate_b = [](auto gamma2) { return ((DilithiumConstants::Q - 1) / (2 * gamma2)) - 1; };
114 switch(mode.gamma2()) {
115 case Gamma2::Qminus1DividedBy88:
116 return poly_pack<0, calculate_b(Gamma2::Qminus1DividedBy88)>(p, stuffer);
117 case Gamma2::Qminus1DividedBy32:
118 return poly_pack<0, calculate_b(Gamma2::Qminus1DividedBy32)>(p, stuffer);
119 }
120
122}
123
124/**
125 * NIST FIPS 204, Algorithm 17 (BitPack)
126 * (for a = -gamma1 - 1, b = gamma1)
127 */
128void poly_pack_gamma1(const DilithiumPoly& p, BufferStuffer& stuffer, const DilithiumConstants& mode) {
130 switch(mode.gamma1()) {
131 case Gamma1::ToThe17th:
132 return poly_pack<Gamma1::ToThe17th - 1, Gamma1::ToThe17th>(p, stuffer);
133 case Gamma1::ToThe19th:
134 return poly_pack<Gamma1::ToThe19th - 1, Gamma1::ToThe19th>(p, stuffer);
135 }
136
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 */
146void poly_pack_eta(const DilithiumPoly& p, BufferStuffer& stuffer, const DilithiumConstants& mode) {
148 switch(mode.eta()) {
149 case Eta::_2:
150 return poly_pack<Eta::_2, Eta::_2>(p, stuffer);
151 case Eta::_4:
152 return poly_pack<Eta::_4, Eta::_4>(p, stuffer);
153 }
154
156}
157
158/**
159 * NIST FIPS 204, Algorithm 17 (BitPack)
160 * (for a = -2^(d-1) - 1, b = 2^(d-1))
161 */
162void poly_pack_t0(const DilithiumPoly& p, BufferStuffer& stuffer) {
163 constexpr auto TwoToTheDminus1 = 1 << (DilithiumConstants::D - 1);
164 poly_pack<TwoToTheDminus1 - 1, TwoToTheDminus1>(p, stuffer);
165}
166
167#endif
168
169/**
170 * NIST FIPS 204, Algorithm 18 (SimpleBitUnpack)
171 * (for a = 2^(bitlen(q-1)-d) - 1)
172 */
173void poly_unpack_t1(DilithiumPoly& p, BufferSlicer& slicer) {
174 constexpr auto b = (1 << (bitlen(DilithiumConstants::Q - 1) - DilithiumConstants::D)) - 1;
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));
179 poly_unpack<0, b>(p, slicer);
180}
181
182/**
183 * NIST FIPS 204, Algorithm 19 (BitUnpack)
184 * (for a = -gamma1 - 1, b = gamma1)
185 */
186template <typename ByteSourceT>
187void poly_unpack_gamma1(DilithiumPoly& p, ByteSourceT& byte_source, const DilithiumConstants& mode) {
189 switch(mode.gamma1()) {
190 case Gamma1::ToThe17th:
191 return poly_unpack<Gamma1::ToThe17th - 1, Gamma1::ToThe17th>(p, byte_source);
192 case Gamma1::ToThe19th:
193 return poly_unpack<Gamma1::ToThe19th - 1, Gamma1::ToThe19th>(p, byte_source);
194 }
195
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 */
205void poly_unpack_eta(DilithiumPoly& p, BufferSlicer& slicer, const DilithiumConstants& mode, bool check_range = false) {
207 switch(mode.eta()) {
208 case Eta::_2:
209 return poly_unpack<Eta::_2, Eta::_2>(p, slicer, check_range);
210 case Eta::_4:
211 return poly_unpack<Eta::_4, Eta::_4>(p, slicer, check_range);
212 }
213
215}
216
217/**
218 * NIST FIPS 204, Algorithm 19 (BitUnpack)
219 * (for a = -2^(d-1) - 1, b = 2^(d-1))
220 */
221void poly_unpack_t0(DilithiumPoly& p, BufferSlicer& slicer) {
222 constexpr auto TwoToTheDminus1 = 1 << (DilithiumConstants::D - 1);
223 poly_unpack<TwoToTheDminus1 - 1, TwoToTheDminus1>(p, slicer);
224}
225
226#endif
227
228/**
229 * NIST FIPS 204, Algorithm 20 (HintBitPack)
230 */
231void hint_pack(const DilithiumPolyVec& h, BufferStuffer& stuffer, const DilithiumConstants& mode) {
232 BOTAN_ASSERT_NOMSG(h.size() == mode.k());
234
235 BufferStuffer bit_positions(stuffer.next(mode.omega()));
236 BufferStuffer offsets(stuffer.next(mode.k()));
237
238 uint8_t index = 0;
239 for(const auto& p : h) {
240 for(size_t i = 0; i < p.size(); ++i) {
241 if(p[i] == 1) {
242 bit_positions.append(static_cast<uint8_t>(i));
243 ++index;
244 }
245 }
246 offsets.append(index);
247 }
248
249 // Fill the remaining bit positions with zeros
250 bit_positions.append(0, bit_positions.remaining_capacity());
251}
252
253/**
254 * NIST FIPS 204, Algorithm 21 (HintBitUnpack)
255 */
256std::optional<DilithiumPolyVec> hint_unpack(BufferSlicer& slicer, const DilithiumConstants& mode) {
257 BufferSlicer bit_positions(slicer.take(mode.omega()));
258 BufferSlicer offsets(slicer.take(mode.k()));
259
260 DilithiumPolyVec hint(mode.k());
261 uint8_t index = 0;
262 for(auto& p : hint) {
263 const auto end_index = offsets.take_byte();
264
265 // Check the bounds of the end index for this polynomial
266 if(end_index < index || end_index > mode.omega()) {
267 return std::nullopt;
268 }
269
270 const auto set_bits = bit_positions.take(end_index - index);
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) {
275 if(set_bits[i] <= set_bits[i - 1]) {
276 return std::nullopt;
277 }
278 }
279
280 // Set the specified bits in the polynomial
281 for(const auto i : set_bits) {
282 p[i] = 1;
283 }
284
285 index = end_index;
286 }
287
288 // Check that the remaining bit positions are all zero (strong unforgeability)
289 uint8_t sum = 0;
290 for(const uint8_t b : bit_positions.take(bit_positions.remaining())) {
291 sum |= b;
292 }
293
294 if(sum != 0) {
295 return std::nullopt;
296 }
297
299 return hint;
300}
301
302/**
303 * NIST FIPS 204, Algorithm 6, lines 5-7 (ML-DSA.KeyGen_internal)
304 *
305 * We have to expose this independently to derive the public key from the
306 * private key when loading a key pair from a serialized private key. This
307 * is needed because of Botan's design decision to let the private key
308 * class inherit from the public key class.
309 *
310 * TODO(Botan4): This should be refactored after PrivateKey does not inherit
311 * from PublicKey anymore.
312 */
313std::pair<DilithiumPolyVec, DilithiumPolyVec> compute_t1_and_t0(const DilithiumPolyMatNTT& A,
314 const DilithiumPolyVec& s1,
315 const DilithiumPolyVec& s2) {
316 auto t_hat = A * ntt(s1.clone());
317 t_hat.reduce();
318 auto t = inverse_ntt(std::move(t_hat));
319 t += s2;
321
323}
324
325} // namespace
326
327/**
328 * NIST FIPS 204, Algorithm 22 (pkEncode)
329 */
331 const DilithiumPolyVec& t1,
332 const DilithiumConstants& mode) {
334 BufferStuffer stuffer(pk);
335
336 stuffer.append(rho);
337 for(const auto& p : t1) {
338 poly_pack_t1(p, stuffer);
339 }
340
341 BOTAN_ASSERT_NOMSG(stuffer.full());
342 return pk;
343}
344
345/**
346 * NIST FIPS 204, Algorithm 23 (pkDecode)
347 */
348std::pair<DilithiumSeedRho, DilithiumPolyVec> decode_public_key(StrongSpan<const DilithiumSerializedPublicKey> pk,
349 const DilithiumConstants& mode) {
350 if(pk.size() != mode.public_key_bytes()) {
351 throw Decoding_Error("Dilithium: Invalid public key length");
352 }
353
354 BufferSlicer slicer(pk);
356
357 DilithiumPolyVec t1(mode.k());
358 for(auto& p : t1) {
359 poly_unpack_t1(p, slicer);
360 }
361 BOTAN_ASSERT_NOMSG(slicer.empty());
362
363 return {std::move(rho), std::move(t1)};
364}
365
366#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
367
368/**
369 * NIST FIPS 204, Algorithm 24 (skEncode)
370 */
371DilithiumSerializedPrivateKey encode_keypair(const DilithiumInternalKeypair& keypair) {
372 const auto& [pk, sk] = keypair;
375 const auto& mode = sk->mode();
376 auto scope = CT::scoped_poison(*sk);
377
378 DilithiumSerializedPrivateKey serialization(mode.private_key_bytes());
379 BufferStuffer stuffer(serialization);
380
381 stuffer.append(pk->rho());
382 stuffer.append(sk->signing_seed());
383 stuffer.append(pk->tr());
384
385 for(const auto& p : sk->s1()) {
386 poly_pack_eta(p, stuffer, mode);
387 }
388
389 for(const auto& p : sk->s2()) {
390 poly_pack_eta(p, stuffer, mode);
391 }
392
393 for(const auto& p : sk->t0()) {
394 poly_pack_t0(p, stuffer);
395 }
396
397 BOTAN_ASSERT_NOMSG(stuffer.full());
398 CT::unpoison(serialization);
399
400 return serialization;
401}
402
403/**
404 * NIST FIPS 204, Algorithm 25 (skDecode)
405 *
406 * Because Botan's Private_Key class inherits from Public_Key, we have to
407 * derive the public key from the private key here.
408 *
409 * TODO(Botan4): This should be refactored after PrivateKey does not inherit
410 * from PublicKey anymore.
411 */
412DilithiumInternalKeypair decode_keypair(StrongSpan<const DilithiumSerializedPrivateKey> sk, DilithiumConstants mode) {
413 auto scope = CT::scoped_poison(sk);
414
415 BOTAN_ASSERT_NOMSG(sk.size() == mode.private_key_bytes());
416
417 BufferSlicer slicer(sk);
418
421 auto tr = slicer.copy<DilithiumHashedPublicKey>(mode.public_key_hash_bytes());
422
423 DilithiumPolyVec s1(mode.l());
424 for(auto& p : s1) {
425 poly_unpack_eta(p, slicer, mode, true /* check decoded value range */);
426 }
427
428 DilithiumPolyVec s2(mode.k());
429 for(auto& p : s2) {
430 poly_unpack_eta(p, slicer, mode, true /* check decoded value range */);
431 }
432
433 DilithiumPolyVec t0(mode.k());
434 for(auto& p : t0) {
435 poly_unpack_t0(p, slicer);
436 }
437
438 BOTAN_ASSERT_NOMSG(slicer.empty());
439
440 // Currently, Botan's Private_Key class inherits from Public_Key, forcing us
441 // to derive the public key from the private key here.
442 // TODO(Botan4): Reconsider once PrivateKey/PublicKey issue is tackled.
443
444 CT::unpoison(rho); // rho is public (used in rejection sampling of matrix A)
445
446 const auto A = expand_A(rho, mode);
447 auto [t1, _] = compute_t1_and_t0(A, s1, s2);
448
449 CT::unpoison(t1); // part of the public key
450
452 std::make_shared<Dilithium_PublicKeyInternal>(mode, std::move(rho), std::move(t1)),
453 std::make_shared<Dilithium_PrivateKeyInternal>(std::move(mode),
454 std::nullopt, // decoding cannot recover the private seed
455 std::move(K),
456 std::move(s1),
457 std::move(s2),
458 std::move(t0)),
459 };
460
461 CT::unpoison(tr); // hash of the public key
462
463 if(keypair.first->tr() != tr) {
464 throw Decoding_Error("Calculated dilithium public key hash does not match the one stored in the private key");
465 }
466
467 CT::unpoison(*keypair.second);
468
469 return keypair;
470}
471
472#endif
473
474/**
475 * NIST FIPS 204, Algorithm 26 (sigEncode)
476 */
478 const DilithiumPolyVec& response,
479 const DilithiumPolyVec& hint,
480 const DilithiumConstants& mode) {
482 BufferStuffer stuffer(sig);
483
484 stuffer.append(c);
485 for(const auto& p : response) {
486 poly_pack_gamma1(p, stuffer, mode);
487 }
488 hint_pack(hint, stuffer, mode);
489
490 return sig;
491}
492
493/**
494 * NIST FIPS 204, Algorithm 27 (sigDecode)
495 */
496std::optional<std::tuple<DilithiumCommitmentHash, DilithiumPolyVec, DilithiumPolyVec>> decode_signature(
498 BufferSlicer slicer(sig);
499 BOTAN_ASSERT_NOMSG(slicer.remaining() == mode.signature_bytes());
500
501 auto commitment_hash = slicer.copy<DilithiumCommitmentHash>(mode.commitment_hash_full_bytes());
502
503 DilithiumPolyVec response(mode.l());
504 for(auto& p : response) {
505 poly_unpack_gamma1(p, slicer, mode);
506 }
507 BOTAN_ASSERT_NOMSG(slicer.remaining() == size_t(mode.omega()) + mode.k());
508
509 auto hint = hint_unpack(slicer, mode);
510 BOTAN_ASSERT_NOMSG(slicer.empty());
511 if(!hint.has_value()) {
512 return std::nullopt;
513 }
514
515 return std::make_tuple(std::move(commitment_hash), std::move(response), std::move(hint.value()));
516}
517
518/**
519 * NIST FIPS 204, Algorithm 28 (w1Encode)
520 */
523 BufferStuffer stuffer(commitment);
524
525 for(const auto& p : w1) {
526 poly_pack_w1(p, stuffer, mode);
527 }
528
529 return commitment;
530}
531
532/**
533 * NIST FIPS 204, Algorithm 29 (SampleInBall)
534 */
536 // This generator resembles the while loop in the spec.
537 auto xof = mode.symmetric_primitives().H(seed);
539
541 uint64_t signs = load_le(bounded_xof.next<8>());
542 for(size_t i = c.size() - mode.tau(); i < c.size(); ++i) {
543 const auto j = bounded_xof.next_byte([i](uint8_t byte) { return byte <= i; });
544 c[i] = c[j];
545 c[j] = 1 - 2 * (signs & 1);
546 signs >>= 1;
547 }
548
551
552 return c;
553}
554
555namespace {
556
557/**
558 * NIST FIPS 204, Algorithm 30 (RejNTTPoly)
559 */
560void sample_ntt_uniform(StrongSpan<const DilithiumSeedRho> rho,
562 uint16_t nonce,
563 const DilithiumConstants& mode) {
564 /**
565 * A generator that returns the next coefficient sampled from the XOF,
566 * according to: NIST FIPS 204, Algorithm 14 (CoeffFromThreeBytes).
567 */
568 auto xof = mode.symmetric_primitives().H(rho, nonce);
570
571 for(auto& coeff : p) {
572 coeff =
573 bounded_xof.next<3>([](const auto bytes) { return make_uint32(0, bytes[2], bytes[1], bytes[0]) & 0x7FFFFF; },
574 [](const uint32_t z) { return z < DilithiumConstants::Q; });
575 }
576
577 BOTAN_DEBUG_ASSERT(p.ct_validate_value_range(0, DilithiumConstants::Q - 1));
578}
579
580/**
581 * NIST FIPS 204, Algorithm 15 (CoeffFromHalfByte)
582 *
583 * Magic numbers for (b mod 5) are taken from the reference implementation.
584 */
585template <DilithiumConstants::DilithiumEta eta>
586std::optional<int32_t> coeff_from_halfbyte(uint8_t b) {
587 BOTAN_DEBUG_ASSERT(b < 16);
588
589 if constexpr(eta == DilithiumConstants::DilithiumEta::_2) {
590 if(CT::driveby_unpoison(b < 15)) {
591 b = b - (205 * b >> 10) * 5; // b = b mod 5
592 return 2 - b;
593 }
594 } else if constexpr(eta == DilithiumConstants::DilithiumEta::_4) {
595 if(CT::driveby_unpoison(b < 9)) {
596 return 4 - b;
597 }
598 }
599
600 return std::nullopt;
601}
602
603template <DilithiumConstants::DilithiumEta eta>
604void sample_uniform_eta(DilithiumPoly& p, Botan::XOF& xof) {
605 // A generator that returns the next coefficient sampled from the XOF. As the
606 // sampling uses half-bytes, this keeps track of the additionally sampled
607 // coefficient as needed.
608 auto next_coeff = [bounded_xof = Bounded_XOF<DilithiumConstants::SAMPLE_POLY_FROM_XOF_BOUND>(xof),
609 stashed_coeff = std::optional<int32_t>{}]() mutable -> int32_t {
610 if(auto stashed = std::exchange(stashed_coeff, std::nullopt)) {
611 return *stashed;
612 }
613
614 BOTAN_DEBUG_ASSERT(!stashed_coeff.has_value());
615 while(true) {
616 const auto b = bounded_xof.next_byte();
617 const auto z0 = coeff_from_halfbyte<eta>(b & 0x0F);
618 const auto z1 = coeff_from_halfbyte<eta>(b >> 4);
619
620 if(z0.has_value()) {
621 stashed_coeff = z1; // keep candidate z1 for the next invocation
622 return *z0;
623 } else if(z1.has_value()) {
624 // z0 was invalid, z1 is valid, nothing to stash
625 return *z1;
626 }
627 }
628 };
629
630 for(auto& coeff : p) {
631 coeff = next_coeff();
632 }
633}
634
635/**
636 * NIST FIPS 204, Algorithm 31 (RejBoundedPoly)
637 */
638void sample_uniform_eta(StrongSpan<const DilithiumSeedRhoPrime> rhoprime,
639 DilithiumPoly& p,
640 uint16_t nonce,
641 const DilithiumConstants& mode) {
643
644 auto xof = mode.symmetric_primitives().H(rhoprime, nonce);
645 switch(mode.eta()) {
646 case Eta::_2:
647 sample_uniform_eta<Eta::_2>(p, *xof);
648 break;
649 case Eta::_4:
650 sample_uniform_eta<Eta::_4>(p, *xof);
651 break;
652 }
653
654 // Rejection sampling is done. Secret polynomial can be repoisoned.
655 CT::poison(p);
656
657 BOTAN_DEBUG_ASSERT(p.ct_validate_value_range(-static_cast<int32_t>(mode.eta()), mode.eta()));
658}
659
660} // namespace
661
662/**
663 * NIST FIPS 204, Algorithm 6 (ML-DSA.KeyGen_internal)
664 *
665 * Lines 5-7 are extracted into a separate function, see above. The key
666 * encoding is deferred until the user explicitly invokes the encoding.
667 */
669 const auto& sympriv = mode.symmetric_primitives();
670 CT::poison(xi);
671
672 auto [rho, rhoprime, K] = sympriv.H(xi);
673 CT::unpoison(rho); // rho is public (seed for the public matrix A)
674
675 const auto A = Dilithium_Algos::expand_A(rho, mode);
676 auto [s1, s2] = Dilithium_Algos::expand_s(rhoprime, mode);
677 auto [t1, t0] = Dilithium_Algos::compute_t1_and_t0(A, s1, s2);
678
679 CT::unpoison(t1); // part of the public key
680
682 std::make_shared<Dilithium_PublicKeyInternal>(mode, std::move(rho), std::move(t1)),
683 std::make_shared<Dilithium_PrivateKeyInternal>(
684 std::move(mode), std::move(xi), std::move(K), std::move(s1), std::move(s2), std::move(t0)),
685 };
686
687 CT::unpoison(*keypair.second);
688
689 return keypair;
690}
691
692/**
693 * NIST FIPS 204, Algorithm 32 (ExpandA)
694 *
695 * Note that the actual concatenation of rho, s and r is done downstream
696 * in the sampling function.
697 */
699 DilithiumPolyMatNTT A(mode.k(), mode.l());
700 for(uint8_t r = 0; r < mode.k(); ++r) {
701 for(uint8_t s = 0; s < mode.l(); ++s) {
702 sample_ntt_uniform(rho, A[r][s], load_le(std::array{s, r}), mode);
703 }
704 }
705 return A;
706}
707
708/**
709 * NIST FIPS 204, Algorithm 33 (ExpandS)
710 */
711std::pair<DilithiumPolyVec, DilithiumPolyVec> expand_s(StrongSpan<const DilithiumSeedRhoPrime> rhoprime,
712 const DilithiumConstants& mode) {
713 auto result = std::make_pair(DilithiumPolyVec(mode.l()), DilithiumPolyVec(mode.k()));
714 auto& [s1, s2] = result;
715
716 uint16_t nonce = 0;
717 for(auto& p : s1) {
718 sample_uniform_eta(rhoprime, p, nonce++, mode);
719 }
720
721 for(auto& p : s2) {
722 sample_uniform_eta(rhoprime, p, nonce++, mode);
723 }
724
725 return result;
726}
727
728/**
729 * NIST FIPS 204, Algorithm 34 (ExpandMask)
730 */
732 uint16_t nonce,
733 const DilithiumConstants& mode) {
734 DilithiumPolyVec s(mode.l());
735 for(auto& p : s) {
736 auto xof = mode.symmetric_primitives().H(rhoprime, nonce++);
737 poly_unpack_gamma1(p, *xof, mode);
738 }
739 return s;
740}
741
742/**
743 * NIST FIPS 204, Algorithm 35 (Power2Round)
744 *
745 * In contrast to the spec, this function takes a polynomial vector and
746 * performs the power2round operation on each coefficient in the vector.
747 * The actual Algorithm 35 as specified is actually just the inner lambda.
748 */
749std::pair<DilithiumPolyVec, DilithiumPolyVec> power2round(const DilithiumPolyVec& vec) {
750 // This procedure is taken verbatim from Dilithium's reference implementation.
751 auto power2round = [d = DilithiumConstants::D](int32_t r) -> std::pair<int32_t, int32_t> {
752 const int32_t r1 = (r + (1 << (d - 1)) - 1) >> d;
753 const int32_t r0 = r - (r1 << d);
754 return {r1, r0};
755 };
756
757 auto result = std::make_pair(DilithiumPolyVec(vec.size()), DilithiumPolyVec(vec.size()));
758
759 for(size_t i = 0; i < vec.size(); ++i) {
760 for(size_t j = 0; j < vec[i].size(); ++j) {
761 std::tie(result.first[i][j], result.second[i][j]) = power2round(vec[i][j]);
762 }
763 }
764
765 return result;
766}
767
768namespace {
769
770/**
771 * NIST FIPS 204, Algorithm 36 (Decompose)
772 *
773 * The implementation is adapted from the verbatim reference implementation using
774 * the magic numbers that depend on the values of Q and gamma2 and ensure that
775 * this operation is done in constant-time.
776 */
777template <DilithiumConstants::DilithiumGamma2 gamma2>
778std::pair<int32_t, int32_t> decompose(int32_t r) {
779 int32_t r1 = (r + 127) >> 7;
780
782 r1 = (r1 * 1025 + (1 << 21)) >> 22;
783 r1 &= 15;
784 } else if constexpr(gamma2 == DilithiumConstants::DilithiumGamma2::Qminus1DividedBy88) {
785 r1 = (r1 * 11275 + (1 << 23)) >> 24;
786 r1 = is_negative_mask(43 - r1).if_not_set_return(r1);
787 }
788
789 int32_t r0 = r - r1 * 2 * gamma2;
790
791 // reduce r0 mod q
792 r0 -= is_negative_mask((DilithiumConstants::Q - 1) / 2 - r0).if_set_return(DilithiumConstants::Q);
793
794 return {r1, r0};
795}
796
797/**
798 * This is templated on all possible values of gamma2 to allow for compile-time
799 * optimization given the statically known value of gamma2.
800 */
801template <DilithiumConstants::DilithiumGamma2 gamma2>
802std::pair<DilithiumPolyVec, DilithiumPolyVec> decompose_all_coefficients(const DilithiumPolyVec& vec) {
803 auto result = std::make_pair(DilithiumPolyVec(vec.size()), DilithiumPolyVec(vec.size()));
804
805 for(size_t i = 0; i < vec.size(); ++i) {
806 for(size_t j = 0; j < vec[i].size(); ++j) {
807 std::tie(result.first[i][j], result.second[i][j]) = decompose<gamma2>(vec[i][j]);
808 }
809 }
810
811 return result;
812}
813
814} // namespace
815
816/**
817 * NIST FIPS 204, Algorithm 36 (Decompose) on a polynomial vector
818 *
819 * Algorithms 37 (HighBits) and 38 (LowBits) are not implemented explicitly,
820 * simply use the first (HighBits) and second (LowBits) element of the result.
821 */
822std::pair<DilithiumPolyVec, DilithiumPolyVec> decompose(const DilithiumPolyVec& vec, const DilithiumConstants& mode) {
824 switch(mode.gamma2()) {
825 case Gamma2::Qminus1DividedBy32:
826 return decompose_all_coefficients<Gamma2::Qminus1DividedBy32>(vec);
827 break;
828 case Gamma2::Qminus1DividedBy88:
829 return decompose_all_coefficients<Gamma2::Qminus1DividedBy88>(vec);
830 break;
831 }
832
834}
835
836/**
837 * NIST FIPS 204, Algorithm 39 (MakeHint)
838 *
839 * MakeHint is specified per value in FIPS 204. This implements the algorithm
840 * for the entire polynomial vector. The specified algorithm is equivalent to
841 * the inner lambda.
842 *
843 * TODO: This is taken from the reference implementation. We should implement it
844 * as specified in the spec, and see if that has any performance impact.
845 */
847 BOTAN_DEBUG_ASSERT(z.size() == r.size());
848
849 auto make_hint = [gamma2 = uint32_t(mode.gamma2()),
850 q_gamma2 = static_cast<uint32_t>(DilithiumConstants::Q) - uint32_t(mode.gamma2())](
851 int32_t c0, int32_t c1) -> CT::Choice {
852 BOTAN_DEBUG_ASSERT(c0 >= 0);
853 BOTAN_DEBUG_ASSERT(c1 >= 0);
854
855 const uint32_t pc0 = static_cast<uint32_t>(c0);
856 const uint32_t pc1 = static_cast<uint32_t>(c1);
857
858 return (CT::Mask<uint32_t>::is_gt(pc0, gamma2) & CT::Mask<uint32_t>::is_lte(pc0, q_gamma2) &
860 .as_choice();
861 };
862
863 DilithiumPolyVec hint(r.size());
864
865 for(size_t i = 0; i < r.size(); ++i) {
866 for(size_t j = 0; j < r[i].size(); ++j) {
867 hint[i][j] = static_cast<int>(make_hint(z[i][j], r[i][j]).as_bool());
868 }
869 }
870
872
873 return hint;
874}
875
876namespace {
877
878/**
879 * This is templated on all possible values of gamma2 to allow for compile-time
880 * optimization given the statically known value of gamma2.
881 */
882template <DilithiumConstants::DilithiumGamma2 gamma2>
883void use_hint_on_coefficients(const DilithiumPolyVec& hints, DilithiumPolyVec& vec) {
884 constexpr auto m = (DilithiumConstants::Q - 1) / (2 * gamma2);
885
886 auto modulo_m = [](int32_t r1) -> int32_t {
887 BOTAN_DEBUG_ASSERT(r1 >= -static_cast<decltype(r1)>(m) && r1 <= static_cast<decltype(r1)>(m));
888 return (r1 + m) % m;
889 };
890
891 auto use_hint = [&modulo_m](bool hint, int32_t r) -> int32_t {
892 auto [r1, r0] = decompose<gamma2>(r);
893
894 if(!hint) {
895 return r1;
896 }
897
898 if(r0 > 0) {
899 return modulo_m(r1 + 1);
900 } else {
901 return modulo_m(r1 - 1);
902 }
903 };
904
905 for(size_t i = 0; i < vec.size(); ++i) {
906 for(size_t j = 0; j < vec[i].size(); ++j) {
907 vec[i][j] = use_hint(hints[i][j], vec[i][j]);
908 }
909 }
910}
911
912} // namespace
913
914/**
915 * NIST FIPS 204, Algorithm 40 (UseHint)
916 *
917 * UseHint is specified per value in FIPS 204. This implements the algorithm
918 * for the entire polynomial vector. The specified algorithm is equivalent to
919 * the inner lambdas of 'use_hint_with_coefficients'.
920 */
921void use_hint(DilithiumPolyVec& vec, const DilithiumPolyVec& hints, const DilithiumConstants& mode) {
922 BOTAN_DEBUG_ASSERT(hints.size() == vec.size());
925
927 switch(mode.gamma2()) {
928 case Gamma2::Qminus1DividedBy32:
929 use_hint_on_coefficients<Gamma2::Qminus1DividedBy32>(hints, vec);
930 break;
931 case Gamma2::Qminus1DividedBy88:
932 use_hint_on_coefficients<Gamma2::Qminus1DividedBy88>(hints, vec);
933 break;
934 }
935
937}
938
939bool infinity_norm_within_bound(const DilithiumPolyVec& vec, size_t bound) {
940 BOTAN_DEBUG_ASSERT(bound <= (DilithiumConstants::Q - 1) / 8);
941
942 // It is ok to leak which coefficient violates the bound as the probability
943 // for each coefficient is independent of secret data but we must not leak
944 // the sign of the centralized representative.
945 for(const auto& p : vec) {
946 for(auto c : p) {
947 const auto abs_c = c - is_negative_mask(c).if_set_return(2 * c);
948 if(CT::driveby_unpoison(abs_c >= bound)) {
949 return false;
950 }
951 }
952 }
953
954 return true;
955}
956
957} // namespace Botan::Dilithium_Algos
#define BOTAN_ASSERT_NOMSG(expr)
Definition assert.h:75
#define BOTAN_DEBUG_ASSERT(expr)
Definition assert.h:129
#define BOTAN_ASSERT_NONNULL(ptr)
Definition assert.h:114
#define BOTAN_ASSERT_UNREACHABLE()
Definition assert.h:163
size_t remaining() const
auto copy(const size_t count)
std::span< const uint8_t > take(const size_t count)
Helper class to ease in-place marshalling of concatenated fixed-length values.
constexpr void append(std::span< const uint8_t > buffer)
constexpr size_t remaining_capacity() const
constexpr std::span< uint8_t > next(size_t bytes)
constexpr bool full() const
ThisPolynomialVector clone() const
Definition pqcrystals.h:418
constexpr bool ct_validate_value_range(T min, T max) const noexcept
Definition pqcrystals.h:439
ThisPolynomialVector & conditional_add_q()
Definition pqcrystals.h:470
constexpr size_t hamming_weight() const noexcept
Definition pqcrystals.h:294
constexpr size_t size() const
Definition pqcrystals.h:276
constexpr bool ct_validate_value_range(T min, T max) const noexcept
Definition pqcrystals.h:289
static constexpr Mask< T > is_lte(T x, T y)
Definition ct_utils.h:463
static constexpr Mask< T > expand_top_bit(T v)
Definition ct_utils.h:415
static constexpr Mask< T > is_equal(T x, T y)
Definition ct_utils.h:442
static constexpr Mask< T > is_gt(T x, T y)
Definition ct_utils.h:458
static constexpr Mask< T > is_zero(T x)
Definition ct_utils.h:437
size_t public_key_bytes() const
byte length of the encoded public key
size_t commitment_hash_full_bytes() const
length of the entire commitment hash 'c~' in bytes (differs between R3 and ML-DSA)
static constexpr T Q
modulus
size_t signature_bytes() const
byte length of the encoded signature
DilithiumGamma1 gamma1() const
coefficient range of the randomly sampled mask 'y'
DilithiumEta eta() const
coefficient range of the private key's polynomial vectors 's1' and 's2'
uint8_t l() const
dimensions of the expanded matrix A
Dilithium_Symmetric_Primitives_Base & symmetric_primitives() const
static constexpr T D
number of dropped bits from t (see FIPS 204 Section 5)
DilithiumTau tau() const
hamming weight of the polynomial 'c' sampled from the commitment's hash
static constexpr size_t SEED_SIGNING_KEY_BYTES
int32_t T
base data type for most calculations
size_t serialized_commitment_bytes() const
byte length of the packed commitment polynomial vector 'w1'
DilithiumOmega omega() const
maximal hamming weight of the hint polynomial vector 'h'
uint8_t k() const
dimensions of the expanded matrix A
DilithiumGamma2 gamma2() const
low-order rounding range for decomposing the commitment from polynomial vector 'w'
static constexpr size_t SEED_RHO_BYTES
DilithiumHashedPublicKey H(StrongSpan< const DilithiumSerializedPublicKey > pk) const
decltype(auto) size() const noexcept(noexcept(this->m_span.size()))
constexpr void unpack(Polynomial< PolyTrait, D > &p, ByteSourceT &byte_source, UnmapFnT unmap)
constexpr void pack(const Polynomial< PolyTrait, D > &p, BufferStuffer &stuffer, MapFnT map)
decltype(auto) driveby_unpoison(T &&v)
Definition ct_utils.h:243
constexpr auto scoped_poison(const Ts &... xs)
Definition ct_utils.h:222
constexpr void unpoison(const T *p, size_t n)
Definition ct_utils.h:67
constexpr void poison(const T *p, size_t n)
Definition ct_utils.h:56
DilithiumSerializedPublicKey encode_public_key(StrongSpan< const DilithiumSeedRho > rho, const DilithiumPolyVec &t1, const DilithiumConstants &mode)
DilithiumInternalKeypair expand_keypair(DilithiumSeedRandomness xi, DilithiumConstants mode)
DilithiumSerializedSignature encode_signature(StrongSpan< const DilithiumCommitmentHash > c, const DilithiumPolyVec &response, const DilithiumPolyVec &hint, const DilithiumConstants &mode)
DilithiumSerializedCommitment encode_commitment(const DilithiumPolyVec &w1, const DilithiumConstants &mode)
std::pair< DilithiumPolyVec, DilithiumPolyVec > power2round(const DilithiumPolyVec &vec)
bool infinity_norm_within_bound(const DilithiumPolyVec &vec, size_t bound)
std::pair< DilithiumPolyVec, DilithiumPolyVec > decompose(const DilithiumPolyVec &vec, const DilithiumConstants &mode)
DilithiumPolyVec expand_mask(StrongSpan< const DilithiumSeedRhoPrime > rhoprime, uint16_t nonce, const DilithiumConstants &mode)
DilithiumPolyMatNTT expand_A(StrongSpan< const DilithiumSeedRho > rho, const DilithiumConstants &mode)
void use_hint(DilithiumPolyVec &vec, const DilithiumPolyVec &hints, const DilithiumConstants &mode)
std::pair< DilithiumPolyVec, DilithiumPolyVec > expand_s(StrongSpan< const DilithiumSeedRhoPrime > rhoprime, const DilithiumConstants &mode)
std::pair< DilithiumSeedRho, DilithiumPolyVec > decode_public_key(StrongSpan< const DilithiumSerializedPublicKey > pk, const DilithiumConstants &mode)
std::optional< std::tuple< DilithiumCommitmentHash, DilithiumPolyVec, DilithiumPolyVec > > decode_signature(StrongSpan< const DilithiumSerializedSignature > sig, const DilithiumConstants &mode)
DilithiumPolyVec make_hint(const DilithiumPolyVec &z, const DilithiumPolyVec &r, const DilithiumConstants &mode)
DilithiumPoly sample_in_ball(StrongSpan< const DilithiumCommitmentHash > seed, const DilithiumConstants &mode)
BOTAN_FORCE_INLINE constexpr bool is_power_of_2(T arg)
Definition bit_ops.h:62
Strong< std::vector< uint8_t >, struct DilithiumSerializedSignature_ > DilithiumSerializedSignature
Serialized signature data.
Strong< secure_vector< uint8_t >, struct DilithiumSeedK_ > DilithiumSigningSeedK
Private seed K used during signing.
Strong< secure_vector< uint8_t >, struct DilithiumSeedRandomness_ > DilithiumSeedRandomness
Principal seed used to generate Dilithium key pairs.
detail::Bounded_XOF< XOF &, bound > Bounded_XOF
Botan::CRYSTALS::PolynomialVector< DilithiumPolyTraits, Botan::CRYSTALS::Domain::Normal > DilithiumPolyVec
Strong< std::vector< uint8_t >, struct DilithiumPublicSeed_ > DilithiumSeedRho
Public seed to sample the polynomial matrix A from.
Strong< secure_vector< uint8_t >, struct DilithiumSerializedPrivateKey_ > DilithiumSerializedPrivateKey
Serialized private key data.
constexpr uint32_t make_uint32(uint8_t i0, uint8_t i1, uint8_t i2, uint8_t i3)
Definition loadstor.h:104
Botan::CRYSTALS::Polynomial< DilithiumPolyTraits, Botan::CRYSTALS::Domain::NTT > DilithiumPolyNTT
Strong< std::vector< uint8_t >, struct DilithiumCommitmentHash_ > DilithiumCommitmentHash
Hash of the message representative and the signer's commitment.
BOTAN_FORCE_INLINE constexpr T rho(T x)
Definition rotate.h:53
Strong< std::vector< uint8_t >, struct DilithiumSerializedPublicKey_ > DilithiumSerializedPublicKey
Serialized public key data (result of pkEncode(pk)).
constexpr auto load_le(ParamTs &&... params)
Definition loadstor.h:495
Botan::CRYSTALS::Polynomial< DilithiumPolyTraits, Botan::CRYSTALS::Domain::Normal > DilithiumPoly
Strong< std::vector< uint8_t >, struct DilithiumSerializedCommitment_ > DilithiumSerializedCommitment
Serialized representation of a commitment w1.
Botan::CRYSTALS::PolynomialMatrix< DilithiumPolyTraits > DilithiumPolyMatNTT
constexpr auto bitlen(size_t x)
std::pair< std::shared_ptr< Dilithium_PublicKeyInternal >, std::shared_ptr< Dilithium_PrivateKeyInternal > > DilithiumInternalKeypair
Internal representation of a Dilithium key pair.
Strong< std::vector< uint8_t >, struct DilithiumHashedPublicKey_ > DilithiumHashedPublicKey