Botan 3.13.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
64DilithiumMode::Mode dilithium_mode_from_oid(const OID& oid) {
65 if(const auto name = oid.registered_name()) {
66 return dilithium_mode_from_string(*name);
67 }
68
69 throw Invalid_Argument(fmt("OID '{}' is not registered as a Dilithium/ML-DSA mode", oid));
70}
71
72} // namespace
73
74DilithiumMode::DilithiumMode(const OID& oid) : m_mode(dilithium_mode_from_oid(oid)) {}
75
76DilithiumMode::DilithiumMode(std::string_view str) : m_mode(dilithium_mode_from_string(str)) {}
77
81
82std::string DilithiumMode::to_string() const {
83 switch(m_mode) {
85 return "Dilithium-4x4-r3";
87 return "Dilithium-4x4-AES-r3";
89 return "Dilithium-6x5-r3";
91 return "Dilithium-6x5-AES-r3";
93 return "Dilithium-8x7-r3";
95 return "Dilithium-8x7-AES-r3";
97 return "ML-DSA-4x4";
99 return "ML-DSA-6x5";
101 return "ML-DSA-8x7";
102 }
103
105}
106
108 return m_mode == Dilithium4x4_AES || m_mode == Dilithium6x5_AES || m_mode == Dilithium8x7_AES;
109}
110
112 return !is_aes();
113}
114
116 return m_mode == ML_DSA_4x4 || m_mode == ML_DSA_6x5 || m_mode == ML_DSA_8x7;
117}
118
120#if defined(BOTAN_HAS_DILITHIUM_AES)
121 if(is_dilithium_round3() && is_aes()) {
122 return true;
123 }
124#endif
125#if defined(BOTAN_HAS_DILITHIUM)
126 if(is_dilithium_round3() && is_modern()) {
127 return true;
128 }
129#endif
130#if defined(BOTAN_HAS_ML_DSA)
131 if(is_ml_dsa()) {
132 return true;
133 }
134#endif
135 return false;
136}
137
138class Dilithium_Signature_Operation final : public PK_Ops::Signature {
139 public:
140 Dilithium_Signature_Operation(DilithiumInternalKeypair keypair, bool randomized) :
141 m_keypair(std::move(keypair)),
142 m_randomized(randomized),
143 m_h(m_keypair.second->mode().symmetric_primitives().get_message_hash(m_keypair.first->tr())),
144 m_s1(ntt(m_keypair.second->s1().clone())),
145 m_s2(ntt(m_keypair.second->s2().clone())),
146 m_t0(ntt(m_keypair.second->t0().clone())),
147 m_A(Dilithium_Algos::expand_A(m_keypair.first->rho(), m_keypair.second->mode())) {}
148
149 void update(std::span<const uint8_t> input) override { m_h->update(input); }
150
151 /**
152 * NIST FIPS 204, Algorithm 2 (ML-DSA.Sign) and Algorithm 7 (ML-DSA.Sign_internal)
153 *
154 * Note that the private key decoding is done ahead of time. Also, the
155 * matrix expansion of A from 'rho' along with the NTT-transforms of s1,
156 * s2 and t0 are done in the constructor of this class, as a 'signature
157 * operation' may be used to sign multiple messages.
158 *
159 * TODO: Implement support for the specified 'ctx' context string which is
160 * application defined and "empty" by default and <= 255 bytes long.
161 */
162 std::vector<uint8_t> sign(RandomNumberGenerator& rng) override {
163 auto scope = CT::scoped_poison(*m_keypair.second);
164
165 const auto mu = m_h->final();
166 const auto& mode = m_keypair.second->mode();
167 const auto& sympri = mode.symmetric_primitives();
168
169 const auto rhoprime = sympri.H_maybe_randomized(m_keypair.second->signing_seed(), mu, maybe(rng));
170 CT::poison(rhoprime);
171
172 for(uint16_t nonce = 0, n = 0; n <= DilithiumConstants::SIGNING_LOOP_BOUND; ++n, nonce += mode.l()) {
173 const auto y = Dilithium_Algos::expand_mask(rhoprime, nonce, mode);
174
175 auto w_ntt = m_A * ntt(y.clone());
176 w_ntt.reduce();
177 auto w = inverse_ntt(std::move(w_ntt));
178 w.conditional_add_q();
179
180 auto [w1, w0] = Dilithium_Algos::decompose(w, mode);
181 const auto ch = CT::driveby_unpoison(sympri.H(mu, Dilithium_Algos::encode_commitment(w1, mode)));
182
183 const auto c = ntt(Dilithium_Algos::sample_in_ball(ch, mode));
184 const auto cs1 = inverse_ntt(c * m_s1);
185 auto z = y + cs1;
186 z.reduce();
187
188 // We validate the infinity norm of z before proceeding to calculate cs2
189 if(!Dilithium_Algos::infinity_norm_within_bound(z, to_underlying(mode.gamma1()) - mode.beta())) {
190 continue;
191 }
192 CT::unpoison(z); // part of the signature
193
194 const auto cs2 = inverse_ntt(c * m_s2);
195
196 // Note: w0 is used as a scratch space for calculation. We're aliasing
197 // the results to const&'s merely to communicate which value the
198 // intermediate results represent in the specification.
199 w0 -= cs2;
200 w0.reduce();
201 const auto& r0 = w0;
202 if(!Dilithium_Algos::infinity_norm_within_bound(r0, to_underlying(mode.gamma2()) - mode.beta())) {
203 continue;
204 }
205
206 auto ct0 = inverse_ntt(c * m_t0);
207 ct0.reduce();
208 // We validate the infinity norm of ct0 before proceeding to calculate the hint.
209 if(!Dilithium_Algos::infinity_norm_within_bound(ct0, mode.gamma2())) {
210 continue;
211 }
212
213 w0 += ct0;
214 w0.conditional_add_q();
215 const auto& w0cs2ct0 = w0;
216
217 const auto hint = Dilithium_Algos::make_hint(w0cs2ct0, w1, mode);
218 if(CT::driveby_unpoison(hint.hamming_weight()) > mode.omega()) {
219 continue;
220 }
221 CT::unpoison(hint); // part of the signature
222
223 return Dilithium_Algos::encode_signature(ch, z, hint, mode).get();
224 }
225
226 throw Internal_Error("ML-DSA/Dilithium signature loop did not terminate");
227 }
228
229 size_t signature_length() const override { return m_keypair.second->mode().signature_bytes(); }
230
231 AlgorithmIdentifier algorithm_identifier() const override {
232 return AlgorithmIdentifier(m_keypair.second->mode().mode().object_identifier(),
234 }
235
236 std::string hash_function() const override { return m_h->name(); }
237
238 private:
239 std::optional<std::reference_wrapper<RandomNumberGenerator>> maybe(RandomNumberGenerator& rng) const {
240 if(m_randomized) {
241 return rng;
242 } else {
243 return std::nullopt;
244 }
245 }
246
247 private:
248 DilithiumInternalKeypair m_keypair;
249 bool m_randomized;
250 std::unique_ptr<DilithiumMessageHash> m_h;
251
252 const DilithiumPolyVecNTT m_s1;
253 const DilithiumPolyVecNTT m_s2;
254 const DilithiumPolyVecNTT m_t0;
255 const DilithiumPolyMatNTT m_A;
256};
257
258class Dilithium_Verification_Operation final : public PK_Ops::Verification {
259 public:
260 explicit Dilithium_Verification_Operation(std::shared_ptr<const Dilithium_PublicKeyInternal> pubkey) :
261 m_pub_key(std::move(pubkey)),
262 m_A(Dilithium_Algos::expand_A(m_pub_key->rho(), m_pub_key->mode())),
263 m_t1_ntt_shifted(ntt(m_pub_key->t1() << DilithiumConstants::D)),
264 m_h(m_pub_key->mode().symmetric_primitives().get_message_hash(m_pub_key->tr())) {}
265
266 void update(std::span<const uint8_t> input) override { m_h->update(input); }
267
268 /**
269 * NIST FIPS 204, Algorithm 3 (ML-DSA.Verify) and 8 (ML-DSA.Verify_internal)
270 *
271 * Note that the public key decoding is done ahead of time. Also, the
272 * matrix A is expanded from 'rho' in the constructor of this class, as
273 * a 'verification operation' may be used to verify multiple signatures.
274 *
275 * TODO: Implement support for the specified 'ctx' context string which is
276 * application defined and "empty" by default and <= 255 bytes long.
277 */
278 bool is_valid_signature(std::span<const uint8_t> sig) override {
279 const auto& mode = m_pub_key->mode();
280 const auto& sympri = mode.symmetric_primitives();
281 const StrongSpan<const DilithiumSerializedSignature> sig_bytes(sig);
282
283 const auto mu = m_h->final();
284
285 if(sig_bytes.size() != mode.signature_bytes()) {
286 return false;
287 }
288
289 auto signature = Dilithium_Algos::decode_signature(sig_bytes, mode);
290 if(!signature.has_value()) {
291 return false;
292 }
293 auto [ch, z, h] = std::move(signature.value());
294
295 // This check was removed from the final version of ML-DSA
296 if(!mode.is_ml_dsa() && h.hamming_weight() > mode.omega()) {
297 return false;
298 }
299
300 if(!Dilithium_Algos::infinity_norm_within_bound(z, to_underlying(mode.gamma1()) - mode.beta())) {
301 return false;
302 }
303
304 const auto c_hat = ntt(Dilithium_Algos::sample_in_ball(ch, mode));
305 auto w_approx = m_A * ntt(std::move(z));
306 w_approx -= c_hat * m_t1_ntt_shifted;
307 w_approx.reduce();
308 auto w1 = inverse_ntt(std::move(w_approx));
309 w1.conditional_add_q();
310 Dilithium_Algos::use_hint(w1, h, mode);
311
312 const auto chprime = sympri.H(mu, Dilithium_Algos::encode_commitment(w1, mode));
313
314 BOTAN_ASSERT_NOMSG(ch.size() == chprime.size());
315 return std::equal(ch.begin(), ch.end(), chprime.begin());
316 }
317
318 std::string hash_function() const override { return m_h->name(); }
319
320 private:
321 std::shared_ptr<const Dilithium_PublicKeyInternal> m_pub_key;
323 DilithiumPolyVecNTT m_t1_ntt_shifted;
324 std::unique_ptr<DilithiumMessageHash> m_h;
325};
326
327Dilithium_PublicKey::Dilithium_PublicKey(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> pk) :
328 Dilithium_PublicKey(pk, DilithiumMode(alg_id.oid())) {
329 // The parameter set is identified by the OID; no parameters are defined.
330 if(!alg_id.parameters_are_empty()) {
331 throw Decoding_Error("Unexpected parameters for ML-DSA/Dilithium public key");
332 }
333}
334
336 DilithiumConstants mode(m);
337 BOTAN_ARG_CHECK(mode.mode().is_available(), "Dilithium/ML-DSA mode is not available in this build");
338 BOTAN_ARG_CHECK(pk.empty() || pk.size() == mode.public_key_bytes(),
339 "dilithium public key does not have the correct byte count");
340
342}
343
345 // Note: For Dilithium we made the blunder to return the OID's human readable
346 // name, e.g. "Dilithium-4x4-AES". This is inconsistent with the other
347 // public key algorithms which return the generic name only.
348 //
349 // TODO(Botan4): Fix the inconsistency described above, also considering that
350 // there might be other code locations that identify Dilithium
351 // by std::string::starts_with("Dilithium-").
352 // (Above assumes that Dilithium won't be removed entirely!)
353 return (m_public->mode().is_ml_dsa()) ? std::string("ML-DSA") : m_public->mode().mode().to_string();
354}
355
359
361 return m_public->mode().mode().object_identifier();
362}
363
365 return m_public->mode().canonical_parameter_set_identifier();
366}
367
369 return m_public->mode().lambda();
370}
371
372std::vector<uint8_t> Dilithium_PublicKey::raw_public_key_bits() const {
373 return m_public->raw_pk().get();
374}
375
376std::vector<uint8_t> Dilithium_PublicKey::public_key_bits() const {
377 // Currently, there isn't a finalized definition of an ASN.1 structure for
378 // Dilithium aka ML-DSA public keys. Therefore, we return the raw public key bits.
379 return raw_public_key_bits();
380}
381
382bool Dilithium_PublicKey::check_key(RandomNumberGenerator& /*rng*/, bool /*strong*/) const {
383 // The public key consists of (rho, t1). Length validation is performed in
384 // the constructor, and t1 coefficients are decoded via SimpleBitUnpack
385 // (FIPS 204 Algorithm 18) into a power-of-2 range that exactly covers all
386 // valid values, so no out-of-range coefficients are possible. For the
387 // private key, s1/s2 coefficient ranges are validated and t is recomputed
388 // from (A, s1, s2) and verified against the stored hash during decoding.
389 return true;
390}
391
392std::unique_ptr<Private_Key> Dilithium_PublicKey::generate_another(RandomNumberGenerator& rng) const {
393 return std::make_unique<Dilithium_PrivateKey>(rng, m_public->mode().mode());
394}
395
396std::unique_ptr<PK_Ops::Verification> Dilithium_PublicKey::create_verification_op(std::string_view params,
397 std::string_view provider) const {
398 BOTAN_ARG_CHECK(params.empty() || params == "Pure", "Unexpected parameters for verifying with Dilithium");
399 if(provider.empty() || provider == "base") {
400 return std::make_unique<Dilithium_Verification_Operation>(m_public);
401 }
402 throw Provider_Not_Found(algo_name(), provider);
403}
404
405std::unique_ptr<PK_Ops::Verification> Dilithium_PublicKey::create_x509_verification_op(
406 const AlgorithmIdentifier& alg_id, std::string_view provider) const {
407 if(provider.empty() || provider == "base") {
408 if(alg_id != this->algorithm_identifier()) {
409 throw Decoding_Error("Unexpected AlgorithmIdentifier for Dilithium X.509 signature");
410 }
411 return std::make_unique<Dilithium_Verification_Operation>(m_public);
412 }
413 throw Provider_Not_Found(algo_name(), provider);
414}
415
416/**
417 * NIST FIPS 204, Algorithm 1 (ML-DSA.KeyGen), and 6 (ML-DSA.KeyGen_internal)
418 *
419 * This integrates the seed generation and the actual key generation into one
420 * function. After generation, the relevant components of the key are kept in
421 * memory; the key encoding is deferred until explicitly requested.
422 *
423 * The calculation of (t1, t0) is done in a separate function, as it is also
424 * needed for the decoding of a private key.
425 */
427 DilithiumConstants mode(m);
428 BOTAN_ARG_CHECK(mode.mode().is_available(), "Dilithium/ML-DSA mode is not available in this build");
429 std::tie(m_public, m_private) = Dilithium_Algos::expand_keypair(
431}
432
433Dilithium_PrivateKey::Dilithium_PrivateKey(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> sk) :
434 Dilithium_PrivateKey(sk, DilithiumMode(alg_id.oid())) {
435 // The parameter set is identified by the OID; no parameters are defined.
436 if(!alg_id.parameters_are_empty()) {
437 throw Decoding_Error("Unexpected parameters for ML-DSA/Dilithium private key");
438 }
439}
440
442 DilithiumConstants mode(m);
443 auto& codec = mode.keypair_codec();
444 std::tie(m_public, m_private) = codec.decode_keypair(sk, std::move(mode));
445}
446
450
452 return m_private->mode().keypair_codec().encode_keypair({m_public, m_private});
453}
454
456 std::string_view params,
457 std::string_view provider) const {
458 BOTAN_UNUSED(rng);
459
460 BOTAN_ARG_CHECK(params.empty() || params == "Deterministic" || params == "Randomized",
461 "Unexpected parameters for signing with ML-DSA/Dilithium");
462
463 // FIPS 204, Section 3.4
464 // By default, this standard specifies the signing algorithm to use both
465 // types of randomness [fresh from the RNG and a value in the private key].
466 // This is referred to as the “hedged” variant of the signing procedure.
467 const bool randomized = (params.empty() || params == "Randomized");
468 if(provider.empty() || provider == "base") {
469 return std::make_unique<Dilithium_Signature_Operation>(DilithiumInternalKeypair{m_public, m_private}, randomized);
470 }
471 throw Provider_Not_Found(algo_name(), provider);
472}
473
475 if(!Dilithium_PublicKey::check_key(rng, strong)) {
476 return false;
477 }
478
479 if(strong) {
480 return KeyPair::signature_consistency_check(rng, *this, "");
481 }
482
483 return true;
484}
485
486std::unique_ptr<Public_Key> Dilithium_PrivateKey::public_key() const {
487 return std::make_unique<Dilithium_PublicKey>(*this);
488}
489} // 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:166
bool parameters_are_empty() const
Definition asn1_obj.h:715
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:78
std::string to_string() const
Definition dilithium.cpp:82
DilithiumMode(Mode mode)
Definition dilithium.h:37
bool is_aes() const
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::shared_ptr< const Dilithium_PublicKeyInternal > m_public
Definition dilithium.h:106
std::vector< uint8_t > raw_public_key_bits() const override
bool check_key(RandomNumberGenerator &rng, bool strong) const override
static OID from_string(std::string_view str)
Definition asn1_oid.cpp:80
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:244
constexpr T & get() &
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::vector< T, secure_allocator< T > > secure_vector
Definition secmem.h:128
Botan::CRYSTALS::PolynomialMatrix< DilithiumPolyTraits > DilithiumPolyMatNTT
std::pair< std::shared_ptr< const Dilithium_PublicKeyInternal >, std::shared_ptr< const Dilithium_PrivateKeyInternal > > DilithiumInternalKeypair
Internal representation of a Dilithium key pair.