Botan 3.12.0
Crypto and TLS for C&
dilithium.cpp
Go to the documentation of this file.
1/*
2* Crystals Dilithium Digital Signature Algorithms
3* Based on the public domain reference implementation by the
4* designers (https://github.com/pq-crystals/dilithium)
5*
6* Further changes
7* (C) 2021-2023 Jack Lloyd
8* (C) 2021-2022 Manuel Glaser - Rohde & Schwarz Cybersecurity
9* (C) 2021-2023 Michael Boric, René Meusel - Rohde & Schwarz Cybersecurity
10* (C) 2024 René Meusel - Rohde & Schwarz Cybersecurity
11*
12* Botan is released under the Simplified BSD License (see license.txt)
13*/
14
15#include <botan/dilithium.h>
16
17#include <botan/exceptn.h>
18#include <botan/rng.h>
19
20#include <botan/internal/dilithium_algos.h>
21#include <botan/internal/dilithium_keys.h>
22#include <botan/internal/dilithium_symmetric_primitives.h>
23#include <botan/internal/dilithium_types.h>
24#include <botan/internal/fmt.h>
25#include <botan/internal/keypair.h>
26#include <botan/internal/pk_ops_impl.h>
27#include <botan/internal/stl_util.h>
28
29namespace Botan {
30namespace {
31
32DilithiumMode::Mode dilithium_mode_from_string(std::string_view str) {
33 if(str == "Dilithium-4x4-r3") {
35 }
36 if(str == "Dilithium-4x4-AES-r3") {
38 }
39 if(str == "Dilithium-6x5-r3") {
41 }
42 if(str == "Dilithium-6x5-AES-r3") {
44 }
45 if(str == "Dilithium-8x7-r3") {
47 }
48 if(str == "Dilithium-8x7-AES-r3") {
50 }
51 if(str == "ML-DSA-4x4") {
53 }
54 if(str == "ML-DSA-6x5") {
56 }
57 if(str == "ML-DSA-8x7") {
59 }
60
61 throw Invalid_Argument(fmt("'{}' is not a valid Dilithium mode name", str));
62}
63
64} // namespace
65
66DilithiumMode::DilithiumMode(const OID& oid) : m_mode(dilithium_mode_from_string(oid.to_formatted_string())) {}
67
68DilithiumMode::DilithiumMode(std::string_view str) : m_mode(dilithium_mode_from_string(str)) {}
69
73
74std::string DilithiumMode::to_string() const {
75 switch(m_mode) {
77 return "Dilithium-4x4-r3";
79 return "Dilithium-4x4-AES-r3";
81 return "Dilithium-6x5-r3";
83 return "Dilithium-6x5-AES-r3";
85 return "Dilithium-8x7-r3";
87 return "Dilithium-8x7-AES-r3";
89 return "ML-DSA-4x4";
91 return "ML-DSA-6x5";
93 return "ML-DSA-8x7";
94 }
95
97}
98
100 return m_mode == Dilithium4x4_AES || m_mode == Dilithium6x5_AES || m_mode == Dilithium8x7_AES;
101}
102
104 return !is_aes();
105}
106
108 return m_mode == ML_DSA_4x4 || m_mode == ML_DSA_6x5 || m_mode == ML_DSA_8x7;
109}
110
112#if defined(BOTAN_HAS_DILITHIUM_AES)
113 if(is_dilithium_round3() && is_aes()) {
114 return true;
115 }
116#endif
117#if defined(BOTAN_HAS_DILITHIUM)
118 if(is_dilithium_round3() && is_modern()) {
119 return true;
120 }
121#endif
122#if defined(BOTAN_HAS_ML_DSA)
123 if(is_ml_dsa()) {
124 return true;
125 }
126#endif
127 return false;
128}
129
130class Dilithium_Signature_Operation final : public PK_Ops::Signature {
131 public:
132 Dilithium_Signature_Operation(DilithiumInternalKeypair keypair, bool randomized) :
133 m_keypair(std::move(keypair)),
134 m_randomized(randomized),
135 m_h(m_keypair.second->mode().symmetric_primitives().get_message_hash(m_keypair.first->tr())),
136 m_s1(ntt(m_keypair.second->s1().clone())),
137 m_s2(ntt(m_keypair.second->s2().clone())),
138 m_t0(ntt(m_keypair.second->t0().clone())),
139 m_A(Dilithium_Algos::expand_A(m_keypair.first->rho(), m_keypair.second->mode())) {}
140
141 void update(std::span<const uint8_t> input) override { m_h->update(input); }
142
143 /**
144 * NIST FIPS 204, Algorithm 2 (ML-DSA.Sign) and Algorithm 7 (ML-DSA.Sign_internal)
145 *
146 * Note that the private key decoding is done ahead of time. Also, the
147 * matrix expansion of A from 'rho' along with the NTT-transforms of s1,
148 * s2 and t0 are done in the constructor of this class, as a 'signature
149 * operation' may be used to sign multiple messages.
150 *
151 * TODO: Implement support for the specified 'ctx' context string which is
152 * application defined and "empty" by default and <= 255 bytes long.
153 */
154 std::vector<uint8_t> sign(RandomNumberGenerator& rng) override {
155 auto scope = CT::scoped_poison(*m_keypair.second);
156
157 const auto mu = m_h->final();
158 const auto& mode = m_keypair.second->mode();
159 const auto& sympri = mode.symmetric_primitives();
160
161 const auto rhoprime = sympri.H_maybe_randomized(m_keypair.second->signing_seed(), mu, maybe(rng));
162 CT::poison(rhoprime);
163
164 for(uint16_t nonce = 0, n = 0; n <= DilithiumConstants::SIGNING_LOOP_BOUND; ++n, nonce += mode.l()) {
165 const auto y = Dilithium_Algos::expand_mask(rhoprime, nonce, mode);
166
167 auto w_ntt = m_A * ntt(y.clone());
168 w_ntt.reduce();
169 auto w = inverse_ntt(std::move(w_ntt));
170 w.conditional_add_q();
171
172 auto [w1, w0] = Dilithium_Algos::decompose(w, mode);
173 const auto ch = CT::driveby_unpoison(sympri.H(mu, Dilithium_Algos::encode_commitment(w1, mode)));
174
175 const auto c = ntt(Dilithium_Algos::sample_in_ball(ch, mode));
176 const auto cs1 = inverse_ntt(c * m_s1);
177 auto z = y + cs1;
178 z.reduce();
179
180 // We validate the infinity norm of z before proceeding to calculate cs2
181 if(!Dilithium_Algos::infinity_norm_within_bound(z, to_underlying(mode.gamma1()) - mode.beta())) {
182 continue;
183 }
184 CT::unpoison(z); // part of the signature
185
186 const auto cs2 = inverse_ntt(c * m_s2);
187
188 // Note: w0 is used as a scratch space for calculation. We're aliasing
189 // the results to const&'s merely to communicate which value the
190 // intermediate results represent in the specification.
191 w0 -= cs2;
192 w0.reduce();
193 const auto& r0 = w0;
194 if(!Dilithium_Algos::infinity_norm_within_bound(r0, to_underlying(mode.gamma2()) - mode.beta())) {
195 continue;
196 }
197
198 auto ct0 = inverse_ntt(c * m_t0);
199 ct0.reduce();
200 // We validate the infinity norm of ct0 before proceeding to calculate the hint.
201 if(!Dilithium_Algos::infinity_norm_within_bound(ct0, mode.gamma2())) {
202 continue;
203 }
204
205 w0 += ct0;
206 w0.conditional_add_q();
207 const auto& w0cs2ct0 = w0;
208
209 const auto hint = Dilithium_Algos::make_hint(w0cs2ct0, w1, mode);
210 if(CT::driveby_unpoison(hint.hamming_weight()) > mode.omega()) {
211 continue;
212 }
213 CT::unpoison(hint); // part of the signature
214
215 return Dilithium_Algos::encode_signature(ch, z, hint, mode).get();
216 }
217
218 throw Internal_Error("ML-DSA/Dilithium signature loop did not terminate");
219 }
220
221 size_t signature_length() const override { return m_keypair.second->mode().signature_bytes(); }
222
223 AlgorithmIdentifier algorithm_identifier() const override {
224 return AlgorithmIdentifier(m_keypair.second->mode().mode().object_identifier(),
226 }
227
228 std::string hash_function() const override { return m_h->name(); }
229
230 private:
231 std::optional<std::reference_wrapper<RandomNumberGenerator>> maybe(RandomNumberGenerator& rng) const {
232 if(m_randomized) {
233 return rng;
234 } else {
235 return std::nullopt;
236 }
237 }
238
239 private:
240 DilithiumInternalKeypair m_keypair;
241 bool m_randomized;
242 std::unique_ptr<DilithiumMessageHash> m_h;
243
244 const DilithiumPolyVecNTT m_s1;
245 const DilithiumPolyVecNTT m_s2;
246 const DilithiumPolyVecNTT m_t0;
247 const DilithiumPolyMatNTT m_A;
248};
249
250class Dilithium_Verification_Operation final : public PK_Ops::Verification {
251 public:
252 explicit Dilithium_Verification_Operation(std::shared_ptr<Dilithium_PublicKeyInternal> pubkey) :
253 m_pub_key(std::move(pubkey)),
254 m_A(Dilithium_Algos::expand_A(m_pub_key->rho(), m_pub_key->mode())),
255 m_t1_ntt_shifted(ntt(m_pub_key->t1() << DilithiumConstants::D)),
256 m_h(m_pub_key->mode().symmetric_primitives().get_message_hash(m_pub_key->tr())) {}
257
258 void update(std::span<const uint8_t> input) override { m_h->update(input); }
259
260 /**
261 * NIST FIPS 204, Algorithm 3 (ML-DSA.Verify) and 8 (ML-DSA.Verify_internal)
262 *
263 * Note that the public key decoding is done ahead of time. Also, the
264 * matrix A is expanded from 'rho' in the constructor of this class, as
265 * a 'verification operation' may be used to verify multiple signatures.
266 *
267 * TODO: Implement support for the specified 'ctx' context string which is
268 * application defined and "empty" by default and <= 255 bytes long.
269 */
270 bool is_valid_signature(std::span<const uint8_t> sig) override {
271 const auto& mode = m_pub_key->mode();
272 const auto& sympri = mode.symmetric_primitives();
273 const StrongSpan<const DilithiumSerializedSignature> sig_bytes(sig);
274
275 const auto mu = m_h->final();
276
277 if(sig_bytes.size() != mode.signature_bytes()) {
278 return false;
279 }
280
281 auto signature = Dilithium_Algos::decode_signature(sig_bytes, mode);
282 if(!signature.has_value()) {
283 return false;
284 }
285 auto [ch, z, h] = std::move(signature.value());
286
287 // This check was removed from the final version of ML-DSA
288 if(!mode.is_ml_dsa() && h.hamming_weight() > mode.omega()) {
289 return false;
290 }
291
292 if(!Dilithium_Algos::infinity_norm_within_bound(z, to_underlying(mode.gamma1()) - mode.beta())) {
293 return false;
294 }
295
296 const auto c_hat = ntt(Dilithium_Algos::sample_in_ball(ch, mode));
297 auto w_approx = m_A * ntt(std::move(z));
298 w_approx -= c_hat * m_t1_ntt_shifted;
299 w_approx.reduce();
300 auto w1 = inverse_ntt(std::move(w_approx));
301 w1.conditional_add_q();
302 Dilithium_Algos::use_hint(w1, h, mode);
303
304 const auto chprime = sympri.H(mu, Dilithium_Algos::encode_commitment(w1, mode));
305
306 BOTAN_ASSERT_NOMSG(ch.size() == chprime.size());
307 return std::equal(ch.begin(), ch.end(), chprime.begin());
308 }
309
310 std::string hash_function() const override { return m_h->name(); }
311
312 private:
313 std::shared_ptr<Dilithium_PublicKeyInternal> m_pub_key;
315 DilithiumPolyVecNTT m_t1_ntt_shifted;
316 std::unique_ptr<DilithiumMessageHash> m_h;
317};
318
319Dilithium_PublicKey::Dilithium_PublicKey(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> pk) :
320 Dilithium_PublicKey(pk, DilithiumMode(alg_id.oid())) {}
321
323 DilithiumConstants mode(m);
324 BOTAN_ARG_CHECK(mode.mode().is_available(), "Dilithium/ML-DSA mode is not available in this build");
325 BOTAN_ARG_CHECK(pk.empty() || pk.size() == mode.public_key_bytes(),
326 "dilithium public key does not have the correct byte count");
327
329}
330
332 // Note: For Dilithium we made the blunder to return the OID's human readable
333 // name, e.g. "Dilithium-4x4-AES". This is inconsistent with the other
334 // public key algorithms which return the generic name only.
335 //
336 // TODO(Botan4): Fix the inconsistency described above, also considering that
337 // there might be other code locations that identify Dilithium
338 // by std::string::starts_with("Dilithium-").
339 // (Above assumes that Dilithium won't be removed entirely!)
340 return (m_public->mode().is_ml_dsa()) ? std::string("ML-DSA") : object_identifier().to_formatted_string();
341}
342
346
348 return m_public->mode().mode().object_identifier();
349}
350
352 return m_public->mode().canonical_parameter_set_identifier();
353}
354
356 return m_public->mode().lambda();
357}
358
359std::vector<uint8_t> Dilithium_PublicKey::raw_public_key_bits() const {
360 return m_public->raw_pk().get();
361}
362
363std::vector<uint8_t> Dilithium_PublicKey::public_key_bits() const {
364 // Currently, there isn't a finalized definition of an ASN.1 structure for
365 // Dilithium aka ML-DSA public keys. Therefore, we return the raw public key bits.
366 return raw_public_key_bits();
367}
368
369bool Dilithium_PublicKey::check_key(RandomNumberGenerator& /*rng*/, bool /*strong*/) const {
370 // The public key consists of (rho, t1). Length validation is performed in
371 // the constructor, and t1 coefficients are decoded via SimpleBitUnpack
372 // (FIPS 204 Algorithm 18) into a power-of-2 range that exactly covers all
373 // valid values, so no out-of-range coefficients are possible. For the
374 // private key, s1/s2 coefficient ranges are validated and t is recomputed
375 // from (A, s1, s2) and verified against the stored hash during decoding.
376 return true;
377}
378
379std::unique_ptr<Private_Key> Dilithium_PublicKey::generate_another(RandomNumberGenerator& rng) const {
380 return std::make_unique<Dilithium_PrivateKey>(rng, m_public->mode().mode());
381}
382
383std::unique_ptr<PK_Ops::Verification> Dilithium_PublicKey::create_verification_op(std::string_view params,
384 std::string_view provider) const {
385 BOTAN_ARG_CHECK(params.empty() || params == "Pure", "Unexpected parameters for verifying with Dilithium");
386 if(provider.empty() || provider == "base") {
387 return std::make_unique<Dilithium_Verification_Operation>(m_public);
388 }
389 throw Provider_Not_Found(algo_name(), provider);
390}
391
392std::unique_ptr<PK_Ops::Verification> Dilithium_PublicKey::create_x509_verification_op(
393 const AlgorithmIdentifier& alg_id, std::string_view provider) const {
394 if(provider.empty() || provider == "base") {
395 if(alg_id != this->algorithm_identifier()) {
396 throw Decoding_Error("Unexpected AlgorithmIdentifier for Dilithium X.509 signature");
397 }
398 return std::make_unique<Dilithium_Verification_Operation>(m_public);
399 }
400 throw Provider_Not_Found(algo_name(), provider);
401}
402
403/**
404 * NIST FIPS 204, Algorithm 1 (ML-DSA.KeyGen), and 6 (ML-DSA.KeyGen_internal)
405 *
406 * This integrates the seed generation and the actual key generation into one
407 * function. After generation, the relevant components of the key are kept in
408 * memory; the key encoding is deferred until explicitly requested.
409 *
410 * The calculation of (t1, t0) is done in a separate function, as it is also
411 * needed for the decoding of a private key.
412 */
414 DilithiumConstants mode(m);
415 BOTAN_ARG_CHECK(mode.mode().is_available(), "Dilithium/ML-DSA mode is not available in this build");
416 std::tie(m_public, m_private) = Dilithium_Algos::expand_keypair(
418}
419
420Dilithium_PrivateKey::Dilithium_PrivateKey(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> sk) :
421 Dilithium_PrivateKey(sk, DilithiumMode(alg_id.oid())) {}
422
424 DilithiumConstants mode(m);
425 auto& codec = mode.keypair_codec();
426 std::tie(m_public, m_private) = codec.decode_keypair(sk, std::move(mode));
427}
428
432
434 return m_private->mode().keypair_codec().encode_keypair({m_public, m_private});
435}
436
438 std::string_view params,
439 std::string_view provider) const {
440 BOTAN_UNUSED(rng);
441
442 BOTAN_ARG_CHECK(params.empty() || params == "Deterministic" || params == "Randomized",
443 "Unexpected parameters for signing with ML-DSA/Dilithium");
444
445 // FIPS 204, Section 3.4
446 // By default, this standard specifies the signing algorithm to use both
447 // types of randomness [fresh from the RNG and a value in the private key].
448 // This is referred to as the “hedged” variant of the signing procedure.
449 const bool randomized = (params.empty() || params == "Randomized");
450 if(provider.empty() || provider == "base") {
451 return std::make_unique<Dilithium_Signature_Operation>(DilithiumInternalKeypair{m_public, m_private}, randomized);
452 }
453 throw Provider_Not_Found(algo_name(), provider);
454}
455
457 if(!Dilithium_PublicKey::check_key(rng, strong)) {
458 return false;
459 }
460
461 if(strong) {
462 return KeyPair::signature_consistency_check(rng, *this, "");
463 }
464
465 return true;
466}
467
468std::unique_ptr<Public_Key> Dilithium_PrivateKey::public_key() const {
469 return std::make_unique<Dilithium_PublicKey>(*this);
470}
471} // namespace Botan
#define BOTAN_UNUSED
Definition assert.h:144
#define BOTAN_ASSERT_NOMSG(expr)
Definition assert.h:75
#define BOTAN_ARG_CHECK(expr, msg)
Definition assert.h:33
#define BOTAN_ASSERT_UNREACHABLE()
Definition assert.h:163
size_t public_key_bytes() const
byte length of the encoded public key
static constexpr size_t SEED_RANDOMNESS_BYTES
Dilithium_Keypair_Codec & keypair_codec() const
static constexpr uint16_t SIGNING_LOOP_BOUND
OID object_identifier() const
Definition dilithium.cpp:70
std::string to_string() const
Definition dilithium.cpp:74
DilithiumMode(Mode mode)
Definition dilithium.h:37
bool is_aes() const
Definition dilithium.cpp:99
bool is_modern() const
bool is_dilithium_round3() const
Definition dilithium.h:49
bool is_available() const
bool is_ml_dsa() const
virtual DilithiumInternalKeypair decode_keypair(std::span< const uint8_t > private_key, DilithiumConstants mode) const =0
std::unique_ptr< PK_Ops::Signature > create_signature_op(RandomNumberGenerator &rng, std::string_view params, std::string_view provider) const override
Dilithium_PrivateKey(RandomNumberGenerator &rng, DilithiumMode mode)
secure_vector< uint8_t > raw_private_key_bits() const override
secure_vector< uint8_t > private_key_bits() const override
bool check_key(RandomNumberGenerator &rng, bool strong) const override
std::unique_ptr< Public_Key > public_key() const override
static std::shared_ptr< Dilithium_PublicKeyInternal > decode(DilithiumConstants mode, StrongSpan< const DilithiumSerializedPublicKey > raw_pk)
AlgorithmIdentifier algorithm_identifier() const override
std::vector< uint8_t > public_key_bits() const override
OID object_identifier() const override
Dilithium_PublicKey(const AlgorithmIdentifier &alg_id, std::span< const uint8_t > pk)
std::unique_ptr< Private_Key > generate_another(RandomNumberGenerator &rng) const final
std::unique_ptr< PK_Ops::Verification > create_x509_verification_op(const AlgorithmIdentifier &signature_algorithm, std::string_view provider) const override
size_t key_length() const override
std::string algo_name() const override
size_t estimated_strength() const override
std::unique_ptr< PK_Ops::Verification > create_verification_op(std::string_view params, std::string_view provider) const override
std::vector< uint8_t > raw_public_key_bits() const override
std::shared_ptr< Dilithium_PublicKeyInternal > m_public
Definition dilithium.h:106
bool check_key(RandomNumberGenerator &rng, bool strong) const override
std::string to_formatted_string() const
Definition asn1_oid.cpp:139
static OID from_string(std::string_view str)
Definition asn1_oid.cpp:86
virtual AlgorithmIdentifier algorithm_identifier() const
Definition pk_ops.cpp:26
virtual size_t signature_length() const =0
virtual std::string hash_function() const =0
virtual std::string hash_function() const =0
void random_vec(std::span< uint8_t > v)
Definition rng.h:204
constexpr T & get() &
Definition strong_type.h:85
Polynomial< Trait, Domain::NTT > ntt(Polynomial< Trait, Domain::Normal > p)
Definition pqcrystals.h:560
Polynomial< Trait, Domain::Normal > inverse_ntt(Polynomial< Trait, Domain::NTT > p_ntt)
Definition pqcrystals.h:567
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
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)
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::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)
bool signature_consistency_check(RandomNumberGenerator &rng, const Private_Key &private_key, const Public_Key &public_key, std::string_view padding)
Definition keypair.cpp:49
Botan::CRYSTALS::PolynomialVector< DilithiumPolyTraits, Botan::CRYSTALS::Domain::NTT > DilithiumPolyVecNTT
std::string fmt(std::string_view format, const T &... args)
Definition fmt.h:53
Strong< secure_vector< uint8_t >, struct DilithiumSeedRandomness_ > DilithiumSeedRandomness
Principal seed used to generate Dilithium key pairs.
auto to_underlying(T e) noexcept
Definition stl_util.h:142
BOTAN_FORCE_INLINE constexpr T rho(T x)
Definition rotate.h:53
std::pair< std::shared_ptr< Dilithium_PublicKeyInternal >, std::shared_ptr< Dilithium_PrivateKeyInternal > > DilithiumInternalKeypair
Internal representation of a Dilithium key pair.
std::vector< T, secure_allocator< T > > secure_vector
Definition secmem.h:68
Botan::CRYSTALS::PolynomialMatrix< DilithiumPolyTraits > DilithiumPolyMatNTT