diff --git a/src/gg2020/keygen.rs b/src/gg2020/keygen.rs new file mode 100644 index 0000000..98ca847 --- /dev/null +++ b/src/gg2020/keygen.rs @@ -0,0 +1,113 @@ +//! Key generation. +use curv::elliptic::curves::secp256_k1::Secp256k1; +use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::state_machine::keygen::{ + Keygen, LocalKey, ProtocolMessage, +}; + +use wasm_bindgen::prelude::*; + +use crate::Parameters; +use serde::{Deserialize, Serialize}; + +use round_based::{Msg, StateMachine}; + +//use crate::{console_log, log}; + +/// Wrapper for a round `Msg` that includes the round +/// number so that we can ensure round messages are grouped +/// together and out of order messages can thus be handled correctly. +#[derive(Serialize)] +struct RoundMsg { + round: u16, + sender: u16, + receiver: Option, + body: ProtocolMessage, +} + +impl RoundMsg { + fn from_round( + round: u16, + messages: Vec::MessageBody>>, + ) -> Vec { + messages + .into_iter() + .map(|m| RoundMsg { + round, + sender: m.sender, + receiver: m.receiver, + body: m.body, + }) + .collect::>() + } +} + +/// Session information for a single party. +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct PartySignup { + /// Unique index for the party. + pub number: u16, + /// Session identifier. + pub uuid: String, +} + +/// Generated key share. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyShare { + /// The secret private key. + #[serde(rename = "localKey")] + pub local_key: LocalKey, + /// The public key. + #[serde(rename = "publicKey")] + pub public_key: Vec, + /// Address generated from the public key. + pub address: String, +} + +/// Round-based key share generator. +#[wasm_bindgen] +pub struct KeyGenerator { + inner: Keygen, +} + +#[wasm_bindgen] +impl KeyGenerator { + /// Create a key generator. + #[wasm_bindgen(constructor)] + pub fn new(parameters: JsValue, party_signup: JsValue) -> Result { + let params: Parameters = parameters.into_serde()?; + let PartySignup { number, uuid } = party_signup.into_serde::()?; + let (party_num_int, _uuid) = (number, uuid); + Ok(Self { + inner: Keygen::new(party_num_int, params.threshold, params.parties)?, + }) + } + + /// Handle an incoming message. + #[wasm_bindgen(js_name = "handleIncoming")] + pub fn handle_incoming(&mut self, message: JsValue) -> Result<(), JsError> { + let message: Msg<::MessageBody> = message.into_serde()?; + self.inner.handle_incoming(message)?; + Ok(()) + } + + /// Proceed to the next round. + pub fn proceed(&mut self) -> Result { + self.inner.proceed()?; + let messages = self.inner.message_queue().drain(..).collect(); + let round = self.inner.current_round(); + let messages = RoundMsg::from_round(round, messages); + Ok(JsValue::from_serde(&(round, &messages))?) + } + + /// Create the key share. + pub fn create(&mut self) -> Result { + let local_key = self.inner.pick_output().unwrap()?; + let public_key = local_key.public_key().to_bytes(false).to_vec(); + let key_share = KeyShare { + local_key, + address: crate::utils::address(&public_key), + public_key, + }; + Ok(JsValue::from_serde(&key_share)?) + } +} diff --git a/src/gg2020/mod.rs b/src/gg2020/mod.rs new file mode 100644 index 0000000..e51884a --- /dev/null +++ b/src/gg2020/mod.rs @@ -0,0 +1,2 @@ +pub mod keygen; +pub mod sign; diff --git a/src/gg2020/sign.rs b/src/gg2020/sign.rs new file mode 100644 index 0000000..f215c87 --- /dev/null +++ b/src/gg2020/sign.rs @@ -0,0 +1,152 @@ +//! Message signing. +use curv::{arithmetic::Converter, elliptic::curves::Secp256k1, BigInt}; +use multi_party_ecdsa::protocols::multi_party_ecdsa::gg_2020::{ + party_i::{verify, SignatureRecid}, + state_machine::{ + keygen::LocalKey, + sign::{ + CompletedOfflineStage, OfflineProtocolMessage, OfflineStage, PartialSignature, + SignManual, + }, + }, +}; + +use round_based::{Msg, StateMachine}; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; +use wasm_bindgen::prelude::*; + +//use crate::{console_log, log}; + +const ERR_COMPLETED_OFFLINE_STAGE: &str = + "completed offline stage unavailable, has partial() been called?"; + +/// Wrapper for a round `Msg` that includes the round +/// number so that we can ensure round messages are grouped +/// together and out of order messages can thus be handled correctly. +#[derive(Serialize)] +struct RoundMsg { + round: u16, + sender: u16, + receiver: Option, + body: OfflineProtocolMessage, +} + +impl RoundMsg { + fn from_round( + round: u16, + messages: Vec::MessageBody>>, + ) -> Vec { + messages + .into_iter() + .map(|m| RoundMsg { + round, + sender: m.sender, + receiver: m.receiver, + body: m.body, + }) + .collect::>() + } +} + +/// Signature generated by a signer. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Signature { + /// The generated ECDSA signature. + pub signature: SignatureRecid, + /// The public key. + #[serde(rename = "publicKey")] + pub public_key: Vec, + /// Address generated from the public key. + pub address: String, +} + +/// Round-based signing protocol. +#[wasm_bindgen] +pub struct Signer { + inner: OfflineStage, + completed: Option<(CompletedOfflineStage, BigInt)>, +} + +#[wasm_bindgen] +impl Signer { + /// Create a signer. + #[wasm_bindgen(constructor)] + pub fn new( + index: JsValue, + participants: JsValue, + local_key: JsValue, + ) -> Result { + let index: u16 = index.into_serde()?; + let participants: Vec = participants.into_serde()?; + let local_key: LocalKey = local_key.into_serde()?; + Ok(Signer { + inner: OfflineStage::new(index, participants.clone(), local_key)?, + completed: None, + }) + } + + /// Handle an incoming message. + #[wasm_bindgen(js_name = "handleIncoming")] + pub fn handle_incoming(&mut self, message: JsValue) -> Result<(), JsError> { + let message: Msg<::MessageBody> = message.into_serde()?; + self.inner.handle_incoming(message)?; + Ok(()) + } + + /// Proceed to the next round. + pub fn proceed(&mut self) -> Result { + if self.inner.wants_to_proceed() { + self.inner.proceed()?; + let messages = self.inner.message_queue().drain(..).collect(); + let round = self.inner.current_round(); + let messages = RoundMsg::from_round(round, messages); + Ok(JsValue::from_serde(&(round, &messages))?) + } else { + Ok(JsValue::from_serde(&false)?) + } + } + + /// Generate the completed offline stage and store the result + /// internally to be used when `create()` is called. + /// + /// Return a partial signature that must be sent to the other + /// signing participents. + pub fn partial(&mut self, message: JsValue) -> Result { + let message: Vec = message.into_serde()?; + let message: [u8; 32] = message.as_slice().try_into()?; + let completed_offline_stage = self.inner.pick_output().unwrap()?; + let data = BigInt::from_bytes(&message); + let (_sign, partial) = SignManual::new(data.clone(), completed_offline_stage.clone())?; + + self.completed = Some((completed_offline_stage, data)); + + Ok(JsValue::from_serde(&partial)?) + } + + /// Create and verify the signature. + pub fn create(&mut self, partials: JsValue) -> Result { + let partials: Vec = partials.into_serde()?; + + let (completed_offline_stage, data) = self + .completed + .take() + .ok_or_else(|| JsError::new(ERR_COMPLETED_OFFLINE_STAGE))?; + let pk = completed_offline_stage.public_key().clone(); + + let (sign, _partial) = SignManual::new(data.clone(), completed_offline_stage.clone())?; + + let signature = sign.complete(&partials)?; + verify(&signature, &pk, &data) + .map_err(|e| JsError::new(&format!("failed to verify signature: {:?}", e)))?; + + let public_key = pk.to_bytes(false).to_vec(); + let result = Signature { + signature, + address: crate::utils::address(&public_key), + public_key, + }; + + Ok(JsValue::from_serde(&result)?) + } +} diff --git a/src/lib.rs b/src/lib.rs index e69de29..80434d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -0,0 +1,59 @@ +//! Webassembly bindings to the GG2020 protocol in [multi-party-ecdsa](https://github.com/ZenGo-X/multi-party-ecdsa) for MPC key generation and signing. +#![deny(missing_docs)] +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +extern crate wasm_bindgen; + +#[cfg(all(test, target_arch = "wasm32"))] +extern crate wasm_bindgen_test; + +#[doc(hidden)] +#[wasm_bindgen(start)] +pub fn start() { + console_error_panic_hook::set_once(); + if let Ok(_) = wasm_log::try_init(wasm_log::Config::new(log::Level::Debug)) { + log::info!("WASM logger initialized"); + } + log::info!("WASM: module started {:?}", std::thread::current().id()); +} + +// Required for rayon thread support +pub use wasm_bindgen_rayon::init_thread_pool; + +mod gg2020; +mod utils; + +// Expose these types for API documentation. +pub use gg2020::keygen::{KeyGenerator, KeyShare, PartySignup}; +pub use gg2020::sign::{Signature, Signer}; + +/// Parameters used during key generation. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Parameters { + /// Number of parties `n`. + pub parties: u16, + /// Threshold for signing `t`. + /// + /// The threshold must be crossed (`t + 1`) for signing + /// to commence. + pub threshold: u16, +} + +impl Default for Parameters { + fn default() -> Self { + return Self { + parties: 3, + threshold: 1, + }; + } +} + +/// Compute the Keccak256 hash of a value. +#[wasm_bindgen] +pub fn keccak256(message: JsValue) -> Result { + use sha3::{Digest, Keccak256}; + let message: Vec = message.into_serde()?; + let digest = Keccak256::digest(&message).to_vec(); + Ok(JsValue::from_serde(&digest)?) +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..e02c9b3 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,10 @@ +use sha3::{Digest, Keccak256}; + +/// Compute the address of an uncompressed public key (65 bytes). +pub(crate) fn address(public_key: &Vec) -> String { + // Remove the leading 0x04 + let bytes = &public_key[1..]; + let digest = Keccak256::digest(bytes); + let final_bytes = &digest[12..]; + format!("0x{}", hex::encode(&final_bytes)) +}