Botan 3.6.1
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/pk_ops_impl.h>
26#include <botan/internal/stl_util.h>
27
28namespace Botan {
29namespace {
30
31DilithiumMode::Mode dilithium_mode_from_string(std::string_view str) {
32 if(str == "Dilithium-4x4-r3") {
34 }
35 if(str == "Dilithium-4x4-AES-r3") {
37 }
38 if(str == "Dilithium-6x5-r3") {
40 }
41 if(str == "Dilithium-6x5-AES-r3") {
43 }
44 if(str == "Dilithium-8x7-r3") {
46 }
47 if(str == "Dilithium-8x7-AES-r3") {
49 }
50 if(str == "ML-DSA-4x4") {
52 }
53 if(str == "ML-DSA-6x5") {
55 }
56 if(str == "ML-DSA-8x7") {
58 }
59
60 throw Invalid_Argument(fmt("'{}' is not a valid Dilithium mode name", str));
61}
62
63} // namespace
64
65DilithiumMode::DilithiumMode(const OID& oid) : m_mode(dilithium_mode_from_string(oid.to_formatted_string())) {}
66
67DilithiumMode::DilithiumMode(std::string_view str) : m_mode(dilithium_mode_from_string(str)) {}
68
72
73std::string DilithiumMode::to_string() const {
74 switch(m_mode) {
76 return "Dilithium-4x4-r3";
78 return "Dilithium-4x4-AES-r3";
80 return "Dilithium-6x5-r3";
82 return "Dilithium-6x5-AES-r3";
84 return "Dilithium-8x7-r3";
86 return "Dilithium-8x7-AES-r3";
88 return "ML-DSA-4x4";
90 return "ML-DSA-6x5";
92 return "ML-DSA-8x7";
93 }
94
96}
97
99 return m_mode == Dilithium4x4_AES || m_mode == Dilithium6x5_AES || m_mode == Dilithium8x7_AES;
100}
101
103 return !is_aes();
104}
105
107 return m_mode == ML_DSA_4x4 || m_mode == ML_DSA_6x5 || m_mode == ML_DSA_8x7;
108}
109
111#if defined(BOTAN_HAS_DILITHIUM_AES)
112 if(is_dilithium_round3() && is_aes()) {
113 return true;
114 }
115#endif
116#if defined(BOTAN_HAS_DILITHIUM)
117 if(is_dilithium_round3() && is_modern()) {
118 return true;
119 }
120#endif
121#if defined(BOTAN_HAS_ML_DSA)
122 if(is_ml_dsa()) {
123 return true;
124 }
125#endif
126 return false;
127}
128
129class Dilithium_Signature_Operation final : public PK_Ops::Signature {
130 public:
131 Dilithium_Signature_Operation(DilithiumInternalKeypair keypair, bool randomized) :
132 m_keypair(std::move(keypair)),
133 m_randomized(randomized),
134 m_h(m_keypair.second->mode().symmetric_primitives().get_message_hash(m_keypair.first->tr())),
135 m_s1(ntt(m_keypair.second->s1().clone())),
136 m_s2(ntt(m_keypair.second->s2().clone())),
137 m_t0(ntt(m_keypair.second->t0().clone())),
138 m_A(Dilithium_Algos::expand_A(m_keypair.first->rho(), m_keypair.second->mode())) {}
139
140 void update(std::span<const uint8_t> input) override { m_h->update(input); }
141
142 /**
143 * NIST FIPS 204, Algorithm 2 (ML-DSA.Sign) and Algorithm 7 (ML-DSA.Sign_internal)
144 *
145 * Note that the private key decoding is done ahead of time. Also, the
146 * matrix expansion of A from 'rho' along with the NTT-transforms of s1,
147 * s2 and t0 are done in the constructor of this class, as a 'signature
148 * operation' may be used to sign multiple messages.
149 *
150 * TODO: Implement support for the specified 'ctx' context string which is
151 * application defined and "empty" by default and <= 255 bytes long.
152 */
153 std::vector<uint8_t> sign(RandomNumberGenerator& rng) override {
154 auto scope = CT::scoped_poison(*m_keypair.second);
155
156 const auto mu = m_h->final();
157 const auto& mode = m_keypair.second->mode();
158 const auto& sympri = mode.symmetric_primitives();
159
160 const auto rhoprime = sympri.H_maybe_randomized(m_keypair.second->signing_seed(), mu, maybe(rng));
161 CT::poison(rhoprime);
162
163 for(uint16_t nonce = 0, n = 0; n <= DilithiumConstants::SIGNING_LOOP_BOUND; ++n, nonce += mode.l()) {
164 const auto y = Dilithium_Algos::expand_mask(rhoprime, nonce, mode);
165
166 auto w_ntt = m_A * ntt(y.clone());
167 w_ntt.reduce();
168 auto w = inverse_ntt(std::move(w_ntt));
169 w.conditional_add_q();
170
171 auto [w1, w0] = Dilithium_Algos::decompose(w, mode);
172 const auto ch = CT::driveby_unpoison(sympri.H(mu, Dilithium_Algos::encode_commitment(w1, mode)));
173
174 const auto c = ntt(Dilithium_Algos::sample_in_ball(ch, mode));
175 const auto cs1 = inverse_ntt(c * m_s1);
176 auto z = y + cs1;
177 z.reduce();
178
179 // We validate the infinity norm of z before proceeding to calculate cs2
180 if(!Dilithium_Algos::infinity_norm_within_bound(z, to_underlying(mode.gamma1()) - mode.beta())) {
181 continue;
182 }
183 CT::unpoison(z); // part of the signature
184
185 const auto cs2 = inverse_ntt(c * m_s2);
186
187 // Note: w0 is used as a scratch space for calculation. We're aliasing
188 // the results to const&'s merely to communicate which value the
189 // intermediate results represent in the specification.
190 w0 -= cs2;
191 w0.reduce();
192 const auto& r0 = w0;
193 if(!Dilithium_Algos::infinity_norm_within_bound(r0, to_underlying(mode.gamma2()) - mode.beta())) {
194 continue;
195 }
196
197 auto ct0 = inverse_ntt(c * m_t0);
198 ct0.reduce();
199 // We validate the infinity norm of ct0 before proceeding to calculate the hint.
200 if(!Dilithium_Algos::infinity_norm_within_bound(ct0, mode.gamma2())) {
201 continue;
202 }
203
204 w0 += ct0;
205 w0.conditional_add_q();
206 const auto& w0cs2ct0 = w0;
207
208 const auto hint = Dilithium_Algos::make_hint(w0cs2ct0, w1, mode);
209 if(CT::driveby_unpoison(hint.hamming_weight()) > mode.omega()) {
210 continue;
211 }
212 CT::unpoison(hint); // part of the signature
213
214 return Dilithium_Algos::encode_signature(ch, z, hint, mode).get();
215 }
216
217 throw Internal_Error("ML-DSA/Dilithium signature loop did not terminate");
218 }
219
220 size_t signature_length() const override { return m_keypair.second->mode().signature_bytes(); }
221
222 AlgorithmIdentifier algorithm_identifier() const override {
223 return AlgorithmIdentifier(m_keypair.second->mode().mode().object_identifier(),
225 }
226
227 std::string hash_function() const override { return m_h->name(); }
228
229 private:
230 std::optional<std::reference_wrapper<RandomNumberGenerator>> maybe(RandomNumberGenerator& rng) const {
231 if(m_randomized) {
232 return rng;
233 } else {
234 return std::nullopt;
235 }
236 }
237
238 private:
239 DilithiumInternalKeypair m_keypair;
240 bool m_randomized;
241 std::unique_ptr<DilithiumMessageHash> m_h;
242
243 const DilithiumPolyVecNTT m_s1;
244 const DilithiumPolyVecNTT m_s2;
245 const DilithiumPolyVecNTT m_t0;
246 const DilithiumPolyMatNTT m_A;
247};
248
249class Dilithium_Verification_Operation final : public PK_Ops::Verification {
250 public:
251 Dilithium_Verification_Operation(std::shared_ptr<Dilithium_PublicKeyInternal> pubkey) :
252 m_pub_key(std::move(pubkey)),
253 m_A(Dilithium_Algos::expand_A(m_pub_key->rho(), m_pub_key->mode())),
254 m_t1_ntt_shifted(ntt(m_pub_key->t1() << DilithiumConstants::D)),
255 m_h(m_pub_key->mode().symmetric_primitives().get_message_hash(m_pub_key->tr())) {}
256
257 void update(std::span<const uint8_t> input) override { m_h->update(input); }
258
259 /**
260 * NIST FIPS 204, Algorithm 3 (ML-DSA.Verify) and 8 (ML-DSA.Verify_internal)
261 *
262 * Note that the public key decoding is done ahead of time. Also, the
263 * matrix A is expanded from 'rho' in the constructor of this class, as
264 * a 'verification operation' may be used to verify multiple signatures.
265 *
266 * TODO: Implement support for the specified 'ctx' context string which is
267 * application defined and "empty" by default and <= 255 bytes long.
268 */
269 bool is_valid_signature(std::span<const uint8_t> sig) override {
270 const auto& mode = m_pub_key->mode();
271 const auto& sympri = mode.symmetric_primitives();
272 StrongSpan<const DilithiumSerializedSignature> sig_bytes(sig);
273
274 if(sig_bytes.size() != mode.signature_bytes()) {
275 return false;
276 }
277
278 const auto mu = m_h->final();
279
280 auto signature = Dilithium_Algos::decode_signature(sig_bytes, mode);
281 if(!signature.has_value()) {
282 return false;
283 }
284 auto [ch, z, h] = std::move(signature.value());
285
286 // TODO: The first check was removed from the final version of ML-DSA
287 if(h.hamming_weight() > mode.omega() ||
288 !Dilithium_Algos::infinity_norm_within_bound(z, to_underlying(mode.gamma1()) - mode.beta())) {
289 return false;
290 }
291
292 const auto c_hat = ntt(Dilithium_Algos::sample_in_ball(ch, mode));
293 auto w_approx = m_A * ntt(std::move(z));
294 w_approx -= c_hat * m_t1_ntt_shifted;
295 w_approx.reduce();
296 auto w1 = inverse_ntt(std::move(w_approx));
297 w1.conditional_add_q();
298 Dilithium_Algos::use_hint(w1, h, mode);
299
300 const auto chprime = sympri.H(mu, Dilithium_Algos::encode_commitment(w1, mode));
301
302 BOTAN_ASSERT_NOMSG(ch.size() == chprime.size());
303 return std::equal(ch.begin(), ch.end(), chprime.begin());
304 }
305
306 std::string hash_function() const override { return m_h->name(); }
307
308 private:
309 std::shared_ptr<Dilithium_PublicKeyInternal> m_pub_key;
311 DilithiumPolyVecNTT m_t1_ntt_shifted;
312 std::unique_ptr<DilithiumMessageHash> m_h;
313};
314
315Dilithium_PublicKey::Dilithium_PublicKey(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> pk) :
316 Dilithium_PublicKey(pk, DilithiumMode(alg_id.oid())) {}
317
319 DilithiumConstants mode(m);
320 BOTAN_ARG_CHECK(mode.mode().is_available(), "Dilithium/ML-DSA mode is not available in this build");
321 BOTAN_ARG_CHECK(pk.empty() || pk.size() == mode.public_key_bytes(),
322 "dilithium public key does not have the correct byte count");
323
325}
326
328 // Note: For Dilithium we made the blunder to return the OID's human readable
329 // name, e.g. "Dilithium-4x4-AES". This is inconsistent with the other
330 // public key algorithms which return the generic name only.
331 //
332 // TODO(Botan4): Fix the inconsistency described above, also considering that
333 // there might be other code locations that identify Dilithium
334 // by std::string::starts_with("Dilithium-").
335 // (Above assumes that Dilithium won't be removed entirely!)
336 return (m_public->mode().is_ml_dsa()) ? std::string("ML-DSA") : object_identifier().to_formatted_string();
337}
338
342
344 return m_public->mode().mode().object_identifier();
345}
346
348 return m_public->mode().canonical_parameter_set_identifier();
349}
350
352 return m_public->mode().lambda();
353}
354
355std::vector<uint8_t> Dilithium_PublicKey::raw_public_key_bits() const {
356 return m_public->raw_pk().get();
357}
358
359std::vector<uint8_t> Dilithium_PublicKey::public_key_bits() const {
360 // Currently, there isn't a finalized definition of an ASN.1 structure for
361 // Dilithium aka ML-DSA public keys. Therefore, we return the raw public key bits.
362 return raw_public_key_bits();
363}
364
366 return true; // ???
367}
368
369std::unique_ptr<Private_Key> Dilithium_PublicKey::generate_another(RandomNumberGenerator& rng) const {
370 return std::make_unique<Dilithium_PrivateKey>(rng, m_public->mode().mode());
371}
372
373std::unique_ptr<PK_Ops::Verification> Dilithium_PublicKey::create_verification_op(std::string_view params,
374 std::string_view provider) const {
375 BOTAN_ARG_CHECK(params.empty() || params == "Pure", "Unexpected parameters for verifying with Dilithium");
376 if(provider.empty() || provider == "base") {
377 return std::make_unique<Dilithium_Verification_Operation>(m_public);
378 }
379 throw Provider_Not_Found(algo_name(), provider);
380}
381
382std::unique_ptr<PK_Ops::Verification> Dilithium_PublicKey::create_x509_verification_op(
383 const AlgorithmIdentifier& alg_id, std::string_view provider) const {
384 if(provider.empty() || provider == "base") {
385 if(alg_id != this->algorithm_identifier()) {
386 throw Decoding_Error("Unexpected AlgorithmIdentifier for Dilithium X.509 signature");
387 }
388 return std::make_unique<Dilithium_Verification_Operation>(m_public);
389 }
390 throw Provider_Not_Found(algo_name(), provider);
391}
392
393/**
394 * NIST FIPS 204, Algorithm 1 (ML-DSA.KeyGen), and 6 (ML-DSA.KeyGen_internal)
395 *
396 * This integrates the seed generation and the actual key generation into one
397 * function. After generation, the relevant components of the key are kept in
398 * memory; the key encoding is deferred until explicitly requested.
399 *
400 * The calculation of (t1, t0) is done in a separate function, as it is also
401 * needed for the decoding of a private key.
402 */
404 DilithiumConstants mode(m);
405 BOTAN_ARG_CHECK(mode.mode().is_available(), "Dilithium/ML-DSA mode is not available in this build");
406 std::tie(m_public, m_private) = Dilithium_Algos::expand_keypair(
408}
409
410Dilithium_PrivateKey::Dilithium_PrivateKey(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> sk) :
411 Dilithium_PrivateKey(sk, DilithiumMode(alg_id.oid())) {}
412
414 DilithiumConstants mode(m);
415 auto& codec = mode.keypair_codec();
416 std::tie(m_public, m_private) = codec.decode_keypair(sk, std::move(mode));
417}
418
422
424 return m_private->mode().keypair_codec().encode_keypair({m_public, m_private});
425}
426
428 std::string_view params,
429 std::string_view provider) const {
430 BOTAN_UNUSED(rng);
431
432 BOTAN_ARG_CHECK(params.empty() || params == "Deterministic" || params == "Randomized",
433 "Unexpected parameters for signing with ML-DSA/Dilithium");
434
435 // FIPS 204, Section 3.4
436 // By default, this standard specifies the signing algorithm to use both
437 // types of randomness [fresh from the RNG and a value in the private key].
438 // This is referred to as the “hedged” variant of the signing procedure.
439 const bool randomized = (params.empty() || params == "Randomized");
440 if(provider.empty() || provider == "base") {
441 return std::make_unique<Dilithium_Signature_Operation>(DilithiumInternalKeypair{m_public, m_private}, randomized);
442 }
443 throw Provider_Not_Found(algo_name(), provider);
444}
445
446std::unique_ptr<Public_Key> Dilithium_PrivateKey::public_key() const {
447 return std::make_unique<Dilithium_PublicKey>(*this);
448}
449} // namespace Botan
#define BOTAN_UNUSED
Definition assert.h:118
#define BOTAN_ASSERT_NOMSG(expr)
Definition assert.h:59
#define BOTAN_ARG_CHECK(expr, msg)
Definition assert.h:29
#define BOTAN_ASSERT_UNREACHABLE()
Definition assert.h:137
ThisPolynomialVector & reduce()
Definition pqcrystals.h:460
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:69
std::string to_string() const
Definition dilithium.cpp:73
DilithiumMode(Mode mode)
Definition dilithium.h:36
bool is_aes() const
Definition dilithium.cpp:98
bool is_modern() const
bool is_dilithium_round3() const
Definition dilithium.h:48
bool is_available() const
bool is_ml_dsa() const
virtual DilithiumInternalKeypair decode_keypair(std::span< const uint8_t > private_key, DilithiumConstants mode) const =0
Dilithium_PrivateKey(RandomNumberGenerator &rng, DilithiumMode mode)
secure_vector< uint8_t > raw_private_key_bits() const override
std::unique_ptr< PK_Ops::Signature > create_signature_op(RandomNumberGenerator &, std::string_view params, std::string_view provider) const override
secure_vector< uint8_t > private_key_bits() 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
bool check_key(RandomNumberGenerator &, bool) const override
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:109
std::string to_formatted_string() const
Definition asn1_oid.cpp:139
static OID from_string(std::string_view str)
Definition asn1_oid.cpp:86
void random_vec(std::span< uint8_t > v)
Definition rng.h:180
constexpr T & get() &
Definition strong_type.h:50
int(* update)(CTX *, const void *, CC_LONG len)
int(* final)(unsigned char *, CTX *)
Polynomial< Trait, Domain::NTT > ntt(Polynomial< Trait, Domain::Normal > p)
Definition pqcrystals.h:557
Polynomial< Trait, Domain::Normal > inverse_ntt(Polynomial< Trait, Domain::NTT > p_ntt)
Definition pqcrystals.h:564
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
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)
Botan::CRYSTALS::PolynomialVector< DilithiumPolyTraits, Botan::CRYSTALS::Domain::NTT > DilithiumPolyVecNTT
constexpr T rho(T x)
Definition rotate.h:51
std::string fmt(std::string_view format, const T &... args)
Definition fmt.h:53
std::vector< T, secure_allocator< T > > secure_vector
Definition secmem.h:61
Botan::CRYSTALS::PolynomialMatrix< DilithiumPolyTraits > DilithiumPolyMatNTT
auto to_underlying(T e) noexcept
Definition stl_util.h:414
std::pair< std::shared_ptr< Dilithium_PublicKeyInternal >, std::shared_ptr< Dilithium_PrivateKeyInternal > > DilithiumInternalKeypair
Internal representation of a Dilithium key pair.