1use crate::SignatureError;
10use mysten_mldsa_native_rs as mldsa;
11use signature::RandomizedSigner;
12use signature::Verifier;
13use sui_sdk_types::MlDsa65PublicKey;
14use sui_sdk_types::MlDsa65Signature;
15use sui_sdk_types::SignatureScheme;
16use sui_sdk_types::SimpleSignature;
17use sui_sdk_types::UserSignature;
18
19pub struct MlDsa65PrivateKey {
24 seed: mldsa::SigningKeySeed,
25 signing_key: mldsa::SigningKey,
26 verifying_key: Box<mldsa::VerifyingKey>,
28}
29
30impl Clone for MlDsa65PrivateKey {
31 fn clone(&self) -> Self {
32 Self::new(*self.seed.as_bytes())
33 }
34}
35
36impl std::fmt::Debug for MlDsa65PrivateKey {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 f.debug_tuple("MlDsa65PrivateKey")
39 .field(&"__elided__")
40 .finish()
41 }
42}
43
44#[cfg(test)]
45impl proptest::arbitrary::Arbitrary for MlDsa65PrivateKey {
46 type Parameters = ();
47 type Strategy = proptest::strategy::BoxedStrategy<Self>;
48
49 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
50 use proptest::strategy::Strategy;
51
52 proptest::arbitrary::any::<[u8; Self::LENGTH]>()
53 .prop_map(Self::new)
54 .boxed()
55 }
56}
57
58impl MlDsa65PrivateKey {
59 pub const LENGTH: usize = mldsa::SEED_LENGTH;
61
62 pub fn new(seed: [u8; Self::LENGTH]) -> Self {
63 let seed = mldsa::SigningKeySeed::from(seed);
64 let (signing_key, verifying_key) = seed.expand();
65 Self {
66 seed,
67 signing_key,
68 verifying_key: Box::new(verifying_key),
69 }
70 }
71
72 pub fn scheme(&self) -> SignatureScheme {
73 SignatureScheme::MlDsa65
74 }
75
76 pub fn verifying_key(&self) -> MlDsa65VerifyingKey {
77 MlDsa65VerifyingKey {
78 key: self.verifying_key.clone(),
79 }
80 }
81
82 pub fn public_key(&self) -> MlDsa65PublicKey {
83 MlDsa65PublicKey::new(*self.verifying_key.as_bytes())
84 }
85
86 pub fn seed(&self) -> &[u8; Self::LENGTH] {
87 self.seed.as_bytes()
88 }
89
90 pub fn generate<R>(mut rng: R) -> Self
91 where
92 R: rand_core::RngCore + rand_core::CryptoRng,
93 {
94 let mut seed = [0; Self::LENGTH];
95 rng.fill_bytes(&mut seed);
96 Self::new(seed)
97 }
98
99 pub(crate) fn from_flagged_key_bytes(
100 scheme: SignatureScheme,
101 key: Vec<u8>,
102 ) -> Result<Self, SignatureError> {
103 if scheme != SignatureScheme::MlDsa65 {
104 return Err(SignatureError::from_source(format!(
105 "private key scheme flag is `{}`, expected `mldsa65`",
106 scheme.name(),
107 )));
108 }
109 let bytes: [u8; Self::LENGTH] = key.try_into().map_err(|_: Vec<u8>| {
110 SignatureError::from_source("private key has invalid length for mldsa65")
111 })?;
112 Ok(Self::new(bytes))
113 }
114
115 #[cfg(feature = "bech32")]
116 #[cfg_attr(doc_cfg, doc(cfg(feature = "bech32")))]
117 pub fn from_suiprivkey(s: &str) -> Result<Self, SignatureError> {
120 let (scheme, key) = crate::suipriv::decode(s)?;
121 Self::from_flagged_key_bytes(scheme, key)
122 }
123
124 #[cfg(feature = "bech32")]
125 #[cfg_attr(doc_cfg, doc(cfg(feature = "bech32")))]
126 pub fn to_suiprivkey(&self) -> Result<String, SignatureError> {
128 crate::suipriv::encode(SignatureScheme::MlDsa65, self.seed())
129 }
130
131 pub fn from_base64(s: &str) -> Result<Self, SignatureError> {
134 let (scheme, key) = crate::suipriv::decode_base64(s)?;
135 Self::from_flagged_key_bytes(scheme, key)
136 }
137
138 pub fn to_base64(&self) -> String {
141 crate::suipriv::encode_base64(SignatureScheme::MlDsa65, self.seed())
142 }
143}
144
145impl RandomizedSigner<MlDsa65Signature> for MlDsa65PrivateKey {
146 fn try_sign_with_rng(
150 &self,
151 rng: &mut impl rand_core::CryptoRngCore,
152 msg: &[u8],
153 ) -> Result<MlDsa65Signature, SignatureError> {
154 let mut rnd = [0; mldsa::RND_LENGTH];
155 rng.try_fill_bytes(&mut rnd)
156 .map_err(SignatureError::from_source)?;
157 self.signing_key
158 .sign(msg, b"", &rnd)
159 .map(|signature| MlDsa65Signature::new(*signature.as_bytes()))
160 .map_err(SignatureError::from_source)
161 }
162}
163
164impl RandomizedSigner<SimpleSignature> for MlDsa65PrivateKey {
165 fn try_sign_with_rng(
166 &self,
167 rng: &mut impl rand_core::CryptoRngCore,
168 msg: &[u8],
169 ) -> Result<SimpleSignature, SignatureError> {
170 <Self as RandomizedSigner<MlDsa65Signature>>::try_sign_with_rng(self, rng, msg).map(
171 |signature| SimpleSignature::MlDsa65 {
172 signature: Box::new(signature),
173 public_key: Box::new(self.public_key()),
174 },
175 )
176 }
177}
178
179impl RandomizedSigner<UserSignature> for MlDsa65PrivateKey {
180 fn try_sign_with_rng(
181 &self,
182 rng: &mut impl rand_core::CryptoRngCore,
183 msg: &[u8],
184 ) -> Result<UserSignature, SignatureError> {
185 <Self as RandomizedSigner<SimpleSignature>>::try_sign_with_rng(self, rng, msg)
186 .map(UserSignature::Simple)
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct MlDsa65VerifyingKey {
192 key: Box<mldsa::VerifyingKey>,
194}
195
196impl MlDsa65VerifyingKey {
197 pub fn new(public_key: &MlDsa65PublicKey) -> Result<Self, SignatureError> {
198 mldsa::VerifyingKey::from_bytes(public_key.inner())
199 .map(|key| Self { key: Box::new(key) })
200 .map_err(SignatureError::from_source)
201 }
202
203 pub fn public_key(&self) -> MlDsa65PublicKey {
204 MlDsa65PublicKey::new(*self.key.as_bytes())
205 }
206}
207
208impl Verifier<MlDsa65Signature> for MlDsa65VerifyingKey {
209 fn verify(&self, message: &[u8], signature: &MlDsa65Signature) -> Result<(), SignatureError> {
210 let signature =
211 mldsa::Signature::from_bytes(signature.inner()).map_err(SignatureError::from_source)?;
212 self.key
213 .verify(message, b"", &signature)
214 .map_err(|_| SignatureError::new())
215 }
216}
217
218impl Verifier<SimpleSignature> for MlDsa65VerifyingKey {
219 fn verify(&self, message: &[u8], signature: &SimpleSignature) -> Result<(), SignatureError> {
220 let SimpleSignature::MlDsa65 {
221 signature,
222 public_key,
223 } = signature
224 else {
225 return Err(SignatureError::from_source("not an mldsa65 signature"));
226 };
227 if public_key.inner() != self.key.as_bytes() {
228 return Err(SignatureError::from_source("public_key mismatch"));
229 }
230 <Self as Verifier<MlDsa65Signature>>::verify(self, message, signature)
231 }
232}
233
234impl Verifier<UserSignature> for MlDsa65VerifyingKey {
235 fn verify(&self, message: &[u8], signature: &UserSignature) -> Result<(), SignatureError> {
236 let UserSignature::Simple(signature) = signature else {
237 return Err(SignatureError::from_source("not a simple signature"));
238 };
239 <Self as Verifier<SimpleSignature>>::verify(self, message, signature)
240 }
241}
242
243#[cfg(test)]
244mod test {
245 use super::*;
246 use crate::SuiRandomizedSigner;
247 use crate::SuiVerifier;
248 use crate::test_util::TestRng;
249 use sui_sdk_types::PersonalMessage;
250 use test_strategy::proptest;
251
252 #[cfg(target_arch = "wasm32")]
253 use wasm_bindgen_test::wasm_bindgen_test as test;
254
255 const SUI_SIGNATURE: &str = include_str!("fixtures/mldsa65-personal-message-signature");
258 const SUI_ADDRESS: &str = "0xa44576e02f83a9e1bddac6fd742a77931d1689d9a61122eb3125dee425f6dd36";
259
260 fn sui_fixture() -> (MlDsa65Signature, MlDsa65PublicKey) {
261 let UserSignature::Simple(SimpleSignature::MlDsa65 {
262 signature,
263 public_key,
264 }) = UserSignature::from_base64(SUI_SIGNATURE.trim()).unwrap()
265 else {
266 panic!("expected an mldsa65 signature");
267 };
268 (*signature, *public_key)
269 }
270
271 struct FailingRng;
273
274 impl rand_core::RngCore for FailingRng {
275 fn next_u32(&mut self) -> u32 {
276 unimplemented!("only try_fill_bytes is expected to be called")
277 }
278
279 fn next_u64(&mut self) -> u64 {
280 unimplemented!("only try_fill_bytes is expected to be called")
281 }
282
283 fn fill_bytes(&mut self, _dst: &mut [u8]) {
284 unimplemented!("only try_fill_bytes is expected to be called")
285 }
286
287 fn try_fill_bytes(&mut self, _dst: &mut [u8]) -> Result<(), rand_core::Error> {
288 Err(rand_core::Error::new("entropy source unavailable"))
289 }
290 }
291
292 impl rand_core::CryptoRng for FailingRng {}
293
294 #[test]
297 fn verifies_sui_signature() {
298 let (sui_signature, sui_public_key) = sui_fixture();
299 let key = MlDsa65PrivateKey::new([2; 32]);
300 assert_eq!(key.public_key(), sui_public_key);
301 assert_eq!(key.public_key().derive_address().to_string(), SUI_ADDRESS);
302
303 let digest = PersonalMessage(b"hello".as_slice().into()).signing_digest();
304 key.verifying_key().verify(&digest, &sui_signature).unwrap();
305 }
306
307 #[test]
309 fn hedging_varies_signatures() {
310 let key = MlDsa65PrivateKey::new([3; 32]);
311 let digest = PersonalMessage(b"hello".as_slice().into()).signing_digest();
312 let first: MlDsa65Signature = key.try_sign_with_rng(&mut TestRng(1), &digest).unwrap();
313 let second: MlDsa65Signature = key.try_sign_with_rng(&mut TestRng(2), &digest).unwrap();
314
315 assert_ne!(first, second);
316 key.verifying_key().verify(&digest, &first).unwrap();
317 key.verifying_key().verify(&digest, &second).unwrap();
318 }
319
320 #[test]
322 fn rng_failure_is_an_error() {
323 let key = MlDsa65PrivateKey::new([3; 32]);
324 let err = RandomizedSigner::<MlDsa65Signature>::try_sign_with_rng(
325 &key,
326 &mut FailingRng,
327 b"hello",
328 )
329 .unwrap_err();
330 let source = std::error::Error::source(&err).expect("rng error is the source");
331 assert_eq!(source.to_string(), "entropy source unavailable");
332 }
333
334 #[test]
335 fn personal_message_roundtrip() {
336 let key = MlDsa65PrivateKey::new([2; 32]);
337 let message = PersonalMessage(b"hello".as_slice().into());
338 let signature = key
339 .sign_personal_message_with_rng(&mut TestRng(1), &message)
340 .unwrap();
341 assert_eq!(signature.derive_address().to_string(), SUI_ADDRESS);
342 key.verifying_key()
343 .verify_personal_message(&message, &signature)
344 .unwrap();
345 crate::simple::SimpleVerifier
346 .verify_personal_message(&message, &signature)
347 .unwrap();
348
349 let other = PersonalMessage(b"bye".as_slice().into());
350 key.verifying_key()
351 .verify_personal_message(&other, &signature)
352 .unwrap_err();
353 }
354
355 #[test]
358 fn randomized_signer_is_dyn_compatible() {
359 let message = PersonalMessage(b"hello".as_slice().into());
360 let signers: Vec<Box<dyn SuiRandomizedSigner>> = vec![
361 Box::new(MlDsa65PrivateKey::new([2; 32])),
362 Box::new(crate::simple::SimpleKeypair::from(MlDsa65PrivateKey::new(
363 [2; 32],
364 ))),
365 ];
366 let rng: &mut dyn rand_core::CryptoRngCore = &mut TestRng(1);
367 for signer in &signers {
368 let signature = signer
369 .sign_personal_message_with_rng(rng, &message)
370 .unwrap();
371 assert_eq!(signature.derive_address().to_string(), SUI_ADDRESS);
372 crate::simple::SimpleVerifier
373 .verify_personal_message(&message, &signature)
374 .unwrap();
375 }
376 }
377
378 #[test]
379 fn tampered_signature_fails() {
380 let (sui_signature, sui_public_key) = sui_fixture();
381 let digest = PersonalMessage(b"hello".as_slice().into()).signing_digest();
382 let mut bytes = *sui_signature.inner();
383 bytes[100] ^= 1;
384 MlDsa65VerifyingKey::new(&sui_public_key)
385 .unwrap()
386 .verify(&digest, &MlDsa65Signature::new(bytes))
387 .unwrap_err();
388 }
389
390 #[cfg(feature = "bech32")]
391 #[test]
392 fn suiprivkey_matches_sui_keytool() {
393 const SUIPRIVKEY: &str =
395 "suiprivkey1qupqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyghdug7";
396 let key = MlDsa65PrivateKey::from_suiprivkey(SUIPRIVKEY).unwrap();
397 assert_eq!(key.seed(), &[2; 32]);
398 assert_eq!(key.public_key().derive_address().to_string(), SUI_ADDRESS);
399 assert_eq!(key.to_suiprivkey().unwrap(), SUIPRIVKEY);
400 assert_eq!(
401 MlDsa65PrivateKey::from_base64(&key.to_base64())
402 .unwrap()
403 .seed(),
404 key.seed()
405 );
406 }
407
408 #[cfg(feature = "ed25519")]
409 #[test]
410 fn hybrid_multisig() {
411 use crate::SuiSigner;
412 use crate::multisig::MultisigAggregator;
413 use crate::multisig::UserSignatureVerifier;
414 use sui_sdk_types::MultisigCommittee;
415 use sui_sdk_types::MultisigMember;
416 use sui_sdk_types::MultisigMemberPublicKey;
417
418 let ed_key = crate::ed25519::Ed25519PrivateKey::new([1; 32]);
419 let key = MlDsa65PrivateKey::new([2; 32]);
420 let committee = MultisigCommittee::new(
422 vec![
423 MultisigMember::new(MultisigMemberPublicKey::Ed25519(ed_key.public_key()), 1),
424 MultisigMember::new(
425 MultisigMemberPublicKey::MlDsa65(Box::new(key.public_key())),
426 1,
427 ),
428 ],
429 2,
430 );
431 let message = PersonalMessage(b"hello".as_slice().into());
432
433 let mut aggregator = MultisigAggregator::new_with_message(committee, &message);
434 aggregator
435 .add_signature(ed_key.sign_personal_message(&message).unwrap())
436 .unwrap();
437 aggregator.finish().unwrap_err();
438 aggregator
439 .add_signature(
440 key.sign_personal_message_with_rng(&mut TestRng(1), &message)
441 .unwrap(),
442 )
443 .unwrap();
444 let multisig = UserSignature::Multisig(aggregator.finish().unwrap());
445
446 UserSignatureVerifier::new()
447 .verify_personal_message(&message, &multisig)
448 .unwrap();
449 let bytes = multisig.to_bytes();
450 assert_eq!(multisig, UserSignature::from_bytes(&bytes).unwrap());
451 }
452
453 #[proptest]
454 fn roundtrip(key: MlDsa65PrivateKey, message: Vec<u8>) {
455 let signature: MlDsa65Signature = key.try_sign_with_rng(&mut TestRng(7), &message).unwrap();
456 key.verifying_key().verify(&message, &signature).unwrap();
457 let recovered = MlDsa65PrivateKey::new(*key.seed());
458 assert_eq!(recovered.public_key(), key.public_key());
459 }
460}