Botan 3.6.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/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
33
34namespace {
35
36/**
37 * Returns an all-one mask if @p x is negative, otherwise an all-zero mask.
38 */
39template <std::signed_integral T>
40constexpr auto is_negative_mask(T x) {
41 using unsigned_T = std::make_unsigned_t<T>;
42 return CT::Mask<unsigned_T>::expand_top_bit(static_cast<unsigned_T>(x));
43}
44
45template <DilithiumConstants::T b>
46constexpr std::make_unsigned_t<DilithiumConstants::T> map_range(DilithiumConstants::T c) {
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);
50 return b - c;
51}
52
53template <DilithiumConstants::T b>
54constexpr DilithiumConstants::T unmap_range(std::make_unsigned_t<DilithiumConstants::T> c) {
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);
58}
59
60template <DilithiumConstants::T a, DilithiumConstants::T b, CRYSTALS::crystals_trait PolyTrait, CRYSTALS::Domain D>
61constexpr void poly_pack(const CRYSTALS::Polynomial<PolyTrait, D>& p, BufferStuffer& stuffer) {
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);
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>);
70 }
71}
72
73template <DilithiumConstants::T a,
75 CRYSTALS::byte_source ByteSourceT,
78constexpr void poly_unpack(CRYSTALS::Polynomial<PolyTrait, D>& p, ByteSourceT& get_bytes, bool check_range = false) {
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);
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>);
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 ||
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)) {
95 throw Decoding_Error("Decoded polynomial coefficients out of range");
96 }
97}
98
99/**
100 * NIST FIPS 204, Algorithm 16 (SimpleBitPack)
101 * (for a = 2^(bitlen(q-1)-d) - 1)
102 */
103void poly_pack_t1(const DilithiumPoly& p, BufferStuffer& stuffer) {
104 constexpr auto b = (1 << (bitlen(DilithiumConstants::Q - 1) - DilithiumConstants::D)) - 1;
105 poly_pack<0, b>(p, stuffer);
106}
107
108/**
109 * NIST FIPS 204, Algorithm 16 (SimpleBitPack)
110 * (for a = (q-1)/(2*gamma2-1))
111 */
112void poly_pack_w1(const DilithiumPoly& p, BufferStuffer& stuffer, const DilithiumConstants& mode) {
114 auto calculate_b = [](auto gamma2) { return ((DilithiumConstants::Q - 1) / (2 * gamma2)) - 1; };
115 switch(mode.gamma2()) {
116 case Gamma2::Qminus1DevidedBy88:
117 return poly_pack<0, calculate_b(Gamma2::Qminus1DevidedBy88)>(p, stuffer);
118 case Gamma2::Qminus1DevidedBy32:
119 return poly_pack<0, calculate_b(Gamma2::Qminus1DevidedBy32)>(p, stuffer);
120 }
121
123}
124
125/**
126 * NIST FIPS 204, Algorithm 17 (BitPack)
127 * (for a = -gamma1 - 1, b = gamma1)
128 */
129void poly_pack_gamma1(const DilithiumPoly& p, BufferStuffer& stuffer, const DilithiumConstants& mode) {
131 switch(mode.gamma1()) {
132 case Gamma1::ToThe17th:
133 return poly_pack<Gamma1::ToThe17th - 1, Gamma1::ToThe17th>(p, stuffer);
134 case Gamma1::ToThe19th:
135 return poly_pack<Gamma1::ToThe19th - 1, Gamma1::ToThe19th>(p, stuffer);
136 }
137
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 */
147void poly_pack_eta(const DilithiumPoly& p, BufferStuffer& stuffer, const DilithiumConstants& mode) {
149 switch(mode.eta()) {
150 case Eta::_2:
151 return poly_pack<Eta::_2, Eta::_2>(p, stuffer);
152 case Eta::_4:
153 return poly_pack<Eta::_4, Eta::_4>(p, stuffer);
154 }
155
157}
158
159/**
160 * NIST FIPS 204, Algorithm 17 (BitPack)
161 * (for a = -2^(d-1) - 1, b = 2^(d-1))
162 */
163void poly_pack_t0(const DilithiumPoly& p, BufferStuffer& stuffer) {
164 constexpr auto TwoToTheDminus1 = 1 << (DilithiumConstants::D - 1);
165 poly_pack<TwoToTheDminus1 - 1, TwoToTheDminus1>(p, stuffer);
166}
167
168#endif
169
170/**
171 * NIST FIPS 204, Algorithm 18 (SimpleBitUnpack)
172 * (for a = 2^(bitlen(q-1)-d) - 1)
173 */
174void poly_unpack_t1(DilithiumPoly& p, BufferSlicer& slicer) {
175 constexpr auto b = (1 << (bitlen(DilithiumConstants::Q - 1) - DilithiumConstants::D)) - 1;
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));
180 poly_unpack<0, b>(p, slicer);
181}
182
183/**
184 * NIST FIPS 204, Algorithm 19 (BitUnpack)
185 * (for a = -gamma1 - 1, b = gamma1)
186 */
187template <typename ByteSourceT>
188void poly_unpack_gamma1(DilithiumPoly& p, ByteSourceT& byte_source, const DilithiumConstants& mode) {
190 switch(mode.gamma1()) {
191 case Gamma1::ToThe17th:
192 return poly_unpack<Gamma1::ToThe17th - 1, Gamma1::ToThe17th>(p, byte_source);
193 case Gamma1::ToThe19th:
194 return poly_unpack<Gamma1::ToThe19th - 1, Gamma1::ToThe19th>(p, byte_source);
195 }
196
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 */
206void poly_unpack_eta(DilithiumPoly& p, BufferSlicer& slicer, const DilithiumConstants& mode, bool check_range = false) {
208 switch(mode.eta()) {
209 case Eta::_2:
210 return poly_unpack<Eta::_2, Eta::_2>(p, slicer, check_range);
211 case Eta::_4:
212 return poly_unpack<Eta::_4, Eta::_4>(p, slicer, check_range);
213 }
214
216}
217
218/**
219 * NIST FIPS 204, Algorithm 19 (BitUnpack)
220 * (for a = -2^(d-1) - 1, b = 2^(d-1))
221 */
222void poly_unpack_t0(DilithiumPoly& p, BufferSlicer& slicer) {
223 constexpr auto TwoToTheDminus1 = 1 << (DilithiumConstants::D - 1);
224 poly_unpack<TwoToTheDminus1 - 1, TwoToTheDminus1>(p, slicer);
225}
226
227#endif
228
229/**
230 * NIST FIPS 204, Algorithm 20 (HintBitPack)
231 */
232void hint_pack(const DilithiumPolyVec& h, BufferStuffer& stuffer, const DilithiumConstants& mode) {
233 BOTAN_ASSERT_NOMSG(h.size() == mode.k());
235
236 BufferStuffer bit_positions(stuffer.next(mode.omega()));
237 BufferStuffer offsets(stuffer.next(mode.k()));
238
239 uint8_t index = 0;
240 for(const auto& p : h) {
241 for(size_t i = 0; i < p.size(); ++i) {
242 if(p[i] == 1) {
243 bit_positions.append(static_cast<uint8_t>(i));
244 ++index;
245 }
246 }
247 offsets.append(index);
248 }
249
250 // Fill the remaining bit positions with zeros
251 bit_positions.append(0, bit_positions.remaining_capacity());
252}
253
254/**
255 * NIST FIPS 204, Algorithm 21 (HintBitUnpack)
256 */
257std::optional<DilithiumPolyVec> hint_unpack(BufferSlicer& slicer, const DilithiumConstants& mode) {
258 BufferSlicer bit_positions(slicer.take(mode.omega()));
259 BufferSlicer offsets(slicer.take(mode.k()));
260
261 DilithiumPolyVec hint(mode.k());
262 uint8_t index = 0;
263 for(auto& p : hint) {
264 const auto end_index = offsets.take_byte();
265
266 // Check the bounds of the end index for this polynomial
267 if(end_index < index || end_index > mode.omega()) {
268 return std::nullopt;
269 }
270
271 const auto set_bits = bit_positions.take(end_index - index);
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) {
276 if(set_bits[i] <= set_bits[i - 1]) {
277 return std::nullopt;
278 }
279 }
280
281 // Set the specified bits in the polynomial
282 for(const auto i : set_bits) {
283 p[i] = 1;
284 }
285
286 index = end_index;
287 }
288
289 // Check that the remaining bit positions are all zero (strong unforgeability)
290 const auto remaining = bit_positions.take(bit_positions.remaining());
291 if(!std::all_of(remaining.begin(), remaining.end(), [](auto b) { return b == 0; })) {
292 return std::nullopt;
293 }
294
296 return hint;
297}
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 */
310std::pair<DilithiumPolyVec, DilithiumPolyVec> compute_t1_and_t0(const DilithiumPolyMatNTT& A,
311 const DilithiumPolyVec& s1,
312 const DilithiumPolyVec& s2) {
313 auto t_hat = A * ntt(s1.clone());
314 t_hat.reduce();
315 auto t = inverse_ntt(std::move(t_hat));
316 t += s2;
318
320}
321
322} // namespace
323
324/**
325 * NIST FIPS 204, Algorithm 22 (pkEncode)
326 */
328 const DilithiumPolyVec& t1,
329 const DilithiumConstants& mode) {
331 BufferStuffer stuffer(pk);
332
333 stuffer.append(rho);
334 for(const auto& p : t1) {
335 poly_pack_t1(p, stuffer);
336 }
337
338 BOTAN_ASSERT_NOMSG(stuffer.full());
339 return pk;
340}
341
342/**
343 * NIST FIPS 204, Algorithm 23 (pkDecode)
344 */
345std::pair<DilithiumSeedRho, DilithiumPolyVec> decode_public_key(StrongSpan<const DilithiumSerializedPublicKey> pk,
346 const DilithiumConstants& mode) {
347 if(pk.size() != mode.public_key_bytes()) {
348 throw Decoding_Error("Dilithium: Invalid public key length");
349 }
350
351 BufferSlicer slicer(pk);
353
354 DilithiumPolyVec t1(mode.k());
355 for(auto& p : t1) {
356 poly_unpack_t1(p, slicer);
357 }
358 BOTAN_ASSERT_NOMSG(slicer.empty());
359
360 return {std::move(rho), std::move(t1)};
361}
362
363#if defined(BOTAN_NEEDS_DILITHIUM_PRIVATE_KEY_ENCODING)
364
365/**
366 * NIST FIPS 204, Algorithm 24 (skEncode)
367 */
368DilithiumSerializedPrivateKey encode_keypair(const DilithiumInternalKeypair& keypair) {
369 auto& [pk, sk] = keypair;
372 const auto& mode = sk->mode();
373 auto scope = CT::scoped_poison(*sk);
374
375 DilithiumSerializedPrivateKey serialization(mode.private_key_bytes());
376 BufferStuffer stuffer(serialization);
377
378 stuffer.append(pk->rho());
379 stuffer.append(sk->signing_seed());
380 stuffer.append(pk->tr());
381
382 for(const auto& p : sk->s1()) {
383 poly_pack_eta(p, stuffer, mode);
384 }
385
386 for(const auto& p : sk->s2()) {
387 poly_pack_eta(p, stuffer, mode);
388 }
389
390 for(const auto& p : sk->t0()) {
391 poly_pack_t0(p, stuffer);
392 }
393
394 BOTAN_ASSERT_NOMSG(stuffer.full());
395 CT::unpoison(serialization);
396
397 return serialization;
398}
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 */
409DilithiumInternalKeypair decode_keypair(StrongSpan<const DilithiumSerializedPrivateKey> sk, DilithiumConstants mode) {
410 auto scope = CT::scoped_poison(sk);
411
412 BOTAN_ASSERT_NOMSG(sk.size() == mode.private_key_bytes());
413
414 BufferSlicer slicer(sk);
415
418 auto tr = slicer.copy<DilithiumHashedPublicKey>(mode.public_key_hash_bytes());
419
420 DilithiumPolyVec s1(mode.l());
421 for(auto& p : s1) {
422 poly_unpack_eta(p, slicer, mode, true /* check decoded value range */);
423 }
424
425 DilithiumPolyVec s2(mode.k());
426 for(auto& p : s2) {
427 poly_unpack_eta(p, slicer, mode, true /* check decoded value range */);
428 }
429
430 DilithiumPolyVec t0(mode.k());
431 for(auto& p : t0) {
432 poly_unpack_t0(p, slicer);
433 }
434
435 BOTAN_ASSERT_NOMSG(slicer.empty());
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)
442
443 const auto A = expand_A(rho, mode);
444 auto [t1, _] = compute_t1_and_t0(A, s1, s2);
445
446 CT::unpoison(t1); // part of the public key
447
449 std::make_shared<Dilithium_PublicKeyInternal>(mode, std::move(rho), std::move(t1)),
450 std::make_shared<Dilithium_PrivateKeyInternal>(std::move(mode),
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 };
457
458 CT::unpoison(tr); // hash of the public key
459
460 if(keypair.first->tr() != tr) {
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);
465
466 return keypair;
467}
468
469#endif
470
471/**
472 * NIST FIPS 204, Algorithm 26 (sigEncode)
473 */
475 const DilithiumPolyVec& response,
476 const DilithiumPolyVec& hint,
477 const DilithiumConstants& mode) {
479 BufferStuffer stuffer(sig);
480
481 stuffer.append(c);
482 for(const auto& p : response) {
483 poly_pack_gamma1(p, stuffer, mode);
484 }
485 hint_pack(hint, stuffer, mode);
486
487 return sig;
488}
489
490/**
491 * NIST FIPS 204, Algorithm 27 (sigDecode)
492 */
493std::optional<std::tuple<DilithiumCommitmentHash, DilithiumPolyVec, DilithiumPolyVec>> decode_signature(
495 BufferSlicer slicer(sig);
496 BOTAN_ASSERT_NOMSG(slicer.remaining() == mode.signature_bytes());
497
498 auto commitment_hash = slicer.copy<DilithiumCommitmentHash>(mode.commitment_hash_full_bytes());
499
500 DilithiumPolyVec response(mode.l());
501 for(auto& p : response) {
502 poly_unpack_gamma1(p, slicer, mode);
503 }
504 BOTAN_ASSERT_NOMSG(slicer.remaining() == mode.omega() + mode.k());
505
506 auto hint = hint_unpack(slicer, mode);
507 BOTAN_ASSERT_NOMSG(slicer.empty());
508 if(!hint.has_value()) {
509 return std::nullopt;
510 }
511
512 return std::make_tuple(std::move(commitment_hash), std::move(response), std::move(hint.value()));
513}
514
515/**
516 * NIST FIPS 204, Algorithm 28 (w1Encode)
517 */
520 BufferStuffer stuffer(commitment);
521
522 for(const auto& p : w1) {
523 poly_pack_w1(p, stuffer, mode);
524 }
525
526 return commitment;
527}
528
529/**
530 * NIST FIPS 204, Algorithm 29 (SampleInBall)
531 */
533 // This generator resembles the while loop in the spec.
534 auto& xof = mode.symmetric_primitives().H(seed);
536
538 uint64_t signs = load_le(bounded_xof.next<8>());
539 for(size_t i = c.size() - mode.tau(); i < c.size(); ++i) {
540 const auto j = bounded_xof.next_byte([i](uint8_t byte) { return byte <= i; });
541 c[i] = c[j];
542 c[j] = 1 - 2 * (signs & 1);
543 signs >>= 1;
544 }
545
548
549 return c;
550}
551
552namespace {
553
554/**
555 * NIST FIPS 204, Algorithm 30 (RejNTTPoly)
556 */
557void sample_ntt_uniform(StrongSpan<const DilithiumSeedRho> rho,
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);
567
568 for(auto& coeff : p) {
569 coeff =
570 bounded_xof.next<3>([](const auto bytes) { return make_uint32(0, bytes[2], bytes[1], bytes[0]) & 0x7FFFFF; },
571 [](const uint32_t z) { return z < DilithiumConstants::Q; });
572 }
573
574 BOTAN_DEBUG_ASSERT(p.ct_validate_value_range(0, DilithiumConstants::Q - 1));
575}
576
577/**
578 * NIST FIPS 204, Algorithm 15 (CoeffFromHalfByte)
579 *
580 * Magic numbers for (b mod 5) are taken from the reference implementation.
581 */
582template <DilithiumConstants::DilithiumEta eta>
583std::optional<int32_t> coeff_from_halfbyte(uint8_t b) {
584 BOTAN_DEBUG_ASSERT(b < 16);
585
586 if constexpr(eta == DilithiumConstants::DilithiumEta::_2) {
587 if(CT::driveby_unpoison(b < 15)) {
588 b = b - (205 * b >> 10) * 5; // b = b mod 5
589 return 2 - b;
590 }
591 } else if constexpr(eta == DilithiumConstants::DilithiumEta::_4) {
592 if(CT::driveby_unpoison(b < 9)) {
593 return 4 - b;
594 }
595 }
596
597 return std::nullopt;
598}
599
600template <DilithiumConstants::DilithiumEta eta>
601void sample_uniform_eta(DilithiumPoly& p, Botan::XOF& xof) {
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),
606 stashed_coeff = std::optional<int32_t>{}]() mutable -> int32_t {
607 if(auto stashed = std::exchange(stashed_coeff, std::nullopt)) {
608 return *stashed;
609 }
610
611 BOTAN_DEBUG_ASSERT(!stashed_coeff.has_value());
612 while(true) {
613 const auto b = bounded_xof.next_byte();
614 const auto z0 = coeff_from_halfbyte<eta>(b & 0x0F);
615 const auto z1 = coeff_from_halfbyte<eta>(b >> 4);
616
617 if(z0.has_value()) {
618 stashed_coeff = z1; // keep candidate z1 for the next invocation
619 return *z0;
620 } else if(z1.has_value()) {
621 // z0 was invalid, z1 is valid, nothing to stash
622 return *z1;
623 }
624 }
625 };
626
627 for(auto& coeff : p) {
628 coeff = next_coeff();
629 }
630}
631
632/**
633 * NIST FIPS 204, Algorithm 31 (RejBoundedPoly)
634 */
635void sample_uniform_eta(StrongSpan<const DilithiumSeedRhoPrime> rhoprime,
636 DilithiumPoly& p,
637 uint16_t nonce,
638 const DilithiumConstants& mode) {
640
641 auto& xof = mode.symmetric_primitives().H(rhoprime, nonce);
642 switch(mode.eta()) {
643 case Eta::_2:
644 sample_uniform_eta<Eta::_2>(p, xof);
645 break;
646 case Eta::_4:
647 sample_uniform_eta<Eta::_4>(p, xof);
648 break;
649 }
650
651 // Rejection sampling is done. Secret polynomial can be repoisoned.
652 CT::poison(p);
653
654 BOTAN_DEBUG_ASSERT(p.ct_validate_value_range(-static_cast<int32_t>(mode.eta()), mode.eta()));
655}
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 */
666 const auto& sympriv = mode.symmetric_primitives();
667 CT::poison(xi);
668
669 auto [rho, rhoprime, K] = sympriv.H(xi);
670 CT::unpoison(rho); // rho is public (seed for the public matrix A)
671
672 const auto A = Dilithium_Algos::expand_A(rho, mode);
673 auto [s1, s2] = Dilithium_Algos::expand_s(rhoprime, mode);
674 auto [t1, t0] = Dilithium_Algos::compute_t1_and_t0(A, s1, s2);
675
676 CT::unpoison(t1); // part of the public key
677
679 std::make_shared<Dilithium_PublicKeyInternal>(mode, std::move(rho), std::move(t1)),
680 std::make_shared<Dilithium_PrivateKeyInternal>(
681 std::move(mode), std::move(xi), std::move(K), std::move(s1), std::move(s2), std::move(t0)),
682 };
683
684 CT::unpoison(*keypair.second);
685
686 return keypair;
687};
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 */
696 DilithiumPolyMatNTT A(mode.k(), mode.l());
697 for(uint8_t r = 0; r < mode.k(); ++r) {
698 for(uint8_t s = 0; s < mode.l(); ++s) {
699 sample_ntt_uniform(rho, A[r][s], load_le(std::array{s, r}), mode);
700 }
701 }
702 return A;
703}
704
705/**
706 * NIST FIPS 204, Algorithm 33 (ExpandS)
707 */
708std::pair<DilithiumPolyVec, DilithiumPolyVec> expand_s(StrongSpan<const DilithiumSeedRhoPrime> rhoprime,
709 const DilithiumConstants& mode) {
710 auto result = std::make_pair(DilithiumPolyVec(mode.l()), DilithiumPolyVec(mode.k()));
711 auto& [s1, s2] = result;
712
713 uint16_t nonce = 0;
714 for(auto& p : s1) {
715 sample_uniform_eta(rhoprime, p, nonce++, mode);
716 }
717
718 for(auto& p : s2) {
719 sample_uniform_eta(rhoprime, p, nonce++, mode);
720 }
721
722 return result;
723}
724
725/**
726 * NIST FIPS 204, Algorithm 34 (ExpandMask)
727 */
729 uint16_t nonce,
730 const DilithiumConstants& mode) {
731 DilithiumPolyVec s(mode.l());
732 for(auto& p : s) {
733 auto& xof = mode.symmetric_primitives().H(rhoprime, nonce++);
734 poly_unpack_gamma1(p, xof, mode);
735 }
736 return s;
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 */
746std::pair<DilithiumPolyVec, DilithiumPolyVec> power2round(const DilithiumPolyVec& vec) {
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> {
749 const int32_t r1 = (r + (1 << (d - 1)) - 1) >> d;
750 const int32_t r0 = r - (r1 << d);
751 return {r1, r0};
752 };
753
754 auto result = std::make_pair(DilithiumPolyVec(vec.size()), DilithiumPolyVec(vec.size()));
755
756 for(size_t i = 0; i < vec.size(); ++i) {
757 for(size_t j = 0; j < vec[i].size(); ++j) {
758 std::tie(result.first[i][j], result.second[i][j]) = power2round(vec[i][j]);
759 }
760 }
761
762 return result;
763}
764
765namespace {
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 */
774template <DilithiumConstants::DilithiumGamma2 gamma2>
775std::pair<int32_t, int32_t> decompose(int32_t r) {
776 int32_t r1 = (r + 127) >> 7;
777
779 r1 = (r1 * 1025 + (1 << 21)) >> 22;
780 r1 &= 15;
781 } else if constexpr(gamma2 == DilithiumConstants::DilithiumGamma2::Qminus1DevidedBy88) {
782 r1 = (r1 * 11275 + (1 << 23)) >> 24;
783 r1 = is_negative_mask(43 - r1).if_not_set_return(r1);
784 }
785
786 int32_t r0 = r - r1 * 2 * gamma2;
787
788 // reduce r0 mod q
789 r0 -= is_negative_mask((DilithiumConstants::Q - 1) / 2 - r0).if_set_return(DilithiumConstants::Q);
790
791 return {r1, r0};
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 */
798template <DilithiumConstants::DilithiumGamma2 gamma2>
799std::pair<DilithiumPolyVec, DilithiumPolyVec> decompose_all_coefficents(const DilithiumPolyVec& vec) {
800 auto result = std::make_pair(DilithiumPolyVec(vec.size()), DilithiumPolyVec(vec.size()));
801
802 for(size_t i = 0; i < vec.size(); ++i) {
803 for(size_t j = 0; j < vec[i].size(); ++j) {
804 std::tie(result.first[i][j], result.second[i][j]) = decompose<gamma2>(vec[i][j]);
805 }
806 }
807
808 return result;
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 */
819std::pair<DilithiumPolyVec, DilithiumPolyVec> decompose(const DilithiumPolyVec& vec, const DilithiumConstants& mode) {
821 switch(mode.gamma2()) {
822 case Gamma2::Qminus1DevidedBy32:
823 return decompose_all_coefficents<Gamma2::Qminus1DevidedBy32>(vec);
824 break;
825 case Gamma2::Qminus1DevidedBy88:
826 return decompose_all_coefficents<Gamma2::Qminus1DevidedBy88>(vec);
827 break;
828 }
829
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 */
844 BOTAN_DEBUG_ASSERT(z.size() == r.size());
845
846 auto make_hint = [gamma2 = uint32_t(mode.gamma2()),
847 q_gamma2 = static_cast<uint32_t>(DilithiumConstants::Q) - uint32_t(mode.gamma2())](
848 int32_t c0, int32_t c1) -> CT::Choice {
849 BOTAN_DEBUG_ASSERT(c0 >= 0);
850 BOTAN_DEBUG_ASSERT(c1 >= 0);
851
852 const uint32_t pc0 = static_cast<uint32_t>(c0);
853 const uint32_t pc1 = static_cast<uint32_t>(c1);
854
855 return (CT::Mask<uint32_t>::is_gt(pc0, gamma2) & CT::Mask<uint32_t>::is_lte(pc0, q_gamma2) &
857 .as_choice();
858 };
859
860 DilithiumPolyVec hint(r.size());
861
862 for(size_t i = 0; i < r.size(); ++i) {
863 for(size_t j = 0; j < r[i].size(); ++j) {
864 hint[i][j] = make_hint(z[i][j], r[i][j]).as_bool();
865 }
866 }
867
869
870 return hint;
871}
872
873namespace {
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 */
879template <DilithiumConstants::DilithiumGamma2 gamma2>
880void use_hint_on_coefficients(const DilithiumPolyVec& hints, DilithiumPolyVec& vec) {
881 constexpr auto m = (DilithiumConstants::Q - 1) / (2 * gamma2);
882
883 auto modulo_m = [](int32_t r1) -> int32_t {
884 BOTAN_DEBUG_ASSERT(r1 >= -static_cast<decltype(r1)>(m) && r1 <= static_cast<decltype(r1)>(m));
885 return (r1 + m) % m;
886 };
887
888 auto use_hint = [&modulo_m](bool hint, int32_t r) -> int32_t {
889 auto [r1, r0] = decompose<gamma2>(r);
890
891 if(!hint) {
892 return r1;
893 }
894
895 if(r0 > 0) {
896 return modulo_m(r1 + 1);
897 } else {
898 return modulo_m(r1 - 1);
899 }
900 };
901
902 for(size_t i = 0; i < vec.size(); ++i) {
903 for(size_t j = 0; j < vec[i].size(); ++j) {
904 vec[i][j] = use_hint(hints[i][j], vec[i][j]);
905 }
906 }
907}
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 */
918void use_hint(DilithiumPolyVec& vec, const DilithiumPolyVec& hints, const DilithiumConstants& mode) {
919 BOTAN_DEBUG_ASSERT(hints.size() == vec.size());
922
924 switch(mode.gamma2()) {
925 case Gamma2::Qminus1DevidedBy32:
926 use_hint_on_coefficients<Gamma2::Qminus1DevidedBy32>(hints, vec);
927 break;
928 case Gamma2::Qminus1DevidedBy88:
929 use_hint_on_coefficients<Gamma2::Qminus1DevidedBy88>(hints, vec);
930 break;
931 }
932
934}
935
936bool infinity_norm_within_bound(const DilithiumPolyVec& vec, size_t bound) {
937 BOTAN_DEBUG_ASSERT(bound <= (DilithiumConstants::Q - 1) / 8);
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) {
943 for(auto c : p) {
944 const auto abs_c = c - is_negative_mask(c).if_set_return(2 * c);
945 if(CT::driveby_unpoison(abs_c >= bound)) {
946 return false;
947 }
948 }
949 }
950
951 return true;
952}
953
954} // namespace Botan::Dilithium_Algos
#define BOTAN_ASSERT_NOMSG(expr)
Definition assert.h:59
#define BOTAN_DEBUG_ASSERT(expr)
Definition assert.h:98
#define BOTAN_ASSERT_NONNULL(ptr)
Definition assert.h:86
#define BOTAN_ASSERT_UNREACHABLE()
Definition assert.h:137
size_t remaining() const
Definition stl_util.h:127
uint8_t take_byte()
Definition stl_util.h:118
auto copy(const size_t count)
Definition stl_util.h:89
bool empty() const
Definition stl_util.h:129
std::span< const uint8_t > take(const size_t count)
Definition stl_util.h:98
Helper class to ease in-place marshalling of concatenated fixed-length values.
Definition stl_util.h:142
constexpr void append(std::span< const uint8_t > buffer)
Definition stl_util.h:177
constexpr size_t remaining_capacity() const
Definition stl_util.h:189
constexpr std::span< uint8_t > next(size_t bytes)
Definition stl_util.h:150
constexpr bool full() const
Definition stl_util.h:187
ThisPolynomialVector clone() const
Definition pqcrystals.h:415
constexpr bool ct_validate_value_range(T min, T max) const noexcept
Definition pqcrystals.h:436
ThisPolynomialVector & conditional_add_q()
Definition pqcrystals.h:467
constexpr size_t hamming_weight() const noexcept
Definition pqcrystals.h:292
constexpr size_t size() const
Definition pqcrystals.h:274
constexpr bool ct_validate_value_range(T min, T max) const noexcept
Definition pqcrystals.h:287
static constexpr Mask< T > expand_top_bit(T v)
Definition ct_utils.h:407
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()))
FE_25519 T
Definition ge.cpp:34
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:237
constexpr auto scoped_poison(const Ts &... xs)
Definition ct_utils.h:216
constexpr void unpoison(const T *p, size_t n)
Definition ct_utils.h:64
constexpr void poison(const T *p, size_t n)
Definition ct_utils.h:53
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)
constexpr T rho(T x)
Definition rotate.h:51
constexpr bool is_power_of_2(T arg)
Definition bit_ops.h:45
Strong< secure_vector< uint8_t >, struct DilithiumSeedK_ > DilithiumSigningSeedK
Private seed K used during signing.
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.
constexpr uint32_t make_uint32(uint8_t i0, uint8_t i1, uint8_t i2, uint8_t i3)
Definition loadstor.h:100
constexpr auto load_le(ParamTs &&... params)
Definition loadstor.h:521
Botan::CRYSTALS::Polynomial< DilithiumPolyTraits, Botan::CRYSTALS::Domain::Normal > DilithiumPoly
const SIMD_8x32 & b
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