sodot_mpc/schemes/
ed25519.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 Ed25519 cryptographic operations.
8pub type Ed25519SecretShare = SecretShare<Ed25519>;
9
10/// The size of a Ed25519 public key in bytes.
11pub const PUB_KEY_SIZE: usize = 32;
12
13/// A public key for Ed25519 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 Ed25519PublicKey = PublicKey<Ed25519, PUB_KEY_SIZE>;
19
20/// An extended public key for Ed25519 operations.
21pub type Ed25519ExtendedPublicKey = ExtendedPublicKey<Ed25519>;
22
23/// An extended private key for Ed25519 operations.
24pub type Ed25519ExtendedPrivateKey = ExtendedPrivateKey<Ed25519>;
25
26impl Ed25519PublicKey {
27    /// The size of an Ed25519 public key in bytes.
28    pub const SIZE: usize = 32;
29
30    /// Returns the public key as a byte array.
31    pub fn as_bytes(&self) -> &[u8; 32] {
32        &self.0
33    }
34
35    /// Consumes the public key and returns it as a byte array.
36    pub fn into_bytes(self) -> [u8; 32] {
37        self.0
38    }
39}
40
41/// A threshold Ed25519 implementation that allows distributed key generation and signing.
42///
43/// This struct provides methods for:
44/// - Distributed key generation among multiple parties using threshold cryptography
45/// - Threshold signing with Ed25519 signatures using BIP-32 compatible derivation
46/// - Key derivation using BIP-32 non-hardened derivation paths (numeric-based)
47/// - Secret share management, refresh, and reshare operations
48/// - Private key import/export functionality for recovery scenarios
49///
50/// Ed25519 is a high-performance elliptic curve signature scheme that offers:
51/// - Fast signature generation and verification
52/// - Small signature size (64 bytes) and public key size (32 bytes)
53/// - Strong security properties with resistance to side-channel attacks
54/// - Deterministic signatures (no randomness required during signing)
55/// - Wide adoption in modern cryptographic applications
56///
57/// Unlike ECDSA, Ed25519 uses a different elliptic curve (Curve25519) and does not
58/// require careful handling of randomness during signing, making it more robust
59/// against implementation errors.
60///
61/// Note: This implementation uses Sodot's custom extended key format (Spub/Spriv)
62/// since there's no standardized format for Ed25519 extended keys like BIP-32.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct Ed25519 {
65    host_url: String,
66}
67
68impl Scheme for Ed25519 {
69    fn new(host_url: String) -> Self {
70        Self { host_url }
71    }
72
73    fn host_url(&self) -> &str {
74        &self.host_url
75    }
76}
77
78/// A full private key for Ed25519 cryptographic operations.
79/// When using `[Self::Raw]` the 32 bytes are the private key itself.
80/// When using `[Self::RFC8032]` the secret is the first 32 bytes of the sha512 of the given 32 bytes (which are the key k in ed25519 (rfc8032)).
81/// the secret is then “clamped” to produce a valid ed25519 scalar.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum FullPrivateKey {
84    /// 32 bytes canonical raw Ed25519 scalar private key
85    Raw([u8; 32]),
86    /// The pre-image bytes for RF8032 Ed25519 key generation
87    /// The actual secret is the first 32 bytes of the sha512 of the pre-image, which is then "clamped", all according to RFC8032.
88    RFC8032([u8; 32]),
89}
90
91impl Ed25519 {
92    /// Creates a new Ed25519 instance with the specified host URL.
93    ///
94    /// # Arguments
95    ///
96    /// * `host_url` - The URL of the Sodot relay server to use for MPC operations
97    ///
98    /// # Returns
99    ///
100    /// A new Ed25519 instance configured with the specified host URL.
101    pub fn new(host_url: String) -> Self {
102        <Self as Scheme>::new(host_url)
103    }
104
105    /// Creates a new Ed25519 instance using the default production host URL.
106    ///
107    /// This is a convenience method that creates an instance configured to use
108    /// the default Sodot production relay server.
109    ///
110    /// # Returns
111    ///
112    /// A new Ed25519 instance configured with the default host URL.
113    pub fn with_default_host() -> Self {
114        <Self as Scheme>::with_default_host()
115    }
116
117    /// Initializes the key generation process by creating a keygen ID and private key.
118    ///
119    /// This method generates the initial cryptographic material needed to participate
120    /// in multi-party key generation protocols.
121    ///
122    /// # Returns
123    ///
124    /// A `Result` containing a tuple of ([`KeygenId`], [`KeygenPrivateKey`])
125    ///
126    /// # Errors
127    ///
128    /// Returns a [`SodotError`] if the key generation initialization fails.
129    pub fn init_keygen(&self) -> Result<(KeygenId, KeygenPrivateKey), SodotError> {
130        <Self as Scheme>::init_keygen(self)
131    }
132
133    /// Creates a room for coordinating multi-party computation protocols.
134    ///
135    /// # Arguments
136    ///
137    /// * `num_parties` - The total number of parties that will participate
138    /// * `api_key` - The API key for authenticating with the Sodot relay server
139    ///
140    /// # Returns
141    ///
142    /// A `Result` containing the [`RoomUUID`] that identifies the created room.
143    pub async fn create_room(&self, num_parties: NonZeroU16, api_key: &str) -> Result<RoomUUID, SodotError> {
144        <Self as Scheme>::create_room(self, num_parties, api_key).await
145    }
146
147    /// Generates a threshold Ed25519 key using distributed key generation.
148    ///
149    /// Multiple parties collaborate to create a shared public key and individual secret shares.
150    /// The resulting public key can be used for verification, while the secret shares are
151    /// required for threshold signing.
152    pub async fn keygen(
153        &self,
154        room_uuid: &RoomUUID,
155        num_parties: NonZeroU16,
156        threshold: NonZeroU16,
157        keygen_init_keypair: &KeygenPrivateKey,
158        keygen_ids: &[KeygenId],
159    ) -> Result<(Ed25519PublicKey, Ed25519SecretShare), SodotError> {
160        let room = Room::new(&self.host_url, room_uuid);
161        let keygen_init_keypair = keygen_init_keypair.ffi();
162
163        let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
164        let keygen_slice = ConstSlice::new(&keygen_ids);
165
166        async_ffi_double(|cb| unsafe {
167            ffi::sodot_ed25519_keygen(
168                room.raw_ffi(),
169                num_parties.get(),
170                threshold.get(),
171                keygen_init_keypair.raw_ffi(),
172                keygen_slice.raw_ffi(),
173                cb.callback(),
174                cb.extra(),
175            )
176        })
177        .await
178    }
179
180    /// Signs a message using the threshold Ed25519 scheme.
181    ///
182    /// A subset of parties (at least the threshold) collaborate to generate a valid Ed25519
183    /// signature for a given message. The signature can be verified against the public
184    /// key generated during keygen.
185    pub async fn sign(
186        &self,
187        room_uuid: &RoomUUID,
188        secret_share: &Ed25519SecretShare,
189        message: &[u8],
190        derivation_path: &[u32],
191    ) -> Result<[u8; 64], SodotError> {
192        let room = Room::new(&self.host_url, room_uuid);
193        let secret_share = secret_share.ffi();
194        let message = message.ffi();
195
196        let derivation_path = ConstSlice::new(derivation_path);
197
198        async_ffi_single(|cb| unsafe {
199            ffi::sodot_ed25519_sign(
200                room.raw_ffi(),
201                secret_share.raw_ffi(),
202                message.raw_ffi(),
203                derivation_path.raw_ffi(),
204                cb.callback(),
205                cb.extra(),
206            )
207        })
208        .await
209    }
210
211    /// Derives an Ed25519 public key using a BIP-32 non-hardened derivation path.
212    ///
213    /// Derives child public keys from a secret share without needing additional threshold
214    /// operations. Follows the BIP-32 standard for hierarchical deterministic key derivation.
215    pub fn derive_pubkey(&self, secret_share: &Ed25519SecretShare, derivation_path: &[u32]) -> Result<Ed25519PublicKey, SodotError> {
216        let secret_share = secret_share.ffi();
217        let derivation_path = ConstSlice::new(derivation_path);
218        let mut pubkey_out = HeapString::NULL;
219        let result =
220            unsafe { ffi::sodot_ed25519_derive_pubkey(secret_share.raw_ffi(), derivation_path.raw_ffi(), pubkey_out.ffi_mut()) };
221        result.ok_and(|| FromFFI::parse_ffi(pubkey_out.as_str()))
222    }
223
224    /// Returns a base58-encoded extended public key (Spub) derived from a secret share.
225    ///
226    /// The returned Spub follows Sodot's custom extended key format and can be used with
227    /// the `derive_pubkey_from_spub` method for additional key derivations.
228    pub fn get_spub(&self, secret_share: &Ed25519SecretShare) -> Result<Ed25519ExtendedPublicKey, SodotError> {
229        let secret_share_cstr = secret_share.ffi();
230        let mut spub_out = HeapString::NULL;
231        let result = unsafe { ffi::sodot_ed25519_get_spub(secret_share_cstr.raw_ffi(), spub_out.ffi_mut()) };
232        result.ok_and(|| FromFFI::parse_ffi(spub_out.as_str()))
233    }
234
235    /// Derives an Ed25519 public key from an extended public key (Spub) using a BIP-32 derivation path.
236    ///
237    /// Derive child public keys from an extended public key without needing access to the
238    /// original secret share. Useful for watch-only wallets and key management systems.
239    pub fn derive_pubkey_from_spub(
240        &self,
241        spub: &Ed25519ExtendedPublicKey,
242        derivation_path: &[u32],
243    ) -> Result<Ed25519PublicKey, SodotError> {
244        let spub = spub.ffi();
245        let derivation_path = ConstSlice::new(derivation_path);
246        let mut pubkey_out = HeapString::NULL;
247        let result =
248            unsafe { ffi::sodot_ed25519_derive_pubkey_from_spub(spub.raw_ffi(), derivation_path.raw_ffi(), pubkey_out.ffi_mut()) };
249        result.ok_and(|| FromFFI::parse_ffi(pubkey_out.as_str()))
250    }
251
252    /// Derives a private key from an extended private key (spriv) using a BIP-32 derivation path.
253    ///
254    /// Derives child private keys from an extended private key following the BIP-32
255    /// hierarchical deterministic key derivation standard.
256    pub fn derive_private_key_from_spriv(
257        &self,
258        spriv: &Ed25519ExtendedPrivateKey,
259        derivation_path: &[u32],
260    ) -> Result<[u8; 32], SodotError> {
261        let spriv = spriv.ffi();
262        let derivation_path = ConstSlice::new(derivation_path);
263        let mut privkey_out = HeapString::NULL;
264        let result = unsafe {
265            ffi::sodot_ed25519_derive_private_key_from_spriv(spriv.raw_ffi(), derivation_path.raw_ffi(), privkey_out.ffi_mut())
266        };
267        result.ok_and(|| FromFFI::parse_ffi(privkey_out.as_str()))
268    }
269
270    /// Refreshes the secret material of all parties without changing the public key.
271    ///
272    /// Generates fresh randomness for the secret shares while maintaining the same public key,
273    /// improving security against long-term attacks. All parties must participate.
274    ///
275    /// **Important**: Delete the input `secret_share` only after confirming all parties
276    /// have successfully stored their new secret shares.
277    pub async fn refresh(
278        &self,
279        room_uuid: &RoomUUID,
280        secret_share: &Ed25519SecretShare,
281    ) -> Result<(Ed25519PublicKey, Ed25519SecretShare), SodotError> {
282        let room = Room::new(&self.host_url, room_uuid);
283        let secret_share = secret_share.ffi();
284        async_ffi_double(|cb| unsafe { ffi::sodot_ed25519_refresh(room.raw_ffi(), secret_share.raw_ffi(), cb.callback(), cb.extra()) })
285            .await
286    }
287
288    /// Receives a key share for a new party in a reshared quorum.
289    ///
290    /// **Warning**: Key resharing is an **advanced** feature. Consult with the Sodot team before
291    /// using it, as incorrect usage might compromise private key security.
292    ///
293    /// Allows a new party to join an existing threshold scheme by participating
294    /// in a reshare protocol with existing parties.
295    pub async fn reshare_new_party(
296        &self,
297        room_uuid: &RoomUUID,
298        new_threshold: NonZeroU16,
299        keygen_private_key: &KeygenPrivateKey,
300        keygen_ids: &[KeygenId],
301    ) -> Result<(Ed25519PublicKey, Ed25519SecretShare), SodotError> {
302        let room = Room::new(&self.host_url, room_uuid);
303        let keygen_init_keypair = keygen_private_key.ffi();
304
305        let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
306        let keygen_slice = ConstSlice::new(&keygen_ids);
307
308        async_ffi_double(|cb| unsafe {
309            ffi::sodot_ed25519_reshare_new_party(
310                room.raw_ffi(),
311                new_threshold.get(),
312                keygen_init_keypair.raw_ffi(),
313                keygen_slice.raw_ffi(),
314                cb.callback(),
315                cb.extra(),
316            )
317        })
318        .await
319    }
320
321    /// Receives a key share for an existing party in a reshared quorum.
322    ///
323    /// **Warning**: Key resharing is an **advanced** feature. Consult with the Sodot team before
324    /// using it, as incorrect usage might compromise private key security.
325    ///
326    /// Allows an existing party to participate in a reshare protocol to
327    /// create a new threshold scheme configuration.
328    pub async fn reshare_remaining_party(
329        &self,
330        room_uuid: &RoomUUID,
331        new_threshold: NonZeroU16,
332        secret_share: &Ed25519SecretShare,
333        keygen_ids: &[KeygenId],
334    ) -> Result<(Ed25519PublicKey, Ed25519SecretShare), SodotError> {
335        let room = Room::new(&self.host_url, room_uuid);
336        let secret_share = secret_share.ffi();
337
338        let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
339        let keygen_slice = ConstSlice::new(&keygen_ids);
340
341        async_ffi_double(|cb| unsafe {
342            ffi::sodot_ed25519_reshare_remaining_party(
343                room.raw_ffi(),
344                new_threshold.get(),
345                secret_share.raw_ffi(),
346                keygen_slice.raw_ffi(),
347                cb.callback(),
348                cb.extra(),
349            )
350        })
351        .await
352    }
353
354    /// Extracts the KeygenID from a secret share.
355    ///
356    /// Useful for reshare operations and private key export/import operations
357    /// to identify parties in the threshold scheme.
358    pub fn export_id(&self, secret_share: &Ed25519SecretShare) -> Result<KeygenId, SodotError> {
359        let secret_share_str = secret_share.ffi();
360        let mut keygen_id_out = HeapString::NULL;
361        let result = unsafe { ffi::sodot_ed25519_get_export_id(secret_share_str.raw_ffi(), keygen_id_out.ffi_mut()) };
362        result.ok_and(|| FromFFI::parse_ffi(keygen_id_out.as_str()))
363    }
364
365    /// Combines multiple secret shares to export the full private key to a designated party.
366    ///
367    /// Requires at least a threshold number of parties to participate. The full private key
368    /// will only be revealed to the party specified by `export_to`. Returns `Some(String)`
369    /// with the full private key if the current party is the designated receiver,
370    /// `None` otherwise.
371    pub async fn export_full_private_key(
372        &self,
373        room_uuid: &RoomUUID,
374        secret_share: &Ed25519SecretShare,
375        export_to: &KeygenId,
376    ) -> Result<Option<Ed25519ExtendedPrivateKey>, SodotError> {
377        let room = Room::new(&self.host_url, room_uuid);
378        let secret_share = secret_share.ffi();
379        let export_to = export_to.ffi();
380        async_ffi_single(|cb| unsafe {
381            ffi::sodot_ed25519_export_full_private_key(
382                room.raw_ffi(),
383                secret_share.raw_ffi(),
384                export_to.raw_ffi(),
385                cb.callback(),
386                cb.extra(),
387            )
388        })
389        .await
390    }
391
392    /// Locally reconstructs the full private key from a threshold number of secret shares.
393    ///
394    /// Designed for offline recovery scenarios where secret shares are collected manually.
395    /// Does not require network communication and can be used to recover the private key
396    /// when threshold signing is not possible.
397    pub fn offline_export_full_private_key(
398        &self,
399        secret_shares: &[Ed25519SecretShare],
400    ) -> Result<Ed25519ExtendedPrivateKey, SodotError> {
401        let secret_share_const_strs: Vec<_> = secret_shares.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
402        let secret_shares_slice = ConstSlice::new(&secret_share_const_strs);
403        let mut privkey_out = HeapString::NULL;
404        let result =
405            unsafe { ffi::sodot_ed25519_offline_export_full_private_key(secret_shares_slice.raw_ffi(), privkey_out.ffi_mut()) };
406        result.ok_and(|| FromFFI::parse_ffi(privkey_out.as_str()))
407    }
408
409    /// Receives threshold shares of an imported private key as a recipient party.
410    ///
411    /// **Warning**: Private key import is an **advanced** feature. Consult with the Sodot team before
412    /// using it, as incorrect usage might compromise security.
413    ///
414    /// Allows a party to participate as a recipient in importing an external private key
415    /// into a threshold scheme. The private key holder will distribute shares of their
416    /// key to all recipients.
417    pub async fn import_private_key_recipient(
418        &self,
419        room_uuid: &RoomUUID,
420        threshold: NonZeroU16,
421        keygen_init_keypair: &KeygenPrivateKey,
422        keygen_ids: &[KeygenId],
423    ) -> Result<(Ed25519PublicKey, Ed25519SecretShare), SodotError> {
424        let room = Room::new(&self.host_url, room_uuid);
425        let keygen_init_keypair = keygen_init_keypair.ffi();
426
427        let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
428        let keygen_slice = ConstSlice::new(&keygen_ids);
429
430        async_ffi_double(|cb| unsafe {
431            ffi::sodot_ed25519_import_private_key_recipient(
432                room.raw_ffi(),
433                threshold.get(),
434                keygen_init_keypair.raw_ffi(),
435                keygen_slice.raw_ffi(),
436                cb.callback(),
437                cb.extra(),
438            )
439        })
440        .await
441    }
442
443    /// Imports a private key into a threshold scheme as the importer party.
444    ///
445    /// **Warning**: Private key import is an **advanced** feature. Consult with the Sodot team before
446    /// using it, as incorrect usage might compromise security.
447    ///
448    /// Allows a party that holds a private key to import it into a threshold scheme
449    /// by distributing shares to other parties. The private key will be split among
450    /// all participants.
451    pub async fn import_private_key_importer(
452        &self,
453        room_uuid: &RoomUUID,
454        threshold: NonZeroU16,
455        private_key: FullPrivateKey,
456        keygen_init_keypair: &KeygenPrivateKey,
457        keygen_ids: &[KeygenId],
458    ) -> Result<(Ed25519PublicKey, Ed25519SecretShare), SodotError> {
459        let room = Room::new(&self.host_url, room_uuid);
460        let keygen_init_keypair = keygen_init_keypair.ffi();
461
462        let keygen_ids: Vec<_> = keygen_ids.iter().map(|s| ConstStr::from_str(s.as_str())).collect();
463        let keygen_slice = ConstSlice::new(&keygen_ids);
464        let (is_private_key_raw, private_key) = match &private_key {
465            FullPrivateKey::Raw(b) => (1, b.ffi()),
466            FullPrivateKey::RFC8032(b) => (0, b.ffi()),
467        };
468
469        async_ffi_double(|cb| unsafe {
470            ffi::sodot_ed25519_import_private_key_importer(
471                room.raw_ffi(),
472                threshold.get(),
473                private_key.raw_ffi(),
474                keygen_init_keypair.raw_ffi(),
475                keygen_slice.raw_ffi(),
476                is_private_key_raw,
477                cb.callback(),
478                cb.extra(),
479            )
480        })
481        .await
482    }
483}