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}