From 43b7930b7a9f82da6a6d49d1323babeedad4297d Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 1 Oct 2024 20:41:03 -0500 Subject: [PATCH] sui-crypto: introduce multisig verifier and aggregator (#25) --- crates/sui-crypto/src/lib.rs | 17 + crates/sui-crypto/src/multisig.rs | 442 ++++++++++++++++++ crates/sui-crypto/src/zklogin/mod.rs | 10 + .../src/types/crypto/multisig.rs | 53 ++- .../sui-sdk-types/src/types/crypto/zklogin.rs | 8 + 5 files changed, 523 insertions(+), 7 deletions(-) create mode 100644 crates/sui-crypto/src/multisig.rs diff --git a/crates/sui-crypto/src/lib.rs b/crates/sui-crypto/src/lib.rs index 1bdd086c4..81a4b854b 100644 --- a/crates/sui-crypto/src/lib.rs +++ b/crates/sui-crypto/src/lib.rs @@ -44,6 +44,23 @@ pub mod zklogin; )] pub mod simple; +#[cfg(any( + feature = "ed25519", + feature = "secp256r1", + feature = "secp256k1", + feature = "zklogin" +))] +#[cfg_attr( + doc_cfg, + doc(cfg(any( + feature = "ed25519", + feature = "secp256r1", + feature = "secp256k1", + feature = "zklogin" + ))) +)] +pub mod multisig; + pub trait SuiSigner { fn sign_transaction(&self, transaction: &Transaction) -> Result; fn sign_personal_message( diff --git a/crates/sui-crypto/src/multisig.rs b/crates/sui-crypto/src/multisig.rs new file mode 100644 index 000000000..03f068596 --- /dev/null +++ b/crates/sui-crypto/src/multisig.rs @@ -0,0 +1,442 @@ +use crate::SignatureError; +use crate::SuiVerifier; +use crate::Verifier; +use sui_sdk_types::types::MultisigAggregatedSignature; +use sui_sdk_types::types::MultisigCommittee; +use sui_sdk_types::types::MultisigMemberPublicKey; +use sui_sdk_types::types::MultisigMemberSignature; +use sui_sdk_types::types::UserSignature; + +#[derive(Default)] +pub struct MultisigVerifier { + #[cfg(feature = "zklogin")] + zklogin_verifier: Option, +} + +impl MultisigVerifier { + pub fn new() -> Self { + Default::default() + } + + fn verify_member_signature( + &self, + message: &[u8], + member_public_key: &MultisigMemberPublicKey, + signature: &MultisigMemberSignature, + ) -> Result<(), SignatureError> { + match (member_public_key, signature) { + #[cfg(not(feature = "ed25519"))] + (MultisigMemberPublicKey::Ed25519(_), MultisigMemberSignature::Ed25519(_)) => Err( + SignatureError::from_source("support for ed25519 is not enabled"), + ), + #[cfg(feature = "ed25519")] + ( + MultisigMemberPublicKey::Ed25519(ed25519_public_key), + MultisigMemberSignature::Ed25519(ed25519_signature), + ) => crate::ed25519::Ed25519VerifyingKey::new(ed25519_public_key)? + .verify(message, ed25519_signature), + + #[cfg(not(feature = "secp256k1"))] + (MultisigMemberPublicKey::Secp256k1(_), MultisigMemberSignature::Secp256k1(_)) => Err( + SignatureError::from_source("support for secp256k1 is not enabled"), + ), + #[cfg(feature = "secp256k1")] + ( + MultisigMemberPublicKey::Secp256k1(k1_public_key), + MultisigMemberSignature::Secp256k1(k1_signature), + ) => crate::secp256k1::Secp256k1VerifyingKey::new(k1_public_key)? + .verify(message, k1_signature), + + #[cfg(not(feature = "secp256r1"))] + (MultisigMemberPublicKey::Secp256r1(_), MultisigMemberSignature::Secp256r1(_)) => Err( + SignatureError::from_source("support for secp256r1 is not enabled"), + ), + #[cfg(feature = "secp256r1")] + ( + MultisigMemberPublicKey::Secp256r1(r1_public_key), + MultisigMemberSignature::Secp256r1(r1_signature), + ) => crate::secp256r1::Secp256r1VerifyingKey::new(r1_public_key)? + .verify(message, r1_signature), + + #[cfg(not(feature = "zklogin"))] + (MultisigMemberPublicKey::ZkLogin(_), MultisigMemberSignature::ZkLogin(_)) => Err( + SignatureError::from_source("support for zklogin is not enabled"), + ), + #[cfg(feature = "zklogin")] + ( + MultisigMemberPublicKey::ZkLogin(zklogin_identifier), + MultisigMemberSignature::ZkLogin(zklogin_authenticator), + ) => { + let zklogin_verifier = self + .zklogin_verifier() + .ok_or_else(|| SignatureError::from_source("no zklogin verifier provided"))?; + + // verify that the member identifier and the authenticator match + if zklogin_identifier + != &crate::zklogin::zklogin_identifier_from_inputs( + &zklogin_authenticator.inputs, + )? + { + return Err(SignatureError::from_source( + "member zklogin identifier does not match signature", + )); + } + + zklogin_verifier.verify(message, zklogin_authenticator.as_ref()) + } + + _ => Err(SignatureError::from_source( + "member and signature scheme do not match", + )), + } + } +} + +#[cfg(feature = "zklogin")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "zklogin")))] +impl MultisigVerifier { + pub fn with_zklogin_verifier(&mut self, zklogin_verifier: crate::zklogin::ZkloginVerifier) { + self.zklogin_verifier = Some(zklogin_verifier); + } + + pub fn zklogin_verifier(&self) -> Option<&crate::zklogin::ZkloginVerifier> { + self.zklogin_verifier.as_ref() + } + + pub fn zklogin_verifier_mut(&mut self) -> Option<&mut crate::zklogin::ZkloginVerifier> { + self.zklogin_verifier.as_mut() + } +} + +impl Verifier for MultisigVerifier { + fn verify( + &self, + message: &[u8], + signature: &MultisigAggregatedSignature, + ) -> Result<(), SignatureError> { + if !signature.committee().is_valid() { + return Err(SignatureError::from_source("invalid MultisigCommittee")); + } + + if signature.signatures().len() != signature.bitmap().count_ones() as usize { + return Err(SignatureError::from_source( + "number of signatures does not match bitmap", + )); + } + + if signature.signatures().len() > signature.committee().members().len() { + return Err(SignatureError::from_source( + "more signatures than committee members", + )); + } + + let weight = BitmapIndices::new(signature.bitmap()) + .map(|member_idx| { + signature + .committee() + .members() + .get(member_idx as usize) + .ok_or_else(|| SignatureError::from_source("invalid bitmap")) + }) + .zip(signature.signatures()) + .map(|(maybe_member, signature)| { + let member = maybe_member?; + self.verify_member_signature(message, member.public_key(), signature) + .map(|()| member.weight() as u16) + }) + .sum::>()?; + + if weight >= signature.committee().threshold() { + Ok(()) + } else { + Err(SignatureError::from_source( + "signature weight does not exceed threshold", + )) + } + } +} + +impl Verifier for MultisigVerifier { + fn verify(&self, message: &[u8], signature: &UserSignature) -> Result<(), SignatureError> { + let UserSignature::Multisig(signature) = signature else { + return Err(SignatureError::from_source("not a multisig signature")); + }; + + self.verify(message, signature) + } +} + +impl SuiVerifier for MultisigVerifier { + fn verify_transaction( + &self, + transaction: &sui_sdk_types::types::Transaction, + signature: &UserSignature, + ) -> Result<(), SignatureError> { + let message = transaction.signing_digest(); + self.verify(&message, signature) + } + + fn verify_personal_message( + &self, + message: &sui_sdk_types::types::PersonalMessage<'_>, + signature: &UserSignature, + ) -> Result<(), SignatureError> { + let message = message.signing_digest(); + self.verify(&message, signature) + } +} + +/// Interpret a bitmap of 01s as a list of indices that is set to 1s. +/// e.g. 22 = 0b10110, then the result is [1, 2, 4]. +struct BitmapIndices { + bitmap: u16, + range: std::ops::Range, +} + +impl BitmapIndices { + pub fn new(bitmap: u16) -> Self { + Self { + bitmap, + range: 0..(u16::BITS as u8), + } + } +} + +impl Iterator for BitmapIndices { + type Item = u8; + + fn next(&mut self) -> Option { + #[allow(clippy::while_let_on_iterator)] + while let Some(i) = self.range.next() { + if self.bitmap & (1 << i) != 0 { + return Some(i); + } + } + + None + } +} + +/// Verifier that will verify all UserSignature variants +#[derive(Default)] +pub struct UserSignatureVerifier { + inner: MultisigVerifier, +} + +impl UserSignatureVerifier { + pub fn new() -> Self { + Default::default() + } +} + +#[cfg(feature = "zklogin")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "zklogin")))] +impl UserSignatureVerifier { + pub fn with_zklogin_verifier(&mut self, zklogin_verifier: crate::zklogin::ZkloginVerifier) { + self.inner.with_zklogin_verifier(zklogin_verifier); + } + + pub fn zklogin_verifier(&self) -> Option<&crate::zklogin::ZkloginVerifier> { + self.inner.zklogin_verifier() + } + + pub fn zklogin_verifier_mut(&mut self) -> Option<&mut crate::zklogin::ZkloginVerifier> { + self.inner.zklogin_verifier_mut() + } +} + +impl Verifier for UserSignatureVerifier { + fn verify(&self, message: &[u8], signature: &UserSignature) -> Result<(), SignatureError> { + match signature { + UserSignature::Simple(simple_signature) => { + crate::simple::SimpleVerifier.verify(message, simple_signature) + } + UserSignature::Multisig(multisig) => self.inner.verify(message, multisig), + + #[cfg(not(feature = "zklogin"))] + UserSignature::ZkLogin(_) => Err(SignatureError::from_source( + "support for zklogin is not enabled", + )), + #[cfg(feature = "zklogin")] + UserSignature::ZkLogin(zklogin_authenticator) => { + let zklogin_verifier = self + .zklogin_verifier() + .ok_or_else(|| SignatureError::from_source("no zklogin verifier provided"))?; + + zklogin_verifier.verify(message, zklogin_authenticator.as_ref()) + } + + UserSignature::Passkey(_) => Err(SignatureError::from_source( + "unsupported user signature scheme", + )), + } + } +} + +impl SuiVerifier for UserSignatureVerifier { + fn verify_transaction( + &self, + transaction: &sui_sdk_types::types::Transaction, + signature: &UserSignature, + ) -> Result<(), SignatureError> { + let message = transaction.signing_digest(); + self.verify(&message, signature) + } + + fn verify_personal_message( + &self, + message: &sui_sdk_types::types::PersonalMessage<'_>, + signature: &UserSignature, + ) -> Result<(), SignatureError> { + let message = message.signing_digest(); + self.verify(&message, signature) + } +} + +pub struct MultisigAggregator { + committee: MultisigCommittee, + signatures: std::collections::BTreeMap, + signed_weight: u16, + message: Vec, + verifier: MultisigVerifier, +} + +impl MultisigAggregator { + pub fn new_with_transaction( + committee: MultisigCommittee, + transaction: &sui_sdk_types::types::Transaction, + ) -> Self { + Self { + committee, + signatures: Default::default(), + signed_weight: 0, + message: transaction.signing_digest().to_vec(), + verifier: Default::default(), + } + } + + pub fn new_with_message( + committee: MultisigCommittee, + message: &sui_sdk_types::types::PersonalMessage<'_>, + ) -> Self { + Self { + committee, + signatures: Default::default(), + signed_weight: 0, + message: message.signing_digest().to_vec(), + verifier: Default::default(), + } + } + + pub fn verifier(&self) -> &MultisigVerifier { + &self.verifier + } + + pub fn verifier_mut(&mut self) -> &mut MultisigVerifier { + &mut self.verifier + } + + pub fn add_signature(&mut self, signature: UserSignature) -> Result<(), SignatureError> { + use std::collections::btree_map::Entry; + + let (public_key, signature) = multisig_pubkey_and_signature_from_user_signature(signature)?; + let member_idx = self + .committee + .members() + .iter() + .position(|member| member.public_key() == &public_key) + .ok_or_else(|| { + SignatureError::from_source( + "provided signature does not belong to committee member", + ) + })?; + + self.verifier() + .verify_member_signature(&self.message, &public_key, &signature)?; + + match self.signatures.entry(member_idx) { + Entry::Vacant(v) => { + v.insert(signature); + } + Entry::Occupied(_) => { + return Err(SignatureError::from_source( + "duplicate signature from same committee member", + )) + } + } + + self.signed_weight += self.committee.members()[member_idx].weight() as u16; + + Ok(()) + } + + pub fn finish(&mut self) -> Result { + if self.signed_weight < self.committee.threshold() { + return Err(SignatureError::from_source( + "insufficient signature weight to reach threshold", + )); + } + + let (signatures, bitmap) = self.signatures.clone().into_iter().fold( + (Vec::new(), 0), + |(mut signatures, mut bitmap), (member_idx, signature)| { + bitmap |= 1 << member_idx; + signatures.push(signature); + (signatures, bitmap) + }, + ); + + Ok(MultisigAggregatedSignature::new( + self.committee.clone(), + signatures, + bitmap, + )) + } +} + +fn multisig_pubkey_and_signature_from_user_signature( + signature: UserSignature, +) -> Result<(MultisigMemberPublicKey, MultisigMemberSignature), SignatureError> { + use sui_sdk_types::types::SimpleSignature; + match signature { + UserSignature::Simple(SimpleSignature::Ed25519 { + signature, + public_key, + }) => Ok(( + MultisigMemberPublicKey::Ed25519(public_key), + MultisigMemberSignature::Ed25519(signature), + )), + UserSignature::Simple(SimpleSignature::Secp256k1 { + signature, + public_key, + }) => Ok(( + MultisigMemberPublicKey::Secp256k1(public_key), + MultisigMemberSignature::Secp256k1(signature), + )), + UserSignature::Simple(SimpleSignature::Secp256r1 { + signature, + public_key, + }) => Ok(( + MultisigMemberPublicKey::Secp256r1(public_key), + MultisigMemberSignature::Secp256r1(signature), + )), + + #[cfg(not(feature = "zklogin"))] + UserSignature::ZkLogin(_) => Err(SignatureError::from_source( + "support for zklogin is not enabled", + )), + #[cfg(feature = "zklogin")] + UserSignature::ZkLogin(zklogin_authenticator) => { + let zklogin_identifier = + crate::zklogin::zklogin_identifier_from_inputs(&zklogin_authenticator.inputs)?; + Ok(( + MultisigMemberPublicKey::ZkLogin(zklogin_identifier), + MultisigMemberSignature::ZkLogin(zklogin_authenticator), + )) + } + + UserSignature::Multisig(_) | UserSignature::Passkey(_) => { + Err(SignatureError::from_source("invalid siganture scheme")) + } + } +} diff --git a/crates/sui-crypto/src/zklogin/mod.rs b/crates/sui-crypto/src/zklogin/mod.rs index b0a5e77b7..aa28c268a 100644 --- a/crates/sui-crypto/src/zklogin/mod.rs +++ b/crates/sui-crypto/src/zklogin/mod.rs @@ -263,3 +263,13 @@ fn verify_extended_claim(claim: &Claim, expected_key: &str) -> Result Result { + const ISS: &str = "iss"; + + let iss = verify_extended_claim(&inputs.iss_base64_details, ISS)?; + sui_sdk_types::types::ZkLoginPublicIdentifier::new(iss, inputs.address_seed.clone()) + .ok_or_else(|| SignatureError::from_source("invalid iss")) +} diff --git a/crates/sui-sdk-types/src/types/crypto/multisig.rs b/crates/sui-sdk-types/src/types/crypto/multisig.rs index 1f6e38007..f5ddc9ecb 100644 --- a/crates/sui-sdk-types/src/types/crypto/multisig.rs +++ b/crates/sui-sdk-types/src/types/crypto/multisig.rs @@ -12,7 +12,6 @@ pub type WeightUnit = u8; pub type ThresholdUnit = u16; pub type BitmapUnit = u16; -#[cfg(feature = "serde")] const MAX_COMMITTEE_SIZE: usize = 10; // TODO validate sigs // const MAX_BITMAP_VALUE: BitmapUnit = 0b1111111111; @@ -75,6 +74,34 @@ impl MultisigCommittee { pub fn scheme(&self) -> SignatureScheme { SignatureScheme::Multisig } + + /// Checks if the Committee is valid. + /// + /// A valid committee is one that: + /// - Has a nonzero threshold + /// - Has at least one member + /// - Has at most ten members + /// - No member has weight 0 + /// - the sum of the weights of all members must be larger than the threshold + /// - contains no duplicate members + pub fn is_valid(&self) -> bool { + self.threshold != 0 + && !self.members.is_empty() + && self.members.len() <= MAX_COMMITTEE_SIZE + && !self.members.iter().any(|member| member.weight == 0) + && self + .members + .iter() + .map(|member| member.weight as ThresholdUnit) + .sum::() + >= self.threshold + && !self.members.iter().enumerate().any(|(i, member)| { + self.members + .iter() + .skip(i + 1) + .any(|m| member.public_key == m.public_key) + }) + } } /// The struct that contains signatures and public keys necessary for authenticating a Multisig. @@ -83,6 +110,8 @@ impl MultisigCommittee { #[cfg_attr(test, derive(test_strategy::Arbitrary))] pub struct MultisigAggregatedSignature { /// The plain signature encoded with signature scheme. + /// + /// The signatures must be in the same order as they are listed in the committee. #[cfg_attr(test, any(proptest::collection::size_range(0..=10).lift()))] signatures: Vec, /// A bitmap that indicates the position of which public key the signature should be authenticated with. @@ -103,6 +132,19 @@ pub struct MultisigAggregatedSignature { } impl MultisigAggregatedSignature { + pub fn new( + committee: MultisigCommittee, + signatures: Vec, + bitmap: BitmapUnit, + ) -> Self { + Self { + signatures, + bitmap, + legacy_bitmap: None, + committee, + } + } + pub fn signatures(&self) -> &[MultisigMemberSignature] { &self.signatures } @@ -146,12 +188,11 @@ fn roaring_bitmap_to_u16(roaring: &roaring::RoaringBitmap) -> Result), } #[cfg(feature = "serde")] @@ -577,24 +618,22 @@ mod serialization { } #[derive(serde_derive::Serialize, serde_derive::Deserialize)] - #[allow(clippy::large_enum_variant)] enum MemberSignature { Ed25519(Ed25519Signature), Secp256k1(Secp256k1Signature), Secp256r1(Secp256r1Signature), - ZkLogin(ZkLoginAuthenticator), + ZkLogin(Box), } #[derive(serde_derive::Serialize, serde_derive::Deserialize)] #[serde(tag = "scheme", rename_all = "lowercase")] #[serde(rename = "MultisigMemberSignature")] - #[allow(clippy::large_enum_variant)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] enum ReadableMemberSignature { Ed25519 { signature: Ed25519Signature }, Secp256k1 { signature: Secp256k1Signature }, Secp256r1 { signature: Secp256r1Signature }, - ZkLogin(ZkLoginAuthenticator), + ZkLogin(Box), } #[cfg(feature = "schemars")] diff --git a/crates/sui-sdk-types/src/types/crypto/zklogin.rs b/crates/sui-sdk-types/src/types/crypto/zklogin.rs index 22c5eef5a..4b3247e22 100644 --- a/crates/sui-sdk-types/src/types/crypto/zklogin.rs +++ b/crates/sui-sdk-types/src/types/crypto/zklogin.rs @@ -82,6 +82,14 @@ pub struct ZkLoginPublicIdentifier { } impl ZkLoginPublicIdentifier { + pub fn new(iss: String, address_seed: Bn254FieldElement) -> Option { + if iss.len() > 255 { + None + } else { + Some(Self { iss, address_seed }) + } + } + pub fn iss(&self) -> &str { &self.iss }