Botan 3.10.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/ct_utils.h>
22#include <botan/internal/dilithium_keys.h>
23#include <botan/internal/dilithium_symmetric_primitives.h>
24#include <botan/internal/loadstor.h>
25#include <botan/internal/pqcrystals_encoding.h>
26#include <botan/internal/pqcrystals_helpers.h>
27#include <botan/internal/stl_util.h>
28
29#include <utility>
30
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 const auto remaining = bit_positions.take(bit_positions.remaining());
290 if(!std::all_of(remaining.begin(), remaining.end(), [](auto b) { return b == 0; })) {
291 return std::nullopt;
292 }
293
295 return hint;
296}
297
298/**
299 * NIST FIPS 204, Algorithm 6, lines 5-7 (ML-DSA.KeyGen_internal)
300 *
301 * We have to expose this independently to derive the public key from the
302 * private key when loading a key pair from a serialized private key. This
303 * is needed because of Botan's design decision to let the private key
304 * class inherit from the public key class.
305 *
306 * TODO(Botan4): This should be refactored after PrivateKey does not inherit
307 * from PublicKey anymore.
308 */
309std::pair<DilithiumPolyVec, DilithiumPolyVec> compute_t1_and_t0(const DilithiumPolyMatNTT& A,
310 const DilithiumPolyVec& s1,
311 const DilithiumPolyVec& s2) {
312 auto t_hat = A * ntt(s1.clone());
313 t_hat.reduce();
314 auto t = inverse_ntt(std::move(t_hat));
315 t += s2;
317
319}
320
321} // namespace
322
323/**
324 * NIST FIPS 204, Algorithm 22 (pkEncode)
325 */
327 const DilithiumPolyVec& t1,
328 const DilithiumConstants& mode) {
330 BufferStuffer stuffer(pk);
331
332 stuffer.append(rho);
333 for(const auto& p : t1) {
334 poly_pack_t1(p, stuffer);
335 }
336
337 BOTAN_ASSERT_NOMSG(stuffer.full());
338 return pk;
339}
340
341/**
342 * NIST FIPS 204, Algorithm 23 (pkDecode)
343 */
344std::pair<DilithiumSeedRho, DilithiumPolyVec> decode_public_key(StrongSpan<const DilithiumSerializedPublicKey> pk,
345 const DilithiumConstants& mode) {
346 if(pk.size() != mode.public_key_bytes()) {
347 throw Decoding_Error("Dilithium: Invalid public key length");
348 }
349
350 BufferSlicer slicer(pk);
352
353 DilithiumPolyVec t1(mode.k());
354 for(auto& p : t1) {
355 poly_unpack_t1(p, slicer);
356 }
357 BOTAN_ASSERT_NOMSG(slicer.empty());
358
359 return {std::move(rho), std::move(t1)};
360}
361
362#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
363
364/**
365 * NIST FIPS 204, Algorithm 24 (skEncode)
366 */
367DilithiumSerializedPrivateKey encode_keypair(const DilithiumInternalKeypair& keypair) {
368 const auto& [pk, sk] = keypair;
371 const auto& mode = sk->mode();
372 auto scope = CT::scoped_poison(*sk);
373
374 DilithiumSerializedPrivateKey serialization(mode.private_key_bytes());
375 BufferStuffer stuffer(serialization);
376
377 stuffer.append(pk->rho());
378 stuffer.append(sk->signing_seed());
379 stuffer.append(pk->tr());
380
381 for(const auto& p : sk->s1()) {
382 poly_pack_eta(p, stuffer, mode);
383 }
384
385 for(const auto& p : sk->s2()) {
386 poly_pack_eta(p, stuffer, mode);
387 }
388
389 for(const auto& p : sk->t0()) {
390 poly_pack_t0(p, stuffer);
391 }
392
393 BOTAN_ASSERT_NOMSG(stuffer.full());
394 CT::unpoison(serialization);
395
396 return serialization;
397}
398
399/**
400 * NIST FIPS 204, Algorithm 25 (skDecode)
401 *
402 * Because Botan's Private_Key class inherits from Public_Key, we have to
403 * derive the public key from the private key here.
404 *
405 * TODO(Botan4): This should be refactored after PrivateKey does not inherit
406 * from PublicKey anymore.
407 */
408DilithiumInternalKeypair decode_keypair(StrongSpan<const DilithiumSerializedPrivateKey> sk, DilithiumConstants mode) {
409 auto scope = CT::scoped_poison(sk);
410
411 BOTAN_ASSERT_NOMSG(sk.size() == mode.private_key_bytes());
412
413 BufferSlicer slicer(sk);
414
417 auto tr = slicer.copy<DilithiumHashedPublicKey>(mode.public_key_hash_bytes());
418
419 DilithiumPolyVec s1(mode.l());
420 for(auto& p : s1) {
421 poly_unpack_eta(p, slicer, mode, true /* check decoded value range */);
422 }
423
424 DilithiumPolyVec s2(mode.k());
425 for(auto& p : s2) {
426 poly_unpack_eta(p, slicer, mode, true /* check decoded value range */);
427 }
428
429 DilithiumPolyVec t0(mode.k());
430 for(auto& p : t0) {
431 poly_unpack_t0(p, slicer);
432 }
433
434 BOTAN_ASSERT_NOMSG(slicer.empty());
435
436 // Currently, Botan's Private_Key class inherits from Public_Key, forcing us
437 // to derive the public key from the private key here.
438 // TODO(Botan4): Reconsider once PrivateKey/PublicKey issue is tackled.
439
440 CT::unpoison(rho); // rho is public (used in rejection sampling of matrix A)
441
442 const auto A = expand_A(rho, mode);
443 auto [t1, _] = compute_t1_and_t0(A, s1, s2);
444
445 CT::unpoison(t1); // part of the public key
446
448 std::make_shared<Dilithium_PublicKeyInternal>(mode, std::move(rho), std::move(t1)),
449 std::make_shared<Dilithium_PrivateKeyInternal>(std::move(mode),
450 std::nullopt, // decoding cannot recover the private seed
451 std::move(K),
452 std::move(s1),
453 std::move(s2),
454 std::move(t0)),
455 };
456
457 CT::unpoison(tr); // hash of the public key
458
459 if(keypair.first->tr() != tr) {
460 throw Decoding_Error("Calculated dilithium public key hash does not match the one stored in the private key");
461 }
462
463 CT::unpoison(*keypair.second);
464
465 return keypair;
466}
467
468#endif
469
470/**
471 * NIST FIPS 204, Algorithm 26 (sigEncode)
472 */
474 const DilithiumPolyVec& response,
475 const DilithiumPolyVec& hint,
476 const DilithiumConstants& mode) {
478 BufferStuffer stuffer(sig);
479
480 stuffer.append(c);
481 for(const auto& p : response) {
482 poly_pack_gamma1(p, stuffer, mode);
483 }
484 hint_pack(hint, stuffer, mode);
485
486 return sig;
487}
488
489/**
490 * NIST FIPS 204, Algorithm 27 (sigDecode)
491 */
492std::optional<std::tuple<DilithiumCommitmentHash, DilithiumPolyVec, DilithiumPolyVec>> decode_signature(
494 BufferSlicer slicer(sig);
495 BOTAN_ASSERT_NOMSG(slicer.remaining() == mode.signature_bytes());
496
497 auto commitment_hash = slicer.copy<DilithiumCommitmentHash>(mode.commitment_hash_full_bytes());
498
499 DilithiumPolyVec response(mode.l());
500 for(auto& p : response) {
501 poly_unpack_gamma1(p, slicer, mode);
502 }
503 BOTAN_ASSERT_NOMSG(slicer.remaining() == size_t(mode.omega()) + mode.k());
504
505 auto hint = hint_unpack(slicer, mode);
506 BOTAN_ASSERT_NOMSG(slicer.empty());
507 if(!hint.has_value()) {
508 return std::nullopt;
509 }
510
511 return std::make_tuple(std::move(commitment_hash), std::move(response), std::move(hint.value()));
512}
513
514/**
515 * NIST FIPS 204, Algorithm 28 (w1Encode)
516 */
519 BufferStuffer stuffer(commitment);
520
521 for(const auto& p : w1) {
522 poly_pack_w1(p, stuffer, mode);
523 }
524
525 return commitment;
526}
527
528/**
529 * NIST FIPS 204, Algorithm 29 (SampleInBall)
530 */
532 // This generator resembles the while loop in the spec.
533 auto& xof = mode.symmetric_primitives().H(seed);
535
537 uint64_t signs = load_le(bounded_xof.next<8>());
538 for(size_t i = c.size() - mode.tau(); i < c.size(); ++i) {
539 const auto j = bounded_xof.next_byte([i](uint8_t byte) { return byte <= i; });
540 c[i] = c[j];
541 c[j] = 1 - 2 * (signs & 1);
542 signs >>= 1;
543 }
544
547
548 return c;
549}
550
551namespace {
552
553/**
554 * NIST FIPS 204, Algorithm 30 (RejNTTPoly)
555 */
556void sample_ntt_uniform(StrongSpan<const DilithiumSeedRho> rho,
558 uint16_t nonce,
559 const DilithiumConstants& mode) {
560 /**
561 * A generator that returns the next coefficient sampled from the XOF,
562 * according to: NIST FIPS 204, Algorithm 14 (CoeffFromThreeBytes).
563 */
564 auto& xof = mode.symmetric_primitives().H(rho, nonce);
566
567 for(auto& coeff : p) {
568 coeff =
569 bounded_xof.next<3>([](const auto bytes) { return make_uint32(0, bytes[2], bytes[1], bytes[0]) & 0x7FFFFF; },
570 [](const uint32_t z) { return z < DilithiumConstants::Q; });
571 }
572
573 BOTAN_DEBUG_ASSERT(p.ct_validate_value_range(0, DilithiumConstants::Q - 1));
574}
575
576/**
577 * NIST FIPS 204, Algorithm 15 (CoeffFromHalfByte)
578 *
579 * Magic numbers for (b mod 5) are taken from the reference implementation.
580 */
581template <DilithiumConstants::DilithiumEta eta>
582std::optional<int32_t> coeff_from_halfbyte(uint8_t b) {
583 BOTAN_DEBUG_ASSERT(b < 16);
584
585 if constexpr(eta == DilithiumConstants::DilithiumEta::_2) {
586 if(CT::driveby_unpoison(b < 15)) {
587 b = b - (205 * b >> 10) * 5; // b = b mod 5
588 return 2 - b;
589 }
590 } else if constexpr(eta == DilithiumConstants::DilithiumEta::_4) {
591 if(CT::driveby_unpoison(b < 9)) {
592 return 4 - b;
593 }
594 }
595
596 return std::nullopt;
597}
598
599template <DilithiumConstants::DilithiumEta eta>
600void sample_uniform_eta(DilithiumPoly& p, Botan::XOF& xof) {
601 // A generator that returns the next coefficient sampled from the XOF. As the
602 // sampling uses half-bytes, this keeps track of the additionally sampled
603 // coefficient as needed.
604 auto next_coeff = [bounded_xof = Bounded_XOF<DilithiumConstants::SAMPLE_POLY_FROM_XOF_BOUND>(xof),
605 stashed_coeff = std::optional<int32_t>{}]() mutable -> int32_t {
606 if(auto stashed = std::exchange(stashed_coeff, std::nullopt)) {
607 return *stashed;
608 }
609
610 BOTAN_DEBUG_ASSERT(!stashed_coeff.has_value());
611 while(true) {
612 const auto b = bounded_xof.next_byte();
613 const auto z0 = coeff_from_halfbyte<eta>(b & 0x0F);
614 const auto z1 = coeff_from_halfbyte<eta>(b >> 4);
615
616 if(z0.has_value()) {
617 stashed_coeff = z1; // keep candidate z1 for the next invocation
618 return *z0;
619 } else if(z1.has_value()) {
620 // z0 was invalid, z1 is valid, nothing to stash
621 return *z1;
622 }
623 }
624 };
625
626 for(auto& coeff : p) {
627 coeff = next_coeff();
628 }
629}
630
631/**
632 * NIST FIPS 204, Algorithm 31 (RejBoundedPoly)
633 */
634void sample_uniform_eta(StrongSpan<const DilithiumSeedRhoPrime> rhoprime,
635 DilithiumPoly& p,
636 uint16_t nonce,
637 const DilithiumConstants& mode) {
639
640 auto& xof = mode.symmetric_primitives().H(rhoprime, nonce);
641 switch(mode.eta()) {
642 case Eta::_2:
643 sample_uniform_eta<Eta::_2>(p, xof);
644 break;
645 case Eta::_4:
646 sample_uniform_eta<Eta::_4>(p, xof);
647 break;
648 }
649
650 // Rejection sampling is done. Secret polynomial can be repoisoned.
651 CT::poison(p);
652
653 BOTAN_DEBUG_ASSERT(p.ct_validate_value_range(-static_cast<int32_t>(mode.eta()), mode.eta()));
654}
655
656} // namespace
657
658/**
659 * NIST FIPS 204, Algorithm 6 (ML-DSA.KeyGen_internal)
660 *
661 * Lines 5-7 are extracted into a separate function, see above. The key
662 * encoding is deferred until the user explicitly invokes the encoding.
663 */
665 const auto& sympriv = mode.symmetric_primitives();
666 CT::poison(xi);
667
668 auto [rho, rhoprime, K] = sympriv.H(xi);
669 CT::unpoison(rho); // rho is public (seed for the public matrix A)
670
671 const auto A = Dilithium_Algos::expand_A(rho, mode);
672 auto [s1, s2] = Dilithium_Algos::expand_s(rhoprime, mode);
673 auto [t1, t0] = Dilithium_Algos::compute_t1_and_t0(A, s1, s2);
674
675 CT::unpoison(t1); // part of the public key
676
678 std::make_shared<Dilithium_PublicKeyInternal>(mode, std::move(rho), std::move(t1)),
679 std::make_shared<Dilithium_PrivateKeyInternal>(
680 std::move(mode), std::move(xi), std::move(K), std::move(s1), std::move(s2), std::move(t0)),
681 };
682
683 CT::unpoison(*keypair.second);
684
685 return keypair;
686}
687
688/**
689 * NIST FIPS 204, Algorithm 32 (ExpandA)
690 *
691 * Note that the actual concatenation of rho, s and r is done downstream
692 * in the sampling function.
693 */
695 DilithiumPolyMatNTT A(mode.k(), mode.l());
696 for(uint8_t r = 0; r < mode.k(); ++r) {
697 for(uint8_t s = 0; s < mode.l(); ++s) {
698 sample_ntt_uniform(rho, A[r][s], load_le(std::array{s, r}), mode);
699 }
700 }
701 return A;
702}
703
704/**
705 * NIST FIPS 204, Algorithm 33 (ExpandS)
706 */
707std::pair<DilithiumPolyVec, DilithiumPolyVec> expand_s(StrongSpan<const DilithiumSeedRhoPrime> rhoprime,
708 const DilithiumConstants& mode) {
709 auto result = std::make_pair(DilithiumPolyVec(mode.l()), DilithiumPolyVec(mode.k()));
710 auto& [s1, s2] = result;
711
712 uint16_t nonce = 0;
713 for(auto& p : s1) {
714 sample_uniform_eta(rhoprime, p, nonce++, mode);
715 }
716
717 for(auto& p : s2) {
718 sample_uniform_eta(rhoprime, p, nonce++, mode);
719 }
720
721 return result;
722}
723
724/**
725 * NIST FIPS 204, Algorithm 34 (ExpandMask)
726 */
728 uint16_t nonce,
729 const DilithiumConstants& mode) {
730 DilithiumPolyVec s(mode.l());
731 for(auto& p : s) {
732 auto& xof = mode.symmetric_primitives().H(rhoprime, nonce++);
733 poly_unpack_gamma1(p, xof, mode);
734 }
735 return s;
736}
737
738/**
739 * NIST FIPS 204, Algorithm 35 (Power2Round)
740 *
741 * In contrast to the spec, this function takes a polynomial vector and
742 * performs the power2round operation on each coefficient in the vector.
743 * The actual Algorithm 35 as specified is actually just the inner lambda.
744 */
745std::pair<DilithiumPolyVec, DilithiumPolyVec> power2round(const DilithiumPolyVec& vec) {
746 // This procedure is taken verbatim from Dilithium's reference implementation.
747 auto power2round = [d = DilithiumConstants::D](int32_t r) -> std::pair<int32_t, int32_t> {
748 const int32_t r1 = (r + (1 << (d - 1)) - 1) >> d;
749 const int32_t r0 = r - (r1 << d);
750 return {r1, r0};
751 };
752
753 auto result = std::make_pair(DilithiumPolyVec(vec.size()), DilithiumPolyVec(vec.size()));
754
755 for(size_t i = 0; i < vec.size(); ++i) {
756 for(size_t j = 0; j < vec[i].size(); ++j) {
757 std::tie(result.first[i][j], result.second[i][j]) = power2round(vec[i][j]);
758 }
759 }
760
761 return result;
762}
763
764namespace {
765
766/**
767 * NIST FIPS 204, Algorithm 36 (Decompose)
768 *
769 * The implementation is adapted from the verbatim reference implementation using
770 * the magic numbers that depend on the values of Q and gamma2 and ensure that
771 * this operation is done in constant-time.
772 */
773template <DilithiumConstants::DilithiumGamma2 gamma2>
774std::pair<int32_t, int32_t> decompose(int32_t r) {
775 int32_t r1 = (r + 127) >> 7;
776
778 r1 = (r1 * 1025 + (1 << 21)) >> 22;
779 r1 &= 15;
780 } else if constexpr(gamma2 == DilithiumConstants::DilithiumGamma2::Qminus1DividedBy88) {
781 r1 = (r1 * 11275 + (1 << 23)) >> 24;
782 r1 = is_negative_mask(43 - r1).if_not_set_return(r1);
783 }
784
785 int32_t r0 = r - r1 * 2 * gamma2;
786
787 // reduce r0 mod q
788 r0 -= is_negative_mask((DilithiumConstants::Q - 1) / 2 - r0).if_set_return(DilithiumConstants::Q);
789
790 return {r1, r0};
791}
792
793/**
794 * This is templated on all possible values of gamma2 to allow for compile-time
795 * optimization given the statically known value of gamma2.
796 */
797template <DilithiumConstants::DilithiumGamma2 gamma2>
798std::pair<DilithiumPolyVec, DilithiumPolyVec> decompose_all_coefficients(const DilithiumPolyVec& vec) {
799 auto result = std::make_pair(DilithiumPolyVec(vec.size()), DilithiumPolyVec(vec.size()));
800
801 for(size_t i = 0; i < vec.size(); ++i) {
802 for(size_t j = 0; j < vec[i].size(); ++j) {
803 std::tie(result.first[i][j], result.second[i][j]) = decompose<gamma2>(vec[i][j]);
804 }
805 }
806
807 return result;
808}
809
810} // namespace
811
812/**
813 * NIST FIPS 204, Algorithm 36 (Decompose) on a polynomial vector
814 *
815 * Algorithms 37 (HighBits) and 38 (LowBits) are not implemented explicitly,
816 * simply use the first (HighBits) and second (LowBits) element of the result.
817 */
818std::pair<DilithiumPolyVec, DilithiumPolyVec> decompose(const DilithiumPolyVec& vec, const DilithiumConstants& mode) {
820 switch(mode.gamma2()) {
821 case Gamma2::Qminus1DividedBy32:
822 return decompose_all_coefficients<Gamma2::Qminus1DividedBy32>(vec);
823 break;
824 case Gamma2::Qminus1DividedBy88:
825 return decompose_all_coefficients<Gamma2::Qminus1DividedBy88>(vec);
826 break;
827 }
828
830}
831
832/**
833 * NIST FIPS 204, Algorithm 39 (MakeHint)
834 *
835 * MakeHint is specified per value in FIPS 204. This implements the algorithm
836 * for the entire polynomial vector. The specified algorithm is equivalent to
837 * the inner lambda.
838 *
839 * TODO: This is taken from the reference implementation. We should implement it
840 * as specified in the spec, and see if that has any performance impact.
841 */
843 BOTAN_DEBUG_ASSERT(z.size() == r.size());
844
845 auto make_hint = [gamma2 = uint32_t(mode.gamma2()),
846 q_gamma2 = static_cast<uint32_t>(DilithiumConstants::Q) - uint32_t(mode.gamma2())](
847 int32_t c0, int32_t c1) -> CT::Choice {
848 BOTAN_DEBUG_ASSERT(c0 >= 0);
849 BOTAN_DEBUG_ASSERT(c1 >= 0);
850
851 const uint32_t pc0 = static_cast<uint32_t>(c0);
852 const uint32_t pc1 = static_cast<uint32_t>(c1);
853
854 return (CT::Mask<uint32_t>::is_gt(pc0, gamma2) & CT::Mask<uint32_t>::is_lte(pc0, q_gamma2) &
856 .as_choice();
857 };
858
859 DilithiumPolyVec hint(r.size());
860
861 for(size_t i = 0; i < r.size(); ++i) {
862 for(size_t j = 0; j < r[i].size(); ++j) {
863 hint[i][j] = static_cast<int>(make_hint(z[i][j], r[i][j]).as_bool());
864 }
865 }
866
868
869 return hint;
870}
871
872namespace {
873
874/**
875 * This is templated on all possible values of gamma2 to allow for compile-time
876 * optimization given the statically known value of gamma2.
877 */
878template <DilithiumConstants::DilithiumGamma2 gamma2>
879void use_hint_on_coefficients(const DilithiumPolyVec& hints, DilithiumPolyVec& vec) {
880 constexpr auto m = (DilithiumConstants::Q - 1) / (2 * gamma2);
881
882 auto modulo_m = [](int32_t r1) -> int32_t {
883 BOTAN_DEBUG_ASSERT(r1 >= -static_cast<decltype(r1)>(m) && r1 <= static_cast<decltype(r1)>(m));
884 return (r1 + m) % m;
885 };
886
887 auto use_hint = [&modulo_m](bool hint, int32_t r) -> int32_t {
888 auto [r1, r0] = decompose<gamma2>(r);
889
890 if(!hint) {
891 return r1;
892 }
893
894 if(r0 > 0) {
895 return modulo_m(r1 + 1);
896 } else {
897 return modulo_m(r1 - 1);
898 }
899 };
900
901 for(size_t i = 0; i < vec.size(); ++i) {
902 for(size_t j = 0; j < vec[i].size(); ++j) {
903 vec[i][j] = use_hint(hints[i][j], vec[i][j]);
904 }
905 }
906}
907
908} // namespace
909
910/**
911 * NIST FIPS 204, Algorithm 40 (UseHint)
912 *
913 * UseHint is specified per value in FIPS 204. This implements the algorithm
914 * for the entire polynomial vector. The specified algorithm is equivalent to
915 * the inner lambdas of 'use_hint_with_coefficients'.
916 */
917void use_hint(DilithiumPolyVec& vec, const DilithiumPolyVec& hints, const DilithiumConstants& mode) {
918 BOTAN_DEBUG_ASSERT(hints.size() == vec.size());
921
923 switch(mode.gamma2()) {
924 case Gamma2::Qminus1DividedBy32:
925 use_hint_on_coefficients<Gamma2::Qminus1DividedBy32>(hints, vec);
926 break;
927 case Gamma2::Qminus1DividedBy88:
928 use_hint_on_coefficients<Gamma2::Qminus1DividedBy88>(hints, vec);
929 break;
930 }
931
933}
934
935bool infinity_norm_within_bound(const DilithiumPolyVec& vec, size_t bound) {
936 BOTAN_DEBUG_ASSERT(bound <= (DilithiumConstants::Q - 1) / 8);
937
938 // It is ok to leak which coefficient violates the bound as the probability
939 // for each coefficient is independent of secret data but we must not leak
940 // the sign of the centralized representative.
941 for(const auto& p : vec) {
942 for(auto c : p) {
943 const auto abs_c = c - is_negative_mask(c).if_set_return(2 * c);
944 if(CT::driveby_unpoison(abs_c >= bound)) {
945 return false;
946 }
947 }
948 }
949
950 return true;
951}
952
953} // 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
Definition stl_util.h:118
uint8_t take_byte()
Definition stl_util.h:109
auto copy(const size_t count)
Definition stl_util.h:80
bool empty() const
Definition stl_util.h:120
std::span< const uint8_t > take(const size_t count)
Definition stl_util.h:89
Helper class to ease in-place marshalling of concatenated fixed-length values.
Definition stl_util.h:133
constexpr void append(std::span< const uint8_t > buffer)
Definition stl_util.h:168
constexpr size_t remaining_capacity() const
Definition stl_util.h:180
constexpr std::span< uint8_t > next(size_t bytes)
Definition stl_util.h:141
constexpr bool full() const
Definition stl_util.h:178
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:491
static constexpr Mask< T > expand_top_bit(T v)
Definition ct_utils.h:443
static constexpr Mask< T > is_equal(T x, T y)
Definition ct_utils.h:470
static constexpr Mask< T > is_gt(T x, T y)
Definition ct_utils.h:486
static constexpr Mask< T > is_zero(T x)
Definition ct_utils.h:465
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:241
constexpr auto scoped_poison(const Ts &... xs)
Definition ct_utils.h:220
constexpr void unpoison(const T *p, size_t n)
Definition ct_utils.h:65
constexpr void poison(const T *p, size_t n)
Definition ct_utils.h:54
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:55
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