1use crate::{
2 ExtendedPrivateKey, ExtendedPublicKey, KeygenId, KeygenPrivateKey, PublicKey, RoomUUID, Scheme, SecretShare, SodotError,
3 ffi::{self, AsRawFFI, ConstSlice, ConstStr, FromFFI, HeapString, Room, ToFFI, async_ffi_double, async_ffi_single},
4};
5use std::num::NonZeroU16;
6
7pub type EcdsaSecretShare = SecretShare<Ecdsa>;
9
10pub const UNCOMPRESSED_PUB_KEY_SIZE: usize = 65;
12
13pub type EcdsaPublicKey = PublicKey<Ecdsa, UNCOMPRESSED_PUB_KEY_SIZE>;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct EcdsaSignature {
28 pub(crate) expanded_sig: Vec<u8>,
29}
30
31pub type EcdsaExtendedPublicKey = ExtendedPublicKey<Ecdsa>;
33
34pub type EcdsaExtendedPrivateKey = ExtendedPrivateKey<Ecdsa>;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct MessageHash(pub(crate) [u8; 32]);
42
43impl MessageHash {
44 pub fn from_hash(hash: [u8; 32]) -> Self {
46 Self(hash)
47 }
48
49 pub fn as_bytes(&self) -> &[u8; 32] {
51 &self.0
52 }
53
54 pub fn into_bytes(self) -> [u8; 32] {
56 self.0
57 }
58
59 pub fn sha256(msg: &[u8]) -> Self {
61 Self::hash(msg, ffi::sodot_sha256)
62 }
63
64 pub fn keccak256(msg: &[u8]) -> Self {
66 Self::hash(msg, ffi::sodot_keccak256)
67 }
68
69 fn hash(msg: &[u8], f: unsafe extern "C" fn(*const u8, usize, *mut [u8; 32])) -> Self {
70 let ptr = if msg.is_empty() { std::ptr::null() } else { msg.as_ptr() };
71 let mut hash32 = [0u8; 32];
72
73 unsafe {
75 f(ptr, msg.len(), &mut hash32);
76 }
77 Self(hash32)
78 }
79}
80
81impl EcdsaPublicKey {
82 pub const COMPRESSED_SIZE: usize = 33;
84
85 pub fn uncompressed(&self) -> [u8; UNCOMPRESSED_PUB_KEY_SIZE] {
87 self.0
88 }
89
90 pub fn compressed(&self) -> [u8; Self::COMPRESSED_SIZE] {
92 let mut compressed = [0u8; Self::COMPRESSED_SIZE];
93 compressed[1..Self::COMPRESSED_SIZE].copy_from_slice(&self.0[1..Self::COMPRESSED_SIZE]);
94 let is_odd = self.0[UNCOMPRESSED_PUB_KEY_SIZE - 1] & 1;
95 compressed[0] = 0x02 + is_odd;
96 compressed
97 }
98}
99
100impl EcdsaSignature {
101 pub(crate) const FIELD_SIZE: usize = 32;
103 pub(crate) const MIN_SIZE: usize = 2 * Self::FIELD_SIZE + 8 + 1;
105 pub(crate) const MAX_SIZE: usize = 2 * Self::FIELD_SIZE + 72 + 1;
107
108 pub fn r(&self) -> [u8; 32] {
110 self.expanded_sig[..Self::FIELD_SIZE].try_into().unwrap()
111 }
112
113 pub fn s(&self) -> [u8; 32] {
115 self.expanded_sig[Self::FIELD_SIZE..Self::FIELD_SIZE * 2].try_into().unwrap()
116 }
117
118 pub fn v(&self) -> u8 {
120 self.expanded_sig[Self::FIELD_SIZE * 2]
121 }
122
123 pub fn der(&self) -> &[u8] {
125 &self.expanded_sig[Self::FIELD_SIZE * 2 + 1..]
126 }
127
128 pub fn compact(&self) -> [u8; 64] {
131 self.expanded_sig[..Self::FIELD_SIZE * 2].try_into().unwrap()
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct Ecdsa {
145 host_url: String,
146}
147
148impl Scheme for Ecdsa {
149 fn new(host_url: String) -> Self {
150 Self { host_url }
151 }
152
153 fn host_url(&self) -> &str {
154 &self.host_url
155 }
156}
157
158impl Ecdsa {
159 pub fn new(host_url: String) -> Self {
161 <Self as Scheme>::new(host_url)
162 }
163
164 pub fn with_default_host() -> Self {
166 <Self as Scheme>::with_default_host()
167 }
168
169 pub fn init_keygen(&self) -> Result<(KeygenId, KeygenPrivateKey), SodotError> {
171 <Self as Scheme>::init_keygen(self)
172 }
173
174 pub async fn create_room(&self, num_parties: NonZeroU16, api_key: &str) -> Result<RoomUUID, SodotError> {
176 <Self as Scheme>::create_room(self, num_parties, api_key).await
177 }
178
179 pub async fn keygen(
185 &self,
186 room_uuid: &RoomUUID,
187 num_parties: NonZeroU16,
188 threshold: NonZeroU16,
189 keygen_init_keypair: &KeygenPrivateKey,
190 keygen_ids: &[KeygenId],
191 ) -> Result<(EcdsaPublicKey, EcdsaSecretShare), SodotError> {
192 let room = Room::new(&self.host_url, room_uuid);
193 let keygen_init_keypair = keygen_init_keypair.ffi();
194
195 let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
196 let keygen_slice = ConstSlice::new(&keygen_ids);
197
198 async_ffi_double(|cb| unsafe {
199 ffi::sodot_ecdsa_keygen(
200 room.raw_ffi(),
201 num_parties.get(),
202 threshold.get(),
203 keygen_init_keypair.raw_ffi(),
204 keygen_slice.raw_ffi(),
205 cb.callback(),
206 cb.extra(),
207 )
208 })
209 .await
210 }
211
212 pub async fn sign(
218 &self,
219 room_uuid: &RoomUUID,
220 secret_share: &EcdsaSecretShare,
221 message_hash: &MessageHash,
222 derivation_path: &[u32],
223 ) -> Result<EcdsaSignature, SodotError> {
224 let room = Room::new(&self.host_url, room_uuid);
225 let secret_share = secret_share.ffi();
226 let message_hash = message_hash.ffi();
227
228 let derivation_path = ConstSlice::new(derivation_path);
229
230 async_ffi_single(|cb| unsafe {
231 ffi::sodot_ecdsa_sign(
232 room.raw_ffi(),
233 secret_share.raw_ffi(),
234 message_hash.raw_ffi(),
235 derivation_path.raw_ffi(),
236 cb.callback(),
237 cb.extra(),
238 )
239 })
240 .await
241 }
242
243 pub fn derive_pubkey(&self, secret_share: &EcdsaSecretShare, derivation_path: &[u32]) -> Result<EcdsaPublicKey, SodotError> {
248 let secret_share = secret_share.ffi();
249 let derivation_path = ConstSlice::new(derivation_path);
250 let mut pubkey_out = HeapString::NULL;
251 let result =
252 unsafe { ffi::sodot_ecdsa_derive_pubkey(secret_share.raw_ffi(), derivation_path.raw_ffi(), pubkey_out.ffi_mut()) };
253 result.ok_and(|| FromFFI::parse_ffi(pubkey_out.as_str()))
254 }
255
256 pub fn get_xpub(&self, secret_share: &EcdsaSecretShare) -> Result<EcdsaExtendedPublicKey, SodotError> {
261 let secret_share_cstr = secret_share.ffi();
262 let mut xpub_out = HeapString::NULL;
263
264 let result = unsafe { ffi::sodot_ecdsa_get_xpub(secret_share_cstr.raw_ffi(), xpub_out.ffi_mut()) };
265 result.ok_and(|| FromFFI::parse_ffi(xpub_out.as_str()))
266 }
267
268 pub fn derive_pubkey_from_xpub(
273 &self,
274 xpub: &EcdsaExtendedPublicKey,
275 derivation_path: &[u32],
276 ) -> Result<EcdsaPublicKey, SodotError> {
277 let xpub = xpub.ffi();
278 let derivation_path = ConstSlice::new(derivation_path);
279 let mut pubkey_out = HeapString::NULL;
280
281 let result =
282 unsafe { ffi::sodot_ecdsa_derive_pubkey_from_xpub(xpub.raw_ffi(), derivation_path.raw_ffi(), pubkey_out.ffi_mut()) };
283 result.ok_and(|| FromFFI::parse_ffi(pubkey_out.as_str()))
284 }
285
286 pub fn derive_private_key_from_xpriv(
291 &self,
292 xpriv: &EcdsaExtendedPrivateKey,
293 derivation_path: &[u32],
294 ) -> Result<[u8; 32], SodotError> {
295 let xpriv = xpriv.ffi();
296 let derivation_path = ConstSlice::new(derivation_path);
297 let mut privkey_out = HeapString::NULL;
298
299 let result = unsafe {
300 ffi::sodot_ecdsa_derive_private_key_from_xpriv(xpriv.raw_ffi(), derivation_path.raw_ffi(), privkey_out.ffi_mut())
301 };
302 result.ok_and(|| FromFFI::parse_ffi(privkey_out.as_str()))
303 }
304
305 pub async fn refresh(
313 &self,
314 room_uuid: &RoomUUID,
315 secret_share: &EcdsaSecretShare,
316 ) -> Result<(EcdsaPublicKey, EcdsaSecretShare), SodotError> {
317 let room = Room::new(&self.host_url, room_uuid);
318 let secret_share = secret_share.ffi();
319 async_ffi_double(|cb| unsafe { ffi::sodot_ecdsa_refresh(room.raw_ffi(), secret_share.raw_ffi(), cb.callback(), cb.extra()) })
320 .await
321 }
322
323 pub async fn reshare_new_party(
331 &self,
332 room_uuid: &RoomUUID,
333 new_threshold: NonZeroU16,
334 keygen_private_key: &KeygenPrivateKey,
335 keygen_ids: &[KeygenId],
336 ) -> Result<(EcdsaPublicKey, EcdsaSecretShare), SodotError> {
337 let room = Room::new(&self.host_url, room_uuid);
338 let keygen_private_key = keygen_private_key.ffi();
339
340 let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
341 let keygen_slice = ConstSlice::new(&keygen_ids);
342
343 async_ffi_double(|cb| unsafe {
344 ffi::sodot_ecdsa_reshare_new_party(
345 room.raw_ffi(),
346 new_threshold.get(),
347 keygen_private_key.raw_ffi(),
348 keygen_slice.raw_ffi(),
349 cb.callback(),
350 cb.extra(),
351 )
352 })
353 .await
354 }
355
356 pub async fn reshare_remaining_party(
364 &self,
365 room_uuid: &RoomUUID,
366 new_threshold: NonZeroU16,
367 secret_share: &EcdsaSecretShare,
368 keygen_ids: &[KeygenId],
369 ) -> Result<(EcdsaPublicKey, EcdsaSecretShare), SodotError> {
370 let room = Room::new(&self.host_url, room_uuid);
371 let secret_share = secret_share.ffi();
372
373 let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
374 let keygen_slice = ConstSlice::new(&keygen_ids);
375
376 async_ffi_double(|cb| unsafe {
377 ffi::sodot_ecdsa_reshare_remaining_party(
378 room.raw_ffi(),
379 new_threshold.get(),
380 secret_share.raw_ffi(),
381 keygen_slice.raw_ffi(),
382 cb.callback(),
383 cb.extra(),
384 )
385 })
386 .await
387 }
388
389 pub fn export_id(&self, secret_share: &EcdsaSecretShare) -> Result<KeygenId, SodotError> {
394 let secret_share_str = secret_share.ffi();
395 let mut keygen_id_out = HeapString::NULL;
396 let result = unsafe { ffi::sodot_ecdsa_get_export_id(secret_share_str.raw_ffi(), keygen_id_out.ffi_mut()) };
397 result.ok_and(|| FromFFI::parse_ffi(keygen_id_out.as_str()))
398 }
399
400 pub async fn export_full_private_key(
407 &self,
408 room_uuid: &RoomUUID,
409 secret_share: &EcdsaSecretShare,
410 export_to: &KeygenId,
411 ) -> Result<Option<EcdsaExtendedPrivateKey>, SodotError> {
412 let room = Room::new(&self.host_url, room_uuid);
413 let secret_share = secret_share.ffi();
414 let export_to = export_to.ffi();
415 async_ffi_single(|cb| unsafe {
416 ffi::sodot_ecdsa_export_full_private_key(
417 room.raw_ffi(),
418 secret_share.raw_ffi(),
419 export_to.raw_ffi(),
420 cb.callback(),
421 cb.extra(),
422 )
423 })
424 .await
425 }
426
427 pub fn offline_export_full_private_key(&self, secret_shares: &[EcdsaSecretShare]) -> Result<EcdsaExtendedPrivateKey, SodotError> {
433 let secret_share_const_strs: Vec<ConstStr> = secret_shares.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
434 let secret_shares_slice = ConstSlice::new(&secret_share_const_strs);
435 let mut privkey_out = HeapString::NULL;
436 let result = unsafe { ffi::sodot_ecdsa_offline_export_full_private_key(secret_shares_slice.raw_ffi(), privkey_out.ffi_mut()) };
437 result.ok_and(|| FromFFI::parse_ffi(privkey_out.as_str()))
438 }
439
440 pub async fn import_private_key_recipient(
449 &self,
450 room_uuid: &RoomUUID,
451 threshold: NonZeroU16,
452 keygen_init_keypair: &KeygenPrivateKey,
453 keygen_ids: &[KeygenId],
454 ) -> Result<(EcdsaPublicKey, EcdsaSecretShare), SodotError> {
455 let room = Room::new(&self.host_url, room_uuid);
456 let keygen_init_keypair = keygen_init_keypair.ffi();
457
458 let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
459 let keygen_slice = ConstSlice::new(&keygen_ids);
460
461 async_ffi_double(|cb| unsafe {
462 ffi::sodot_ecdsa_import_private_key_recipient(
463 room.raw_ffi(),
464 threshold.get(),
465 keygen_init_keypair.raw_ffi(),
466 keygen_slice.raw_ffi(),
467 cb.callback(),
468 cb.extra(),
469 )
470 })
471 .await
472 }
473
474 pub async fn import_private_key_importer(
483 &self,
484 room_uuid: &RoomUUID,
485 threshold: NonZeroU16,
486 private_key: &[u8; 32],
487 keygen_init_keypair: &KeygenPrivateKey,
488 keygen_ids: &[KeygenId],
489 ) -> Result<(EcdsaPublicKey, EcdsaSecretShare), SodotError> {
490 let room = Room::new(&self.host_url, room_uuid);
491 let keygen_init_keypair = keygen_init_keypair.ffi();
492
493 let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
494 let keygen_slice = ConstSlice::new(&keygen_ids);
495 let private_key = private_key.ffi();
496
497 async_ffi_double(|cb| unsafe {
498 ffi::sodot_ecdsa_import_private_key_importer(
499 room.raw_ffi(),
500 threshold.get(),
501 private_key.raw_ffi(),
502 keygen_init_keypair.raw_ffi(),
503 keygen_slice.raw_ffi(),
504 ConstStr::NULL.raw_ffi(),
505 cb.callback(),
506 cb.extra(),
507 )
508 })
509 .await
510 }
511}
512
513#[cfg(test)]
514mod test {
515 use crate::{Ecdsa, Scheme, ecdsa::MessageHash};
516 use rand::{Rng, SeedableRng, rngs::SmallRng};
517 use rstest::rstest;
518 use sha2::Digest;
519 use std::num::NonZeroU16;
520
521 fn test_hashing<F1, F2>(f1: F1, f2: F2)
522 where
523 F1: Fn(&[u8]) -> [u8; 32],
524 F2: Fn(&[u8]) -> [u8; 32],
525 {
526 const MAX_MESSAGE: usize = 4096;
527 let mut rng = SmallRng::seed_from_u64(0xdeadbeaf);
528 let mut msg = [0u8; MAX_MESSAGE];
529 for i in 0..MAX_MESSAGE {
530 let msg = &mut msg[..i];
531 rng.fill(msg);
532 assert_eq!(f1(msg), f2(msg));
533 }
534 }
535
536 #[cfg_attr(miri, ignore)]
537 #[rstest]
538 fn test_message_sha2() {
539 test_hashing(|msg| MessageHash::sha256(msg).0, |msg| sha2::Sha256::digest(msg).into());
540 }
541
542 #[cfg_attr(miri, ignore)]
543 #[rstest]
544 fn test_message_keccak256() {
545 test_hashing(|msg| MessageHash::keccak256(msg).0, |msg| sha3::Keccak256::digest(msg).into());
546 }
547
548 #[cfg_attr(miri, ignore)]
549 #[tokio::test]
550 async fn test_ecdsa_public_key_format() {
551 const ONE: NonZeroU16 = NonZeroU16::new(1).unwrap();
552 let ecdsa = Ecdsa::with_test_host();
553 let (_, keygen_secret) = ecdsa.init_keygen().expect("Keygen init should succeed");
554 let room_uuid = ecdsa.create_room(ONE, "").await.expect("Room creation should succeed");
555 let (pubkey, secret_share) = ecdsa.keygen(&room_uuid, ONE, ONE, &keygen_secret, &[]).await.expect("Keygen should succeed");
556
557 assert_eq!(pubkey.uncompressed().len(), 65, "Uncompressed public key should be 65 bytes");
558 assert_eq!(pubkey.compressed().len(), 33, "Compressed public key should be 33 bytes");
559 assert_eq!(pubkey.uncompressed()[1..33], pubkey.compressed()[1..], "Compressed and uncompressed should share x-coordinate");
560
561 let room_uuid = ecdsa.create_room(ONE, "").await.expect("Room creation should succeed");
562 let message_hash = MessageHash::sha256(&[]);
563 let sig = ecdsa.sign(&room_uuid, &secret_share, &message_hash, &[]).await.expect("Signing should succeed");
564
565 assert_eq!(sig.r().len(), 32, "Signature r component should be 32 bytes");
566 assert_eq!(sig.s().len(), 32, "Signature s component should be 32 bytes");
567 assert!(sig.v() >= 27 && sig.v() <= 30, "Signature v should be in range 27-30");
568 assert!(sig.der().len() >= 8 && sig.der().len() <= 72, "DER signature should be 8-72 bytes");
569 }
570}