sodot_mpc/schemes/
ecdsa.rs

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
7/// A secret share for ECDSA cryptographic operations.
8pub type EcdsaSecretShare = SecretShare<Ecdsa>;
9
10/// The size of an uncompressed ECDSA public key in bytes.
11pub const UNCOMPRESSED_PUB_KEY_SIZE: usize = 65;
12
13/// A public key for ECDSA cryptographic operations.
14///
15/// This struct provides functionality to serialize the public key into both uncompressed
16/// and compressed formats. The public key is stored in uncompressed format (65 bytes)
17/// and can be converted to compressed format (33 bytes) when needed.
18pub type EcdsaPublicKey = PublicKey<Ecdsa, UNCOMPRESSED_PUB_KEY_SIZE>;
19
20/// A signature for ECDSA cryptographic operations.
21///
22/// This struct encapsulates the components of an ECDSA signature, which include the 'r' and 's' values as byte arrays,
23/// a 'v' recovery id, and the signature in DER encoding. The 'r' and 's' components are the main parts of the signature,
24/// 'v' is used to recover the public key from the signature, and the DER encoding is used for compatibility with
25/// systems that require the signature in ASN.1/DER format.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct EcdsaSignature {
28    pub(crate) expanded_sig: Vec<u8>,
29}
30
31/// An extended public key for ECDSA operations.
32pub type EcdsaExtendedPublicKey = ExtendedPublicKey<Ecdsa>;
33
34/// An extended private key for ECDSA operations.
35pub type EcdsaExtendedPrivateKey = ExtendedPrivateKey<Ecdsa>;
36
37/// A message hash for ECDSA operations.
38/// Can be created either via [MessageHash::sha256] or [MessageHash::keccak256].
39/// If you need a different hash function you can hash yourself and construct via [MessageHash::from_hash].
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct MessageHash(pub(crate) [u8; 32]);
42
43impl MessageHash {
44    /// Creates a new MessageHash from a 32-byte hash.
45    pub fn from_hash(hash: [u8; 32]) -> Self {
46        Self(hash)
47    }
48
49    /// Returns a reference to the underlying hash byte array.
50    pub fn as_bytes(&self) -> &[u8; 32] {
51        &self.0
52    }
53
54    /// Consumes the MessageHash and returns the underlying hash byte array.
55    pub fn into_bytes(self) -> [u8; 32] {
56        self.0
57    }
58
59    /// Uses `SHA-256` to hash the message and returns a MessageHash.
60    pub fn sha256(msg: &[u8]) -> Self {
61        Self::hash(msg, ffi::sodot_sha256)
62    }
63
64    /// Uses `Keccak-256` to hash the message and returns a MessageHash.
65    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        // SAFETY: The pointer is either null or valid for the length of the message.
74        unsafe {
75            f(ptr, msg.len(), &mut hash32);
76        }
77        Self(hash32)
78    }
79}
80
81impl EcdsaPublicKey {
82    /// The size of a compressed ECDSA public key in bytes.
83    pub const COMPRESSED_SIZE: usize = 33;
84
85    /// Returns the uncompressed public key as a byte array.
86    pub fn uncompressed(&self) -> [u8; UNCOMPRESSED_PUB_KEY_SIZE] {
87        self.0
88    }
89
90    /// Returns the compressed public key as a byte array.
91    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    /// The size of a field element in bytes (32 bytes for secp256k1).
102    pub(crate) const FIELD_SIZE: usize = 32;
103    /// Minimum size: 2 field elements + minimum 8 DER bytes + 1 v byte.
104    pub(crate) const MIN_SIZE: usize = 2 * Self::FIELD_SIZE + 8 + 1;
105    /// Maximum size: 2 field elements + maximum 72 DER bytes + 1 v byte.
106    pub(crate) const MAX_SIZE: usize = 2 * Self::FIELD_SIZE + 72 + 1;
107
108    /// Returns the 'r' component of the ECDSA signature.
109    pub fn r(&self) -> [u8; 32] {
110        self.expanded_sig[..Self::FIELD_SIZE].try_into().unwrap()
111    }
112
113    /// Returns the 's' component of the ECDSA signature.
114    pub fn s(&self) -> [u8; 32] {
115        self.expanded_sig[Self::FIELD_SIZE..Self::FIELD_SIZE * 2].try_into().unwrap()
116    }
117
118    /// Returns the 'v' value (recovery ID) of the ECDSA signature.
119    pub fn v(&self) -> u8 {
120        self.expanded_sig[Self::FIELD_SIZE * 2]
121    }
122
123    /// Returns the DER-encoded representation of the ECDSA signature.
124    pub fn der(&self) -> &[u8] {
125        &self.expanded_sig[Self::FIELD_SIZE * 2 + 1..]
126    }
127
128    /// Returns the compact representation of the ECDSA signature as a 64-byte array.
129    /// This is equivilant to `self.r() || self.s()`
130    pub fn compact(&self) -> [u8; 64] {
131        self.expanded_sig[..Self::FIELD_SIZE * 2].try_into().unwrap()
132    }
133}
134
135/// A threshold ECDSA implementation that allows distributed key generation and signing.
136///
137/// This struct provides methods for:
138/// - Distributed key generation among multiple parties
139/// - Threshold signing with a subset of parties
140/// - Key derivation using BIP-32 paths
141/// - Secret share management and refresh operations
142/// - Private key import/export functionality
143#[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    /// Creates a new ECDSA instance with the specified host URL.
160    pub fn new(host_url: String) -> Self {
161        <Self as Scheme>::new(host_url)
162    }
163
164    /// Creates a new ECDSA instance using the default production host URL.
165    pub fn with_default_host() -> Self {
166        <Self as Scheme>::with_default_host()
167    }
168
169    /// Initializes the key generation process by creating a keygen ID and private key.
170    pub fn init_keygen(&self) -> Result<(KeygenId, KeygenPrivateKey), SodotError> {
171        <Self as Scheme>::init_keygen(self)
172    }
173
174    /// Creates a room for coordinating multi-party computation protocols.
175    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    /// Generates a threshold ECDSA key.
180    ///
181    /// Multiple parties collaborate to create a shared public key and individual secret shares.
182    /// The resulting public key can be used for verification, while the secret shares are
183    /// required for threshold signing.
184    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    /// Signs a message using the threshold ECDSA scheme.
213    ///
214    /// A subset of parties (at least the threshold) collaborate to generate a valid ECDSA
215    /// signature for a given message hash. The signature can be verified against the public
216    /// key generated during keygen.
217    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    /// Derives a public key using a BIP-32 non-hardened derivation path.
244    ///
245    /// Derives child public keys from a secret share without needing additional threshold
246    /// operations. Follows the BIP-32 standard for hierarchical deterministic key derivation.
247    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    /// Returns a base58-encoded extended public key (Xpub) derived from a secret share.
257    ///
258    /// The returned Xpub follows the BIP-32 standard and can be used with third-party libraries
259    /// or with the `derive_pubkey_from_xpub` method for additional key derivations.
260    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    /// Derives a public key from an extended public key (Xpub) using a BIP-32 derivation path.
269    ///
270    /// Derive child public keys from an extended public key without needing access to the
271    /// original secret share. Useful for watch-only wallets and key management systems.
272    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    /// Derives a private key from an extended private key (xpriv) using a BIP-32 derivation path.
287    ///
288    /// Derives child private keys from an extended private key following the BIP-32
289    /// hierarchical deterministic key derivation standard.
290    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    /// Refreshes the secret material of all parties without changing the public key.
306    ///
307    /// Generates fresh randomness for the secret shares while maintaining the same public key,
308    /// improving security against long-term attacks. All parties must participate.
309    ///
310    /// **Important**: Delete the input `secret_share` only after confirming all parties
311    /// have successfully stored their new secret shares.
312    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    /// Receives a key share for a new party in a reshared quorum.
324    ///
325    /// **Warning**: Key resharing is an **advanced** feature. Consult with the Sodot team before
326    /// using it, as incorrect usage might compromise private key security.
327    ///
328    /// Allows a new party to join an existing threshold scheme by participating
329    /// in a reshare protocol with existing parties.
330    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    /// Receives a new key share for a remaining party in a reshared quorum.
357    ///
358    /// **Warning**: Key resharing is an **advanced** feature. Consult with the Sodot team before
359    /// using it, as incorrect usage might compromise private key security.
360    ///
361    /// Allows an existing party to participate in a reshare protocol to
362    /// create a new threshold scheme configuration.
363    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    /// Extracts the KeygenID from a secret share.
390    ///
391    /// Useful for reshare operations and private key export/import operations
392    /// to identify parties in the threshold scheme.
393    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    /// Combines multiple secret shares to export the full private key to a designated party.
401    ///
402    /// Requires at least a threshold number of parties to participate. The full private key
403    /// will only be revealed to the party specified by `export_to`. Returns `Some(String)`
404    /// with the full private key (xpriv) if the current party is the designated receiver,
405    /// `None` otherwise.
406    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    /// Locally reconstructs the full private key from a threshold number of secret shares.
428    ///
429    /// Designed for offline recovery scenarios where secret shares are collected manually.
430    /// Does not require network communication and can be used to recover the private key
431    /// when threshold signing is not possible.
432    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    /// Participates as a recipient in a private key import operation.
441    ///
442    /// **Warning**: Private key import is an **advanced** feature. Consult with the Sodot team before
443    /// using it, as incorrect usage might compromise security.
444    ///
445    /// Allows a party to participate as a recipient in importing an external private key
446    /// into a threshold scheme. The private key holder will distribute shares of their
447    /// key to all recipients.
448    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    /// Participates as the importing party in a private key import operation.
475    ///
476    /// **Warning**: Private key import is an **advanced** feature. Consult with the Sodot team before
477    /// using it, as incorrect usage might compromise security.
478    ///
479    /// Allows a party that holds a private key to import it into a threshold scheme
480    /// by distributing shares to other parties. The private key will be split among
481    /// all participants.
482    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}