• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

randombit / botan / 22172928579

19 Feb 2026 07:42AM UTC coverage: 90.338% (-1.3%) from 91.672%
22172928579

Pull #5307

github

web-flow
Merge 6ff0be01a into 59d4d64fb
Pull Request #5307: Fix ML-DSA private key encoding

102778 of 113770 relevant lines covered (90.34%)

11436124.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.53
/src/tests/test_dilithium.cpp
1
/*
2
 * Tests for Crystals Dilithium
3
 * - KAT tests using the KAT vectors from
4
 *   https://csrc.nist.gov/CSRC/media/Projects/post-quantum-cryptography/documents/round-3/submissions/Dilithium-Round3.zip
5
 *
6
 * (C) 2022,2023 Jack Lloyd
7
 * (C) 2022 Manuel Glaser, Michael Boric, René Meusel - Rohde & Schwarz Cybersecurity
8
 *
9
 * Botan is released under the Simplified BSD License (see license.txt)
10
 */
11

12
#include "test_rng.h"
13
#include "tests.h"
14

15
#include <memory>
16
#include <vector>
17

18
#if defined(BOTAN_HAS_DILITHIUM_COMMON)
19
   #include <botan/dilithium.h>
20
   #include <botan/hash.h>
21
   #include <botan/pk_algs.h>
22
   #include <botan/pubkey.h>
23
   #include <botan/pk_keys.h>
24

25
   #include "test_pubkey.h"
26
#endif
27

28
namespace Botan_Tests {
29

30
#if defined(BOTAN_HAS_DILITHIUM_COMMON) && defined(BOTAN_HAS_AES) && defined(BOTAN_HAS_SHA3)
31

32
template <typename DerivedT>
33
class Dilithium_KAT_Tests : public Text_Based_Test {
×
34
   public:
35
      // NOLINTNEXTLINE(*crtp-constructor-accessibility)
36
      Dilithium_KAT_Tests() : Text_Based_Test(DerivedT::test_vector, "Seed,Msg,HashPk,HashSk,HashSig", "Sig") {}
36✔
37

38
      Test::Result run_one_test(const std::string& name, const VarMap& vars) override {
1,350✔
39
         Test::Result result(name);
1,350✔
40

41
         // read input from test file
42
         const auto ref_seed = vars.get_req_bin("Seed");
1,350✔
43
         const auto ref_msg = vars.get_req_bin("Msg");
1,350✔
44
         const auto ref_pk_hash = vars.get_req_bin("HashPk");
1,350✔
45
         const auto ref_sk_hash = vars.get_req_bin("HashSk");
1,350✔
46
         const auto ref_sig_hash = vars.get_req_bin("HashSig");
1,350✔
47
         const auto ref_sig = vars.get_opt_bin("Sig");
1,350✔
48

49
         auto sha3_256 = Botan::HashFunction::create_or_throw("SHA-3(256)");
1,350✔
50

51
         auto dilithium_test_rng = std::make_unique<CTR_DRBG_AES256>(ref_seed);
1,350✔
52

53
         const Botan::Dilithium_PrivateKey priv_key(*dilithium_test_rng, DerivedT::mode);
1,350✔
54
         
55
         if(!priv_key.is_mldsa()) {
1,350✔
56
            result.test_bin_eq(
3,600✔
57
               "generated expected private key hash", sha3_256->process(priv_key.private_key_bits()), ref_sk_hash);
1,200✔
58
         }
59

60
         result.test_bin_eq(
2,700✔
61
            "generated expected public key hash", sha3_256->process(priv_key.public_key_bits()), ref_pk_hash);
1,350✔
62

63
         auto signer = Botan::PK_Signer(priv_key, *dilithium_test_rng, DerivedT::sign_param);
1,350✔
64
         auto signature = signer.sign_message(ref_msg.data(), ref_msg.size(), *dilithium_test_rng);
2,700✔
65

66
         result.test_bin_eq("generated expected signature hash", sha3_256->process(signature), ref_sig_hash);
2,700✔
67
         if(!ref_sig.empty()) {
1,350✔
68
            result.test_bin_eq("generated expected signature", signature, ref_sig);
12✔
69
         }
70

71
         const Botan::Dilithium_PublicKey pub_key(priv_key.public_key_bits(), DerivedT::mode);
1,350✔
72
         auto verifier = Botan::PK_Verifier(pub_key, "");
1,350✔
73
         verifier.update(ref_msg.data(), ref_msg.size());
1,350✔
74
         result.test_is_true("signature verifies", verifier.check_signature(signature.data(), signature.size()));
1,350✔
75

76
         // test validating incorrect wrong signature
77
         auto mutated_signature = Test::mutate_vec(signature, this->rng());
1,350✔
78
         result.test_is_true("invalid signature rejected",
1,350✔
79
                             !verifier.check_signature(mutated_signature.data(), mutated_signature.size()));
1,350✔
80

81
         verifier.update(ref_msg.data(), ref_msg.size());
1,350✔
82
         result.test_is_true("signature verifies", verifier.check_signature(signature.data(), signature.size()));
1,350✔
83

84
         return result;
1,350✔
85
      }
4,050✔
86
};
87

88
   // NOLINTNEXTLINE(*-macro-usage)
89
   #define REGISTER_DILITHIUM_KAT_TEST(m, rand)                                          \
90
      class DILITHIUM##m##rand final : public Dilithium_KAT_Tests<DILITHIUM##m##rand> {  \
91
         public:                                                                         \
92
            constexpr static auto test_vector = "pubkey/dilithium_" #m "_" #rand ".vec"; \
93
            constexpr static auto mode = Botan::DilithiumMode::Dilithium##m;             \
94
            constexpr static auto sign_param = #rand;                                    \
95
      };                                                                                 \
96
      BOTAN_REGISTER_TEST("pubkey", "dilithium_kat_" #m "_" #rand, DILITHIUM##m##rand)
97

98
   // NOLINTNEXTLINE(*-macro-usage)
99
   #define REGISTER_ML_DSA_KAT_TEST(m, rand)                                          \
100
      class ML_DSA##m##rand final : public Dilithium_KAT_Tests<ML_DSA##m##rand> {     \
101
         public:                                                                      \
102
            constexpr static auto test_vector = "pubkey/ml-dsa-" #m "_" #rand ".vec"; \
103
            constexpr static auto mode = Botan::DilithiumMode::ML_DSA_##m;            \
104
            constexpr static auto sign_param = #rand;                                 \
105
      };                                                                              \
106
      BOTAN_REGISTER_TEST("pubkey", "ml-dsa_kat_" #m "_" #rand, ML_DSA##m##rand)
107

108
   #if defined(BOTAN_HAS_DILITHIUM)
109
REGISTER_DILITHIUM_KAT_TEST(4x4, Deterministic);
1✔
110
REGISTER_DILITHIUM_KAT_TEST(6x5, Deterministic);
1✔
111
REGISTER_DILITHIUM_KAT_TEST(8x7, Deterministic);
1✔
112
REGISTER_DILITHIUM_KAT_TEST(4x4, Randomized);
1✔
113
REGISTER_DILITHIUM_KAT_TEST(6x5, Randomized);
1✔
114
REGISTER_DILITHIUM_KAT_TEST(8x7, Randomized);
1✔
115
   #endif
116

117
   #if defined(BOTAN_HAS_DILITHIUM_AES)
118
REGISTER_DILITHIUM_KAT_TEST(4x4_AES, Deterministic);
1✔
119
REGISTER_DILITHIUM_KAT_TEST(6x5_AES, Deterministic);
1✔
120
REGISTER_DILITHIUM_KAT_TEST(8x7_AES, Deterministic);
1✔
121
REGISTER_DILITHIUM_KAT_TEST(4x4_AES, Randomized);
1✔
122
REGISTER_DILITHIUM_KAT_TEST(6x5_AES, Randomized);
1✔
123
REGISTER_DILITHIUM_KAT_TEST(8x7_AES, Randomized);
1✔
124
   #endif
125

126
   #if defined(BOTAN_HAS_ML_DSA)
127
REGISTER_ML_DSA_KAT_TEST(4x4, Deterministic);
1✔
128
REGISTER_ML_DSA_KAT_TEST(6x5, Deterministic);
1✔
129
REGISTER_ML_DSA_KAT_TEST(8x7, Deterministic);
1✔
130
REGISTER_ML_DSA_KAT_TEST(4x4, Randomized);
1✔
131
REGISTER_ML_DSA_KAT_TEST(6x5, Randomized);
1✔
132
REGISTER_ML_DSA_KAT_TEST(8x7, Randomized);
1✔
133
   #endif
134

135
class DilithiumRoundtripTests final : public Test {
1✔
136
   public:
137
      static Test::Result run_roundtrip(
18✔
138
         const char* test_name, Botan::DilithiumMode mode, bool randomized, size_t strength, size_t psid) {
139
         Test::Result result(test_name);
18✔
140
         if(!mode.is_available()) {
18✔
141
            result.note_missing(mode.to_string());
×
142
            return result;
×
143
         }
144

145
         auto rng = Test::new_rng(test_name);
18✔
146

147
         auto sign = [randomized, &rng](const auto& private_key, const auto& msg) {
72✔
148
            const std::string param = (randomized) ? "Randomized" : "Deterministic";
81✔
149
            auto signer = Botan::PK_Signer(private_key, *rng, param);
54✔
150
            return signer.sign_message(msg, *rng);
54✔
151
         };
72✔
152

153
         auto verify = [](const auto& public_key, const auto& msg, const auto& signature) {
288✔
154
            auto verifier = Botan::PK_Verifier(public_key, "");
270✔
155
            verifier.update(msg);
270✔
156
            return verifier.check_signature(signature);
540✔
157
         };
270✔
158

159
         const std::string msg = "The quick brown fox jumps over the lazy dog.";
18✔
160
         const std::vector<uint8_t> msgvec(msg.data(), msg.data() + msg.size());
18✔
161

162
         const Botan::Dilithium_PrivateKey priv_key(*rng, mode);
18✔
163
         const Botan::Dilithium_PublicKey& pub_key = priv_key;
18✔
164

165
         result.test_sz_eq("key strength", priv_key.estimated_strength(), strength);
18✔
166
         result.test_sz_eq("key length", priv_key.key_length(), psid);
18✔
167
         result.test_sz_eq("key strength", pub_key.estimated_strength(), strength);
18✔
168
         result.test_sz_eq("key length", pub_key.key_length(), psid);
18✔
169

170
         const auto sig_before_codec = sign(priv_key, msgvec);
18✔
171

172
         const auto priv_key_encoded = priv_key.private_key_bits();
18✔
173
         const auto pub_key_encoded = priv_key.public_key_bits();
18✔
174

175
         const Botan::Dilithium_PrivateKey priv_key_decoded(priv_key_encoded, mode);
18✔
176
         const Botan::Dilithium_PublicKey pub_key_decoded(pub_key_encoded, mode);
18✔
177

178
         const auto sig_after_codec = sign(priv_key_decoded, msgvec);
18✔
179

180
         result.test_is_true("Pubkey: before,   Sig: before", verify(pub_key, msgvec, sig_before_codec));
18✔
181
         result.test_is_true("Pubkey: before,   Sig: after", verify(pub_key, msgvec, sig_after_codec));
18✔
182
         result.test_is_true("Pubkey: after,    Sig: after", verify(pub_key_decoded, msgvec, sig_after_codec));
18✔
183
         result.test_is_true("Pubkey: after,    Sig: before", verify(pub_key_decoded, msgvec, sig_before_codec));
18✔
184
         result.test_is_true("Pubkey: recalc'ed Sig: before", verify(priv_key_decoded, msgvec, sig_before_codec));
18✔
185
         result.test_is_true("Pubkey: recalc'ed Sig: after", verify(priv_key_decoded, msgvec, sig_after_codec));
18✔
186

187
         auto tampered_msgvec = msgvec;
18✔
188
         tampered_msgvec.front() = 'X';
18✔
189
         result.test_is_true("Pubkey: before,   Broken Sig: before",
54✔
190
                             !verify(pub_key, tampered_msgvec, sig_before_codec));
18✔
191
         result.test_is_true("Pubkey: before,   Broken Sig: after", !verify(pub_key, tampered_msgvec, sig_after_codec));
18✔
192
         result.test_is_true("Pubkey: after,    Broken Sig: after",
54✔
193
                             !verify(pub_key_decoded, tampered_msgvec, sig_after_codec));
18✔
194
         result.test_is_true("Pubkey: after,    Broken Sig: before",
54✔
195
                             !verify(pub_key_decoded, tampered_msgvec, sig_before_codec));
18✔
196
         result.test_is_true("Pubkey: recalc'ed Sig: before",
54✔
197
                             !verify(priv_key_decoded, tampered_msgvec, sig_before_codec));
18✔
198
         result.test_is_true("Pubkey: recalc'ed Sig: after",
54✔
199
                             !verify(priv_key_decoded, tampered_msgvec, sig_after_codec));
18✔
200

201
         // decoding via generic pk_algs.h
202
         const auto generic_pubkey_decoded = Botan::load_public_key(pub_key.algorithm_identifier(), pub_key_encoded);
18✔
203
         const auto generic_privkey_decoded =
18✔
204
            Botan::load_private_key(priv_key.algorithm_identifier(), priv_key_encoded);
18✔
205

206
         result.test_not_null("generic pubkey", generic_pubkey_decoded);
18✔
207
         result.test_not_null("generic privkey", generic_privkey_decoded);
18✔
208

209
         const auto sig_after_generic_codec = sign(*generic_privkey_decoded, msgvec);
18✔
210

211
         result.test_is_true("verification with generic public key",
18✔
212
                             verify(*generic_pubkey_decoded, msgvec, sig_before_codec));
18✔
213
         result.test_is_true("verification of signature with generic private key",
18✔
214
                             verify(*generic_pubkey_decoded, msgvec, sig_after_generic_codec));
18✔
215
         result.test_is_true("verification with generic private key",
18✔
216
                             verify(*generic_privkey_decoded, msgvec, sig_before_codec));
18✔
217

218
         return result;
18✔
219
      }
90✔
220

221
      std::vector<Test::Result> run() override {
1✔
222
         return {
1✔
223
            run_roundtrip("Dilithium_4x4_Common", Botan::DilithiumMode::Dilithium4x4, false, 128, 44),
224
            run_roundtrip("Dilithium_6x5_Common", Botan::DilithiumMode::Dilithium6x5, false, 192, 65),
225
            run_roundtrip("Dilithium_8x7_Common", Botan::DilithiumMode::Dilithium8x7, false, 256, 87),
226
            run_roundtrip("Dilithium_4x4_Common_Randomized", Botan::DilithiumMode::Dilithium4x4, true, 128, 44),
227
            run_roundtrip("Dilithium_6x5_Common_Randomized", Botan::DilithiumMode::Dilithium6x5, true, 192, 65),
228
            run_roundtrip("Dilithium_8x7_Common_Randomized", Botan::DilithiumMode::Dilithium8x7, true, 256, 87),
229
            run_roundtrip("Dilithium_4x4_AES", Botan::DilithiumMode::Dilithium4x4_AES, false, 128, 44),
230
            run_roundtrip("Dilithium_6x5_AES", Botan::DilithiumMode::Dilithium6x5_AES, false, 192, 65),
231
            run_roundtrip("Dilithium_8x7_AES", Botan::DilithiumMode::Dilithium8x7_AES, false, 256, 87),
232
            run_roundtrip("Dilithium_4x4_AES_Randomized", Botan::DilithiumMode::Dilithium4x4_AES, true, 128, 44),
233
            run_roundtrip("Dilithium_6x5_AES_Randomized", Botan::DilithiumMode::Dilithium6x5_AES, true, 192, 65),
234
            run_roundtrip("Dilithium_8x7_AES_Randomized", Botan::DilithiumMode::Dilithium8x7_AES, true, 256, 87),
235
            run_roundtrip("ML-DSA_4x4", Botan::DilithiumMode::ML_DSA_4x4, false, 128, 44),
236
            run_roundtrip("ML-DSA_6x5", Botan::DilithiumMode::ML_DSA_6x5, false, 192, 65),
237
            run_roundtrip("ML-DSA_8x7", Botan::DilithiumMode::ML_DSA_8x7, false, 256, 87),
238
            run_roundtrip("ML-DSA_4x4_Randomized", Botan::DilithiumMode::ML_DSA_4x4, true, 128, 44),
239
            run_roundtrip("ML-DSA_6x5_Randomized", Botan::DilithiumMode::ML_DSA_6x5, true, 192, 65),
240
            run_roundtrip("ML-DSA_8x7_Randomized", Botan::DilithiumMode::ML_DSA_8x7, true, 256, 87),
241
         };
19✔
242
      }
2✔
243
};
244

245
BOTAN_REGISTER_TEST("pubkey", "dilithium_roundtrips", DilithiumRoundtripTests);
246

247
class Dilithium_Keygen_Tests final : public PK_Key_Generation_Test {
1✔
248
   public:
249
      std::vector<std::string> keygen_params() const override {
1✔
250
         const std::vector<std::string> all_instances = {
1✔
251
            "Dilithium-4x4-AES-r3",
252
            "Dilithium-6x5-AES-r3",
253
            "Dilithium-8x7-AES-r3",
254
            "Dilithium-4x4-r3",
255
            "Dilithium-6x5-r3",
256
            "Dilithium-8x7-r3",
257
            "ML-DSA-4x4",
258
            "ML-DSA-6x5",
259
            "ML-DSA-8x7",
260
         };
1✔
261

262
         std::vector<std::string> available_instances;
1✔
263

264
         for(const auto& mode : all_instances) {
10✔
265
            if(Botan::DilithiumMode(mode).is_available()) {
9✔
266
               available_instances.push_back(mode);
9✔
267
            }
268
         }
269
         return available_instances;
1✔
270
      }
1✔
271

272
      std::string algo_name(std::string_view param) const override {
9✔
273
         if(param.starts_with("Dilithium-")) {
9✔
274
            return "Dilithium";
6✔
275
         } else {
276
            return "ML-DSA";
3✔
277
         }
278
      }
279

280
      std::string algo_name() const override { throw Test_Error("No default algo name set for Dilithium"); }
×
281

282
      std::unique_ptr<Botan::Public_Key> public_key_from_raw(std::string_view keygen_params,
9✔
283
                                                             std::string_view /* provider */,
284
                                                             std::span<const uint8_t> raw_pk) const override {
285
         return std::make_unique<Botan::Dilithium_PublicKey>(raw_pk, Botan::DilithiumMode(keygen_params));
9✔
286
      }
287
};
288

289
BOTAN_REGISTER_TEST("pubkey", "dilithium_keygen", Dilithium_Keygen_Tests);
290

291
#endif
292

293
#if defined(BOTAN_HAS_DILITHIUM_COMMON) && defined(BOTAN_HAS_SHA3)
294
class MLDSA_Privkey_Tests : public Text_Based_Test {
295
   public:
296
      MLDSA_Privkey_Tests() : Text_Based_Test("mldsa_privkey.vec", "key") {}
2✔
297

298
      Test::Result run_one_test(const std::string& name, const VarMap& vars) override {
12✔
299
         Test::Result result(name);
12✔
300
         const std::vector<uint8_t> key_bits = vars.get_req_bin("key");
12✔
301
         bool expect_decoding_failure = false;
12✔
302
         Botan::DilithiumMode mode = Botan::DilithiumMode::ML_DSA_4x4;
12✔
303
         if(name.starts_with("mldsa-44")) {
12✔
304
            mode = Botan::DilithiumMode::ML_DSA_4x4;
7✔
305
         } else if(name.starts_with("mldsa-65")) {
5✔
306
            mode = Botan::DilithiumMode::ML_DSA_6x5;
3✔
307
         } else if(name.starts_with("mldsa-87")) {
2✔
308
            mode = Botan::DilithiumMode::ML_DSA_8x7;
2✔
309
         } else {
310
            throw Botan_Tests::Test_Error(
×
311
               "internal error for ML-DSA test vector: encountered unknown ml-dsa mode string (test-case-specific token)");
×
312
         }
313
         if(name.ends_with("-invalid")) {
12✔
314
            expect_decoding_failure = true;
315
         }
316
         std::unique_ptr<Botan::Dilithium_PrivateKey> priv_key;
12✔
317
         try {
12✔
318
            priv_key = std::make_unique<Botan::Dilithium_PrivateKey>(key_bits, mode);
20✔
319
         } catch(const Botan::Decoding_Error& e) {
4✔
320
            result.test_is_true("invalid ML-DSA key rejected", expect_decoding_failure);
4✔
321
            return result;
4✔
322
         }
4✔
323
         std::vector<uint8_t> ref_msg = {0, 1, 2, 4};
8✔
324
         std::vector<uint8_t> rng_seed(48);
8✔
325
         Botan_Tests::CTR_DRBG_AES256 rng(rng_seed);
8✔
326
         auto signer = Botan::PK_Signer(*priv_key, rng, "Randomized");
8✔
327
         auto signature = signer.sign_message(ref_msg.data(), ref_msg.size(), rng);
8✔
328

329
         const Botan::Dilithium_PublicKey pub_key(priv_key->public_key_bits(), mode);
8✔
330
         auto verifier = Botan::PK_Verifier(pub_key, "");
8✔
331
         verifier.update(ref_msg.data(), ref_msg.size());
8✔
332
         result.test_is_true("signature verifies", verifier.check_signature(signature.data(), signature.size()));
8✔
333

334
         auto reencoded_priv_key = priv_key->private_key_bits();
8✔
335
         auto redecoded_priv_key = Botan::Dilithium_PrivateKey(reencoded_priv_key, mode);
8✔
336
         result.test_is_true("re-encoding and subsequent re-decoding of private ML-DSA key without error", true);
8✔
337
         return result;
8✔
338
      }
20✔
339
};
340

341
BOTAN_REGISTER_TEST("pubkey", "mldsa_private_key", MLDSA_Privkey_Tests);
342

343
#endif
344

345
}  // namespace Botan_Tests
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc