diff --git a/Cargo.toml b/Cargo.toml index 1e30e8fc..25b71b17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ categories = ["cryptography"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +aes-gcm = "0.10" bs58 = "0.5" hashbrown = { version = "0.14", features = ["serde"] } hex = "0.4.3" diff --git a/src/lib.rs b/src/lib.rs index 74a7e9db..91cde050 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,13 +7,15 @@ pub mod common; pub mod compute; /// Errors which are returned from objects and functions pub mod errors; +/// Network messages +pub mod net; /// Schnorr utility types pub mod schnorr; /// Functions for doing BIP-340 schnorr proofs and other taproot actions pub mod taproot; /// Traits which are used for v1 and v2 pub mod traits; -/// Utilities for hashing scalars +/// Utilities for hashing and encryption pub mod util; /// Version 1 of WSTS, which encapsulates a number of parties using vanilla FROST pub mod v1; diff --git a/src/net.rs b/src/net.rs new file mode 100644 index 00000000..d8ce9e20 --- /dev/null +++ b/src/net.rs @@ -0,0 +1,274 @@ +use hashbrown::HashMap; +use p256k1::{ + ecdsa, + scalar::Scalar, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::{ + common::{PolyCommitment, PublicNonce, SignatureShare}, +}; + +/// Trait to encapsulate sign/verify, users only need to impl hash +pub trait Signable { + /// Hash this object in a consistent way so it can be signed/verified + fn hash(&self, hasher: &mut Sha256); + + /// Sign a hash of this object using the passed private key + fn sign(&self, private_key: &Scalar) -> Result, ecdsa::Error> { + let mut hasher = Sha256::new(); + + self.hash(&mut hasher); + + let hash = hasher.finalize(); + match ecdsa::Signature::new(hash.as_slice(), private_key) { + Ok(sig) => Ok(sig.to_bytes().to_vec()), + Err(e) => Err(e), + } + } + + /// Verify a hash of this object using the passed public key + fn verify(&self, signature: &[u8], public_key: &ecdsa::PublicKey) -> bool { + let mut hasher = Sha256::new(); + + self.hash(&mut hasher); + + let hash = hasher.finalize(); + let sig = match ecdsa::Signature::try_from(signature) { + Ok(sig) => sig, + Err(_) => return false, + }; + + sig.verify(hash.as_slice(), public_key) + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +/// Final DKG status after receiving public and private shares +pub enum DkgStatus { + /// DKG completed successfully + Success, + /// DKG failed with error + Failure(String), +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +/// Encapsulation of all possible network message types +pub enum Message { + /// Tell signers to begin DKG + DkgBegin(DkgBegin), + /// Send DKG public shares + DkgPublicShares(DkgPublicShares), + /// Send DKG private shares + DkgPrivateShares(DkgPrivateShares), + /// Tell coordinator that DKG is complete + DkgEnd(DkgEnd), + /// Tell signers to send signing nonces + NonceRequest(NonceRequest), + /// Tell coordinator signing nonces + NonceResponse(NonceResponse), + /// Tell signers to construct signature shares + SigmatureShareRequest(SignatureShareRequest), + /// Tell coordinator signature shares + SignatureShareResponse(SignatureShareResponse), +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +/// DKG begin message from coordinator to signers +pub struct DkgBegin { + /// DKG round ID + pub dkg_id: u64, +} + +impl Signable for DkgBegin { + fn hash(&self, hasher: &mut Sha256) { + hasher.update("DKG_BEGIN".as_bytes()); + hasher.update(self.dkg_id.to_be_bytes()); + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +/// DKG public shares message from signer to all signers and coordinator +pub struct DkgPublicShares { + /// DKG round ID + pub dkg_id: u64, + /// Signer ID + pub signer_id: u32, + /// (party_id, commitment) + pub comms: Vec<(u32, PolyCommitment)>, +} + +impl Signable for DkgPublicShares { + fn hash(&self, hasher: &mut Sha256) { + hasher.update("DKG_PUBLIC_SHARES".as_bytes()); + hasher.update(self.dkg_id.to_be_bytes()); + hasher.update(self.signer_id.to_be_bytes()); + for (party_id, comm) in &self.comms { + hasher.update(party_id.to_be_bytes()); + for a in &comm.poly { + hasher.update(a.compress().as_bytes()); + } + } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +/// DKG private shares message from signer to all signers and coordinator +pub struct DkgPrivateShares { + /// DKG round ID + pub dkg_id: u64, + /// Signer ID + pub signer_id: u32, + /// Set of (src_key_id, (dst_key_id, encrypted_share)) + pub shares: Vec<(u32, HashMap>)>, +} + +impl Signable for DkgPrivateShares { + fn hash(&self, hasher: &mut Sha256) { + hasher.update("DKG_PRIVATE_SHARES".as_bytes()); + hasher.update(self.dkg_id.to_be_bytes()); + hasher.update(self.signer_id.to_be_bytes()); + // make sure we iterate sequentially + for (src_id, share) in &self.shares { + hasher.update(src_id.to_be_bytes()); + for dst_id in 0..share.len() as u32 { + hasher.update(dst_id.to_be_bytes()); + hasher.update(&share[&dst_id]); + } + } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +/// DKG end message from signers to coordinator +pub struct DkgEnd { + /// DKG round ID + pub dkg_id: u64, + /// Signer ID + pub signer_id: u32, + /// DKG status for this Signer after receiving public/private shares + pub status: DkgStatus, +} + +impl Signable for DkgEnd { + fn hash(&self, hasher: &mut Sha256) { + hasher.update("DKG_END".as_bytes()); + hasher.update(self.dkg_id.to_be_bytes()); + hasher.update(self.signer_id.to_be_bytes()); + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +/// Nonce request message from coordinator to signers +pub struct NonceRequest { + /// DKG round ID + pub dkg_id: u64, + /// Signing round ID + pub sign_id: u64, + /// Signing round iteration ID + pub sign_iter_id: u64, +} + +impl Signable for NonceRequest { + fn hash(&self, hasher: &mut Sha256) { + hasher.update("NONCE_REQUEST".as_bytes()); + hasher.update(self.dkg_id.to_be_bytes()); + hasher.update(self.sign_id.to_be_bytes()); + hasher.update(self.sign_iter_id.to_be_bytes()); + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +/// Nonce response message from signers to coordinator +pub struct NonceResponse { + /// DKG round ID + pub dkg_id: u64, + /// Signing round ID + pub sign_id: u64, + /// Signing round iteration ID + pub sign_iter_id: u64, + /// Signer ID + pub signer_id: u32, + /// Key IDs + pub key_ids: Vec, + /// Public nonces + pub nonces: Vec, +} + +impl Signable for NonceResponse { + fn hash(&self, hasher: &mut Sha256) { + hasher.update("NONCE_RESPONSE".as_bytes()); + hasher.update(self.dkg_id.to_be_bytes()); + hasher.update(self.sign_id.to_be_bytes()); + hasher.update(self.sign_iter_id.to_be_bytes()); + hasher.update(self.signer_id.to_be_bytes()); + + for key_id in &self.key_ids { + hasher.update(key_id.to_be_bytes()); + } + + for nonce in &self.nonces { + hasher.update(nonce.D.compress().as_bytes()); + hasher.update(nonce.E.compress().as_bytes()); + } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +/// Signature share request message from coordinator to signers +pub struct SignatureShareRequest { + /// DKG round ID + pub dkg_id: u64, + /// Signing round ID + pub sign_id: u64, + /// Signing round iteration ID + pub sign_iter_id: u64, + /// Nonces responses used for this signature + pub nonce_responses: Vec, + /// Bytes to sign + pub message: Vec, +} + +impl Signable for SignatureShareRequest { + fn hash(&self, hasher: &mut Sha256) { + hasher.update("SIGNATURE_SHARE_REQUEST".as_bytes()); + hasher.update(self.dkg_id.to_be_bytes()); + hasher.update(self.sign_id.to_be_bytes()); + + for nonce_response in &self.nonce_responses { + nonce_response.hash(hasher); + } + + hasher.update(self.message.as_slice()); + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +/// Signature share response message from signers to coordinator +pub struct SignatureShareResponse { + /// DKG round ID + pub dkg_id: u64, + /// Signing round ID + pub sign_id: u64, + /// Signing round iteration ID + pub sign_iter_id: u64, + /// Signer ID + pub signer_id: u32, + /// Signature shares from this Signer + pub signature_shares: Vec, +} + +impl Signable for SignatureShareResponse { + fn hash(&self, hasher: &mut Sha256) { + hasher.update("SIGNATURE_SHARE_RESPONSE".as_bytes()); + hasher.update(self.dkg_id.to_be_bytes()); + hasher.update(self.sign_id.to_be_bytes()); + hasher.update(self.signer_id.to_be_bytes()); + + for signature_share in &self.signature_shares { + hasher.update(signature_share.id.to_be_bytes()); + hasher.update(signature_share.z_i.to_bytes()); + } + } +} diff --git a/src/util.rs b/src/util.rs index d90fe4a3..14d9006f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,11 @@ -use p256k1::scalar::Scalar; +use aes_gcm::{aead::Aead, Aes256Gcm, Error as AesGcmError, KeyInit, Nonce}; +use p256k1::{point::Point, scalar::Scalar}; +use rand_core::{CryptoRng, RngCore}; use sha2::{Digest, Sha256}; +/// Size of the AES-GCM nonce +pub const AES_GCM_NONCE_SIZE: usize = 12; + #[allow(dead_code)] /// Digest the hasher to a Scalar pub fn hash_to_scalar(hasher: &mut Sha256) -> Scalar { @@ -11,3 +16,96 @@ pub fn hash_to_scalar(hasher: &mut Sha256) -> Scalar { Scalar::from(hash_bytes) } + +/// Do a Diffie-Hellman key exchange to create a shared secret from the passed private and public keys +pub fn make_shared_secret(private_key: &Scalar, public_key: &Point) -> [u8; 32] { + let mut hasher = Sha256::new(); + let shared_key = private_key * public_key; + + hasher.update("DH_SHARED_SECRET_KEY/".as_bytes()); + hasher.update(shared_key.compress().as_bytes()); + + let hash = hasher.finalize(); + let mut bytes = [0u8; 32]; + + bytes.clone_from_slice(hash.as_slice()); + bytes +} + +/// Encrypt the passed data using the key +pub fn encrypt( + key: &[u8; 32], + data: &[u8], + rng: &mut RNG, +) -> Result, AesGcmError> { + let mut nonce_bytes = [0u8; AES_GCM_NONCE_SIZE]; + + rng.fill_bytes(&mut nonce_bytes); + + let nonce_vec = nonce_bytes.to_vec(); + let nonce = Nonce::from_slice(&nonce_vec); + let cipher = Aes256Gcm::new(key.into()); + let cipher_vec = cipher.encrypt(nonce, data.to_vec().as_ref())?; + let mut bytes = Vec::new(); + + bytes.extend_from_slice(&nonce_vec); + bytes.extend_from_slice(&cipher_vec); + + Ok(bytes) +} + +/// Decrypt the passed data using the key +pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result, AesGcmError> { + let nonce_vec = data[..AES_GCM_NONCE_SIZE].to_vec(); + let cipher_vec = data[AES_GCM_NONCE_SIZE..].to_vec(); + let nonce = Nonce::from_slice(&nonce_vec); + let cipher = Aes256Gcm::new(key.into()); + + cipher.decrypt(nonce, cipher_vec.as_ref()) +} + +#[cfg(test)] +mod test { + use p256k1::{point::Point, scalar::Scalar}; + use rand_core::OsRng; + + use super::*; + + #[test] + #[allow(non_snake_case)] + fn test_shared_secret() { + let mut rng = OsRng; + + let x = Scalar::random(&mut rng); + let y = Scalar::random(&mut rng); + + let X = Point::from(x); + let Y = Point::from(y); + + let xy = make_shared_secret(&x, &Y); + let yx = make_shared_secret(&y, &X); + + assert_eq!(xy, yx); + } + + #[test] + #[allow(non_snake_case)] + fn test_encrypt_decrypt() { + let mut rng = OsRng; + let msg = "It was many and many a year ago, in a kingdom by the sea..."; + + let x = Scalar::random(&mut rng); + let y = Scalar::random(&mut rng); + + let X = Point::from(x); + let Y = Point::from(y); + + let xy = make_shared_secret(&x, &Y); + let yx = make_shared_secret(&y, &X); + + let cipher = encrypt(&xy, msg.as_bytes(), &mut rng).unwrap(); + let plain = decrypt(&yx, &cipher).unwrap(); + + assert_eq!(msg.as_bytes(), &plain); + } +}