diff --git a/xmtp_id/Cargo.toml b/xmtp_id/Cargo.toml index 6191d0fc6..d8d01864b 100644 --- a/xmtp_id/Cargo.toml +++ b/xmtp_id/Cargo.toml @@ -1,27 +1,26 @@ [package] +edition = "2021" name = "xmtp_id" version = "0.1.0" -edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait.workspace = true +chrono.workspace = true +futures.workspace = true +hex.workspace = true log.workspace = true -tracing.workspace = true -thiserror.workspace = true -xmtp_cryptography.workspace = true -xmtp_mls.workspace = true -xmtp_proto.workspace = true -openmls_traits.workspace = true openmls.workspace = true openmls_basic_credential.workspace = true openmls_rust_crypto.workspace = true +openmls_traits.workspace = true prost.workspace = true -chrono.workspace = true +rand.workspace = true serde.workspace = true -async-trait.workspace = true -futures.workspace = true sha2 = "0.10.8" -rand.workspace = true -hex.workspace = true - +thiserror.workspace = true +tracing.workspace = true +xmtp_cryptography.workspace = true +xmtp_mls.workspace = true +xmtp_proto.workspace = true diff --git a/xmtp_id/src/associations/association_log.rs b/xmtp_id/src/associations/association_log.rs index d0c8dde72..68d64c67a 100644 --- a/xmtp_id/src/associations/association_log.rs +++ b/xmtp_id/src/associations/association_log.rs @@ -1,9 +1,13 @@ use super::hashes::generate_inbox_id; use super::member::{Member, MemberIdentifier, MemberKind}; +use super::serialization::{ + from_identity_update_proto, to_identity_update_proto, DeserializationError, SerializationError, +}; use super::signature::{Signature, SignatureError, SignatureKind}; use super::state::AssociationState; use thiserror::Error; +use xmtp_proto::xmtp::identity::associations::IdentityUpdate as IdentityUpdateProto; #[derive(Debug, Error, PartialEq)] pub enum AssociationError { @@ -23,6 +27,8 @@ pub enum AssociationError { LegacySignatureReuse, #[error("The new member identifier does not match the signer")] NewMemberIdSignatureMismatch, + #[error("Wrong inbox_id specified on association")] + WrongInboxId, #[error("Signature not allowed for role {0:?} {1:?}")] SignatureNotAllowed(String, String), #[error("Replay detected")] @@ -90,6 +96,7 @@ impl IdentityAction for CreateInbox { /// AddAssociation Action pub struct AddAssociation { + pub inbox_id: String, pub new_member_signature: Box, pub new_member_identifier: MemberIdentifier, pub existing_member_signature: Box, @@ -102,6 +109,7 @@ impl IdentityAction for AddAssociation { ) -> Result { let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?; self.replay_check(&existing_state)?; + ensure_matching_inbox_id(&self.inbox_id, existing_state.inbox_id())?; // Validate the new member signature and get the recovered signer let new_member_address = self.new_member_signature.recover_signer()?; @@ -186,6 +194,7 @@ impl IdentityAction for AddAssociation { /// RevokeAssociation Action pub struct RevokeAssociation { + pub inbox_id: String, pub recovery_address_signature: Box, pub revoked_member: MemberIdentifier, } @@ -197,6 +206,7 @@ impl IdentityAction for RevokeAssociation { ) -> Result { let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?; self.replay_check(&existing_state)?; + ensure_matching_inbox_id(&self.inbox_id, existing_state.inbox_id())?; if is_legacy_signature(&self.recovery_address_signature) { return Err(AssociationError::SignatureNotAllowed( @@ -238,6 +248,7 @@ impl IdentityAction for RevokeAssociation { /// ChangeRecoveryAddress Action pub struct ChangeRecoveryAddress { + pub inbox_id: String, pub recovery_address_signature: Box, pub new_recovery_address: String, } @@ -249,6 +260,7 @@ impl IdentityAction for ChangeRecoveryAddress { ) -> Result { let existing_state = existing_state.ok_or(AssociationError::NotCreated)?; self.replay_check(&existing_state)?; + ensure_matching_inbox_id(&self.inbox_id, existing_state.inbox_id())?; if is_legacy_signature(&self.recovery_address_signature) { return Err(AssociationError::SignatureNotAllowed( @@ -314,6 +326,14 @@ impl IdentityUpdate { client_timestamp_ns, } } + + pub fn to_proto(&self) -> Result { + to_identity_update_proto(self) + } + + pub fn from_proto(proto: IdentityUpdateProto) -> Result { + from_identity_update_proto(proto) + } } impl IdentityAction for IdentityUpdate { @@ -363,6 +383,17 @@ fn allowed_association( Ok(()) } +fn ensure_matching_inbox_id( + action_inbox_id: &String, + state_inbox_id: &String, +) -> Result<(), AssociationError> { + if action_inbox_id.ne(state_inbox_id) { + return Err(AssociationError::WrongInboxId); + } + + Ok(()) +} + // Ensure that the type of signature matches the new entity's role. fn allowed_signature_for_kind( role: &MemberKind, diff --git a/xmtp_id/src/associations/builder.rs b/xmtp_id/src/associations/builder.rs index 90d63a9b3..855dc6231 100644 --- a/xmtp_id/src/associations/builder.rs +++ b/xmtp_id/src/associations/builder.rs @@ -278,6 +278,7 @@ fn build_action( .ok_or(SignatureRequestError::MissingSigner)?; Ok(Action::AddAssociation(AddAssociation { + inbox_id: unsigned_action.inbox_id, new_member_identifier: unsigned_action.new_member_identifier, existing_member_signature, new_member_signature, @@ -294,6 +295,7 @@ fn build_action( .ok_or(SignatureRequestError::MissingSigner)?; Ok(Action::RevokeAssociation(RevokeAssociation { + inbox_id: unsigned_action.inbox_id, recovery_address_signature, revoked_member: unsigned_action.revoked_member, })) @@ -310,6 +312,7 @@ fn build_action( .ok_or(SignatureRequestError::MissingSigner)?; Ok(Action::ChangeRecoveryAddress(ChangeRecoveryAddress { + inbox_id: unsigned_action.inbox_id, recovery_address_signature, new_recovery_address: unsigned_action.new_recovery_address, })) diff --git a/xmtp_id/src/associations/mod.rs b/xmtp_id/src/associations/mod.rs index 67fb2fefe..212d51431 100644 --- a/xmtp_id/src/associations/mod.rs +++ b/xmtp_id/src/associations/mod.rs @@ -37,6 +37,8 @@ pub fn get_state(updates: Vec) -> Result Self { let signer = rand_string(); return Self { + inbox_id: rand_string(), recovery_address_signature: MockSignature::new_boxed( true, signer.into(), @@ -114,6 +118,7 @@ mod tests { initial_state.recovery_address().clone().into(); let update = Action::AddAssociation(AddAssociation { + inbox_id: initial_state.inbox_id().clone(), existing_member_signature: MockSignature::new_boxed( true, initial_wallet_address.clone(), @@ -145,6 +150,7 @@ mod tests { let first_member: MemberIdentifier = initial_state.recovery_address().clone().into(); let update = Action::AddAssociation(AddAssociation { + inbox_id: initial_state.inbox_id().clone(), new_member_identifier: new_installation_identifier.clone(), new_member_signature: MockSignature::new_boxed( true, @@ -175,6 +181,7 @@ mod tests { let account_address = create_action.account_address.clone(); let new_member_identifier: MemberIdentifier = rand_vec().into(); let add_action = AddAssociation { + inbox_id: generate_inbox_id(&account_address, &create_action.nonce), existing_member_signature: MockSignature::new_boxed( true, account_address.clone().into(), @@ -224,6 +231,7 @@ mod tests { // The legacy key can only be used once. After this, subsequent updates should fail let update = Action::AddAssociation(AddAssociation { + inbox_id: state.inbox_id().clone(), existing_member_signature: MockSignature::new_boxed( true, member_identifier, @@ -250,6 +258,7 @@ mod tests { let new_wallet_address: MemberIdentifier = rand_string().into(); let add_association = Action::AddAssociation(AddAssociation { + inbox_id: initial_state.inbox_id().clone(), new_member_identifier: new_wallet_address.clone(), new_member_signature: MockSignature::new_boxed( true, @@ -296,10 +305,12 @@ mod tests { #[test] fn reject_invalid_signature_on_update() { let initial_state = new_test_inbox(); + let inbox_id = initial_state.inbox_id().clone(); let bad_signature = MockSignature::new_boxed(false, rand_string().into(), SignatureKind::Erc191, None); let update_with_bad_existing_member = Action::AddAssociation(AddAssociation { + inbox_id: inbox_id.clone(), existing_member_signature: bad_signature.clone(), ..Default::default() }); @@ -315,6 +326,7 @@ mod tests { ); let update_with_bad_new_member = Action::AddAssociation(AddAssociation { + inbox_id: inbox_id.clone(), new_member_signature: bad_signature.clone(), existing_member_signature: MockSignature::new_boxed( true, @@ -338,9 +350,12 @@ mod tests { #[test] fn reject_if_signer_not_existing_member() { - let create_request = Action::CreateInbox(CreateInbox::default()); + let create_inbox = CreateInbox::default(); + let inbox_id = generate_inbox_id(&create_inbox.account_address, &create_inbox.nonce); + let create_request = Action::CreateInbox(create_inbox); // The default here will create an AddAssociation from a random wallet let update = Action::AddAssociation(AddAssociation { + inbox_id, // Existing member signature is coming from a random wallet existing_member_signature: MockSignature::new_boxed( true, @@ -367,6 +382,7 @@ mod tests { let new_installation_id: MemberIdentifier = rand_vec().into(); let update = Action::AddAssociation(AddAssociation { + inbox_id: existing_state.inbox_id().clone(), existing_member_signature: MockSignature::new_boxed( true, existing_installation.identifier.clone(), @@ -404,6 +420,7 @@ mod tests { .unwrap() .identifier; let update = Action::RevokeAssociation(RevokeAssociation { + inbox_id: initial_state.inbox_id().clone(), recovery_address_signature: MockSignature::new_boxed( true, initial_state.recovery_address().clone().into(), @@ -430,6 +447,7 @@ mod tests { .identifier; let add_second_installation = Action::AddAssociation(AddAssociation { + inbox_id: initial_state.inbox_id().clone(), existing_member_signature: MockSignature::new_boxed( true, wallet_address.clone(), @@ -447,6 +465,7 @@ mod tests { assert_eq!(new_state.members().len(), 3); let revocation = Action::RevokeAssociation(RevokeAssociation { + inbox_id: new_state.inbox_id().clone(), recovery_address_signature: MockSignature::new_boxed( true, wallet_address.clone(), @@ -475,6 +494,7 @@ mod tests { let second_wallet_address: MemberIdentifier = rand_string().into(); let add_second_wallet = Action::AddAssociation(AddAssociation { + inbox_id: initial_state.inbox_id().clone(), new_member_identifier: second_wallet_address.clone(), new_member_signature: MockSignature::new_boxed( true, @@ -492,6 +512,7 @@ mod tests { }); let revoke_second_wallet = Action::RevokeAssociation(RevokeAssociation { + inbox_id: initial_state.inbox_id().clone(), recovery_address_signature: MockSignature::new_boxed( true, wallet_address.clone(), @@ -510,6 +531,7 @@ mod tests { assert_eq!(state_after_remove.members().len(), 1); let add_second_wallet_again = Action::AddAssociation(AddAssociation { + inbox_id: state_after_remove.inbox_id().clone(), new_member_identifier: second_wallet_address.clone(), new_member_signature: MockSignature::new_boxed( true, @@ -541,6 +563,7 @@ mod tests { initial_state.recovery_address().clone().into(); let new_recovery_address = rand_string(); let update_recovery = Action::ChangeRecoveryAddress(ChangeRecoveryAddress { + inbox_id: initial_state.inbox_id().clone(), new_recovery_address: new_recovery_address.clone(), recovery_address_signature: MockSignature::new_boxed( true, @@ -558,6 +581,7 @@ mod tests { assert_eq!(new_state.recovery_address(), &new_recovery_address); let attempted_revoke = Action::RevokeAssociation(RevokeAssociation { + inbox_id: new_state.inbox_id().clone(), recovery_address_signature: MockSignature::new_boxed( true, initial_recovery_address.clone(), diff --git a/xmtp_id/src/associations/serialization.rs b/xmtp_id/src/associations/serialization.rs index eee708855..cc4eb02f2 100644 --- a/xmtp_id/src/associations/serialization.rs +++ b/xmtp_id/src/associations/serialization.rs @@ -1,79 +1,150 @@ use super::{ + association_log::{ + Action, AddAssociation, ChangeRecoveryAddress, CreateInbox, RevokeAssociation, + }, + signature::{ + Erc1271Signature, InstallationKeySignature, LegacyDelegatedSignature, + RecoverableEcdsaSignature, + }, unsigned_actions::{ SignatureTextCreator, UnsignedAction, UnsignedAddAssociation, UnsignedChangeRecoveryAddress, UnsignedCreateInbox, UnsignedIdentityUpdate, UnsignedRevokeAssociation, }, - IdentityUpdate, MemberIdentifier, + IdentityUpdate, MemberIdentifier, Signature, }; use thiserror::Error; use xmtp_proto::xmtp::identity::associations::{ - identity_action::Kind as IdentityActionKind, - member_identifier::Kind as MemberIdentifierKindProto, IdentityAction as IdentityActionProto, - IdentityUpdate as IdentityUpdateProto, MemberIdentifier as MemberIdentifierProto, + identity_action::Kind as IdentityActionKindProto, + member_identifier::Kind as MemberIdentifierKindProto, + signature::Signature as SignatureKindProto, AddAssociation as AddAssociationProto, + ChangeRecoveryAddress as ChangeRecoveryAddressProto, CreateInbox as CreateInboxProto, + IdentityAction as IdentityActionProto, IdentityUpdate as IdentityUpdateProto, + MemberIdentifier as MemberIdentifierProto, RevokeAssociation as RevokeAssociationProto, + Signature as SignatureWrapperProto, }; #[derive(Error, Debug)] -pub enum SerializationError { - #[error("Invalid action")] - InvalidAction, +pub enum DeserializationError { #[error("Missing action")] MissingAction, #[error("Missing member identifier")] MissingMemberIdentifier, + #[error("Missing signature")] + MissingSignature, } pub fn from_identity_update_proto( proto: IdentityUpdateProto, -) -> Result { +) -> Result { let client_timestamp_ns = proto.client_timestamp_ns; - let all_actions: Vec = proto + let all_actions = proto .actions .into_iter() .map(|action| match action.kind { Some(action) => Ok(action), - None => Err(SerializationError::MissingAction), + None => Err(DeserializationError::MissingAction), }) - .collect()?; + .collect::, DeserializationError>>()?; + + let signature_text = get_signature_text(&all_actions, client_timestamp_ns)?; + + let processed_actions: Vec = all_actions + .into_iter() + .map(|action| match action { + IdentityActionKindProto::Add(add_action) => { + Ok(Action::AddAssociation(AddAssociation { + inbox_id: add_action.inbox_id, + new_member_signature: from_signature_proto_option( + add_action.new_member_signature, + signature_text.clone(), + )?, + existing_member_signature: from_signature_proto_option( + add_action.existing_member_signature, + signature_text.clone(), + )?, + new_member_identifier: from_member_identifier_proto_option( + add_action.new_member_identifier, + )?, + })) + } + IdentityActionKindProto::CreateInbox(create_inbox_action) => { + Ok(Action::CreateInbox(CreateInbox { + nonce: create_inbox_action.nonce, + account_address: create_inbox_action.initial_address, + initial_address_signature: from_signature_proto_option( + create_inbox_action.initial_address_signature, + signature_text.clone(), + )?, + })) + } + IdentityActionKindProto::ChangeRecoveryAddress(change_recovery_address_action) => { + Ok(Action::ChangeRecoveryAddress(ChangeRecoveryAddress { + inbox_id: change_recovery_address_action.inbox_id, + new_recovery_address: change_recovery_address_action.new_recovery_address, + recovery_address_signature: from_signature_proto_option( + change_recovery_address_action.existing_recovery_address_signature, + signature_text.clone(), + )?, + })) + } + IdentityActionKindProto::Revoke(revoke_action) => { + Ok(Action::RevokeAssociation(RevokeAssociation { + inbox_id: revoke_action.inbox_id, + revoked_member: from_member_identifier_proto_option( + revoke_action.member_to_revoke, + )?, + recovery_address_signature: from_signature_proto_option( + revoke_action.recovery_address_signature, + signature_text.clone(), + )?, + })) + } + }) + .collect::, DeserializationError>>()?; + + Ok(IdentityUpdate::new(processed_actions, client_timestamp_ns)) } fn get_signature_text( - actions: &Vec, + actions: &Vec, client_timestamp_ns: u64, -) -> Result { +) -> Result { let unsigned_actions: Vec = actions .iter() .map(|action| match action { - IdentityActionKind::Add(add_action) => { + IdentityActionKindProto::Add(add_action) => { Ok(UnsignedAction::AddAssociation(UnsignedAddAssociation { - inbox_id: add_action.inbox_id, + inbox_id: add_action.inbox_id.clone(), new_member_identifier: from_member_identifier_proto_option( - add_action.new_member_identifier, + add_action.new_member_identifier.clone(), )?, })) } - IdentityActionKind::CreateInbox(create_inbox_action) => { + IdentityActionKindProto::CreateInbox(create_inbox_action) => { Ok(UnsignedAction::CreateInbox(UnsignedCreateInbox { - nonce: create_inbox_action.nonce as u64, - account_address: create_inbox_action.initial_address, + nonce: create_inbox_action.nonce, + account_address: create_inbox_action.initial_address.clone(), })) } - IdentityActionKind::ChangeRecoveryAddress(change_recovery_address_action) => Ok( + IdentityActionKindProto::ChangeRecoveryAddress(change_recovery_address_action) => Ok( UnsignedAction::ChangeRecoveryAddress(UnsignedChangeRecoveryAddress { - inbox_id: change_recovery_address_action.inbox_id, - new_recovery_address: change_recovery_address_action.new_recovery_address, + inbox_id: change_recovery_address_action.inbox_id.clone(), + new_recovery_address: change_recovery_address_action + .new_recovery_address + .clone(), }), ), - IdentityActionKind::Revoke(revoke_action) => Ok(UnsignedAction::RevokeAssociation( - UnsignedRevokeAssociation { - inbox_id: revoke_action.inbox_id, + IdentityActionKindProto::Revoke(revoke_action) => Ok( + UnsignedAction::RevokeAssociation(UnsignedRevokeAssociation { + inbox_id: revoke_action.inbox_id.clone(), revoked_member: from_member_identifier_proto_option( - revoke_action.member_to_revoke, + revoke_action.member_to_revoke.clone(), )?, - }, - )), + }), + ), }) - .collect::, SerializationError>>()?; + .collect::, DeserializationError>>()?; let unsigned_update = UnsignedIdentityUpdate::new(client_timestamp_ns, unsigned_actions); @@ -82,12 +153,12 @@ fn get_signature_text( fn from_member_identifier_proto_option( proto: Option, -) -> Result { +) -> Result { match proto { - None => return Err(SerializationError::MissingMemberIdentifier), + None => return Err(DeserializationError::MissingMemberIdentifier), Some(identifier_proto) => match identifier_proto.kind { Some(identifier) => Ok(from_member_identifier_kind_proto(identifier)), - None => Err(SerializationError::MissingMemberIdentifier), + None => Err(DeserializationError::MissingMemberIdentifier), }, } } @@ -98,3 +169,177 @@ fn from_member_identifier_kind_proto(proto: MemberIdentifierKindProto) -> Member MemberIdentifierKindProto::InstallationPublicKey(public_key) => public_key.into(), } } + +fn from_signature_proto_option( + proto: Option, + signature_text: String, +) -> Result, DeserializationError> { + match proto { + None => return Err(DeserializationError::MissingSignature), + Some(signature_proto) => match signature_proto.signature { + Some(signature) => Ok(from_signature_kind_proto(signature, signature_text)?), + None => Err(DeserializationError::MissingSignature), + }, + } +} + +fn from_signature_kind_proto( + proto: SignatureKindProto, + signature_text: String, +) -> Result, DeserializationError> { + Ok(match proto { + SignatureKindProto::InstallationKey(installation_key_signature) => Box::new( + InstallationKeySignature::new(signature_text, installation_key_signature.bytes), + ), + SignatureKindProto::Erc191(erc191_signature) => Box::new(RecoverableEcdsaSignature::new( + signature_text, + erc191_signature.bytes, + )), + SignatureKindProto::Erc1271(erc1271_signature) => Box::new(Erc1271Signature::new( + signature_text, + erc1271_signature.signature, + erc1271_signature.contract_address, + erc1271_signature.block_height as u64, + )), + SignatureKindProto::DelegatedErc191(delegated_erc191_signature) => { + let signature_value = delegated_erc191_signature + .signature + .ok_or(DeserializationError::MissingSignature)?; + let recoverable_ecdsa_signature = + RecoverableEcdsaSignature::new(signature_text, signature_value.bytes); + + Box::new(LegacyDelegatedSignature::new( + recoverable_ecdsa_signature, + delegated_erc191_signature + .delegated_key + .ok_or(DeserializationError::MissingSignature)?, + )) + } + }) +} + +// Serialization +#[derive(Error, Debug)] +pub enum SerializationError { + #[error("Missing action")] + MissingAction, +} + +pub fn to_identity_update_proto( + identity_update: &IdentityUpdate, +) -> Result { + let actions: Vec = identity_update + .actions + .iter() + .map(to_identity_action_proto) + .collect(); + + let proto = IdentityUpdateProto { + client_timestamp_ns: identity_update.client_timestamp_ns, + actions, + }; + + Ok(proto) +} + +fn to_identity_action_proto(action: &Action) -> IdentityActionProto { + match action { + Action::AddAssociation(add_association) => IdentityActionProto { + kind: Some(IdentityActionKindProto::Add(AddAssociationProto { + inbox_id: "TODO".to_string(), + new_member_identifier: Some(to_member_identifier_proto( + add_association.new_member_identifier.clone(), + )), + new_member_signature: Some(add_association.new_member_signature.to_proto()), + existing_member_signature: Some( + add_association.existing_member_signature.to_proto(), + ), + })), + }, + Action::CreateInbox(create_inbox) => IdentityActionProto { + kind: Some(IdentityActionKindProto::CreateInbox(CreateInboxProto { + nonce: create_inbox.nonce, + initial_address: create_inbox.account_address.clone(), + initial_address_signature: Some(create_inbox.initial_address_signature.to_proto()), + })), + }, + Action::RevokeAssociation(revoke_association) => IdentityActionProto { + kind: Some(IdentityActionKindProto::Revoke(RevokeAssociationProto { + inbox_id: "TODO".to_string(), + member_to_revoke: Some(to_member_identifier_proto( + revoke_association.revoked_member.clone(), + )), + recovery_address_signature: Some( + revoke_association.recovery_address_signature.to_proto(), + ), + })), + }, + Action::ChangeRecoveryAddress(change_recovery_address) => IdentityActionProto { + kind: Some(IdentityActionKindProto::ChangeRecoveryAddress( + ChangeRecoveryAddressProto { + inbox_id: "TODO".to_string(), + new_recovery_address: change_recovery_address.new_recovery_address.clone(), + existing_recovery_address_signature: Some( + change_recovery_address + .recovery_address_signature + .to_proto(), + ), + }, + )), + }, + } +} + +fn to_member_identifier_proto(member_identifier: MemberIdentifier) -> MemberIdentifierProto { + match member_identifier { + MemberIdentifier::Address(address) => MemberIdentifierProto { + kind: Some(MemberIdentifierKindProto::Address(address)), + }, + MemberIdentifier::Installation(public_key) => MemberIdentifierProto { + kind: Some(MemberIdentifierKindProto::InstallationPublicKey(public_key)), + }, + } +} + +#[cfg(test)] +mod tests { + use crate::associations::test_utils::{rand_string, rand_u64}; + + use super::*; + + #[test] + fn test_round_trip() { + let account_address = rand_string(); + let nonce = rand_u64(); + // let inbox_id = generate_inbox_id(&account_address, &nonce); + + let identity_update = IdentityUpdate::new( + vec![Action::CreateInbox(CreateInbox { + nonce: nonce, + account_address: account_address, + initial_address_signature: Box::new(RecoverableEcdsaSignature::new( + "foo".to_string(), + vec![1, 2, 3], + )), + })], + rand_u64(), + ); + + let serialized_update = + to_identity_update_proto(&identity_update).expect("serialization should succeed"); + + assert_eq!( + serialized_update.client_timestamp_ns, + identity_update.client_timestamp_ns + ); + assert_eq!(serialized_update.actions.len(), 1); + + let deserialized_update = from_identity_update_proto(serialized_update.clone()) + .expect("deserialization should succeed"); + + let reserialized = + to_identity_update_proto(&deserialized_update).expect("serialization should succeed"); + + assert_eq!(serialized_update, reserialized); + } +} diff --git a/xmtp_id/src/associations/signature.rs b/xmtp_id/src/associations/signature.rs index d71bec608..2e48aaf25 100644 --- a/xmtp_id/src/associations/signature.rs +++ b/xmtp_id/src/associations/signature.rs @@ -1,6 +1,12 @@ use thiserror::Error; use super::MemberIdentifier; +use xmtp_proto::xmtp::identity::associations::{ + signature::Signature as SignatureKindProto, Erc1271Signature as Erc1271SignatureProto, + LegacyDelegatedSignature as LegacyDelegatedSignatureProto, + RecoverableEcdsaSignature as RecoverableEcdsaSignatureProto, + RecoverableEd25519Signature as RecoverableEd25519SignatureProto, Signature as SignatureProto, +}; #[derive(Debug, Error, PartialEq)] pub enum SignatureError { @@ -32,6 +38,7 @@ pub trait Signature: SignatureClone { fn recover_signer(&self) -> Result; fn signature_kind(&self) -> SignatureKind; fn bytes(&self) -> Vec; + fn to_proto(&self) -> SignatureProto; } pub trait SignatureClone { @@ -52,3 +59,182 @@ impl Clone for Box { self.clone_box() } } + +#[allow(dead_code)] +#[derive(Clone)] +pub struct RecoverableEcdsaSignature { + signature_text: String, + signature_bytes: Vec, +} + +impl RecoverableEcdsaSignature { + pub fn new(signature_text: String, signature_bytes: Vec) -> Self { + RecoverableEcdsaSignature { + signature_text, + signature_bytes, + } + } +} + +impl Signature for RecoverableEcdsaSignature { + fn recover_signer(&self) -> Result { + todo!() + } + + fn signature_kind(&self) -> SignatureKind { + SignatureKind::Erc191 + } + + fn bytes(&self) -> Vec { + self.signature_bytes.clone() + } + + fn to_proto(&self) -> SignatureProto { + SignatureProto { + signature: Some(SignatureKindProto::Erc191(RecoverableEcdsaSignatureProto { + bytes: self.bytes(), + })), + } + } +} + +#[allow(dead_code)] +#[derive(Clone)] +pub struct Erc1271Signature { + signature_text: String, + signature_bytes: Vec, + contract_address: String, + block_height: u64, +} + +impl Erc1271Signature { + pub fn new( + signature_text: String, + signature_bytes: Vec, + contract_address: String, + block_height: u64, + ) -> Self { + Erc1271Signature { + signature_text, + signature_bytes, + contract_address, + block_height, + } + } +} + +impl Signature for Erc1271Signature { + fn recover_signer(&self) -> Result { + // TODO: Verify signature first + Ok(self.contract_address.clone().into()) + } + + fn signature_kind(&self) -> SignatureKind { + SignatureKind::Erc1271 + } + + fn bytes(&self) -> Vec { + self.signature_bytes.clone() + } + + fn to_proto(&self) -> SignatureProto { + SignatureProto { + signature: Some(SignatureKindProto::Erc1271(Erc1271SignatureProto { + contract_address: self.contract_address.clone(), + block_height: self.block_height as i64, + signature: self.bytes(), + })), + } + } +} + +#[allow(dead_code)] +#[derive(Clone)] +pub struct InstallationKeySignature { + signature_text: String, + signature_bytes: Vec, +} + +impl InstallationKeySignature { + pub fn new(signature_text: String, signature_bytes: Vec) -> Self { + InstallationKeySignature { + signature_text, + signature_bytes, + } + } +} + +impl Signature for InstallationKeySignature { + fn recover_signer(&self) -> Result { + todo!() + } + + fn signature_kind(&self) -> SignatureKind { + SignatureKind::InstallationKey + } + + fn bytes(&self) -> Vec { + self.signature_bytes.clone() + } + + fn to_proto(&self) -> SignatureProto { + SignatureProto { + signature: Some(SignatureKindProto::InstallationKey( + RecoverableEd25519SignatureProto { + bytes: self.bytes(), + }, + )), + } + } +} + +#[allow(dead_code)] +#[derive(Clone)] +pub struct LegacyDelegatedSignature { + // This would be the signature from the legacy key + legacy_key_signature: RecoverableEcdsaSignature, + signed_public_key: xmtp_proto::xmtp::message_contents::SignedPublicKey, +} + +impl LegacyDelegatedSignature { + pub fn new( + legacy_key_signature: RecoverableEcdsaSignature, + signed_public_key: xmtp_proto::xmtp::message_contents::SignedPublicKey, + ) -> Self { + LegacyDelegatedSignature { + legacy_key_signature, + signed_public_key, + } + } +} + +impl Signature for LegacyDelegatedSignature { + fn recover_signer(&self) -> Result { + // TODO: Two steps needed here: + // 1. Verify the RecoverableEcdsaSignature and make sure it recovers to the public key specified in the `signed_public_key` + // 2. Verify the wallet signature on the `signed_public_key` + // Return the wallet address + todo!() + } + + fn signature_kind(&self) -> SignatureKind { + SignatureKind::LegacyDelegated + } + + fn bytes(&self) -> Vec { + self.legacy_key_signature.bytes() + } + + fn to_proto(&self) -> SignatureProto { + SignatureProto { + signature: Some(SignatureKindProto::DelegatedErc191( + LegacyDelegatedSignatureProto { + delegated_key: Some(self.signed_public_key.clone()), + signature: Some(RecoverableEcdsaSignatureProto { + bytes: self.bytes(), + }), + }, + )), + } + } +} diff --git a/xmtp_id/src/associations/test_utils.rs b/xmtp_id/src/associations/test_utils.rs index 3fd06ab1f..d1806ef21 100644 --- a/xmtp_id/src/associations/test_utils.rs +++ b/xmtp_id/src/associations/test_utils.rs @@ -1,4 +1,5 @@ use rand::{distributions::Alphanumeric, Rng}; +use xmtp_proto::xmtp::identity::associations::Signature as SignatureProto; use super::{MemberIdentifier, Signature, SignatureError, SignatureKind}; @@ -65,4 +66,8 @@ impl Signature for MockSignature { let sig = format!("{}{}", self.signer_identity, self.signature_nonce); sig.as_bytes().to_vec() } + + fn to_proto(&self) -> SignatureProto { + SignatureProto { signature: None } + } } diff --git a/xmtp_proto/buf.gen.yaml b/xmtp_proto/buf.gen.yaml index ed80953ff..aa5ea1dae 100644 --- a/xmtp_proto/buf.gen.yaml +++ b/xmtp_proto/buf.gen.yaml @@ -17,9 +17,11 @@ plugins: - compile_well_known_types - extern_path=.google.protobuf=::pbjson_types # Exclude it from non-tonic builds (so we can use the rest in Wasm) + - client_mod_attribute=xmtp.identity.api.v1=#[cfg(feature = "tonic")] - client_mod_attribute=xmtp.message_api.v1=#[cfg(feature = "tonic")] - client_mod_attribute=xmtp.mls.api.v1=#[cfg(feature = "tonic")] - client_mod_attribute=xmtp.mls_validation.v1=#[cfg(feature = "tonic")] + - server_mod_attribute=xmtp.identity.api.v1=#[cfg(feature = "tonic")] - server_mod_attribute=xmtp.mls_validation.v1=#[cfg(feature = "tonic")] - server_mod_attribute=xmtp.message_api.v1=#[cfg(feature = "tonic")] - server_mod_attribute=xmtp.mls.api.v1=#[cfg(feature = "tonic")] diff --git a/xmtp_proto/src/gen/xmtp.identity.api.v1.tonic.rs b/xmtp_proto/src/gen/xmtp.identity.api.v1.tonic.rs index 4b2cf2811..90f53d301 100644 --- a/xmtp_proto/src/gen/xmtp.identity.api.v1.tonic.rs +++ b/xmtp_proto/src/gen/xmtp.identity.api.v1.tonic.rs @@ -1,5 +1,6 @@ // @generated /// Generated client implementations. +#[cfg(feature = "tonic")] pub mod identity_api_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] use tonic::codegen::*; @@ -174,6 +175,7 @@ pub mod identity_api_client { } } /// Generated server implementations. +#[cfg(feature = "tonic")] pub mod identity_api_server { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] use tonic::codegen::*; diff --git a/xmtp_proto/src/gen/xmtp.identity.associations.rs b/xmtp_proto/src/gen/xmtp.identity.associations.rs index 217a1bcb1..f57a832da 100644 --- a/xmtp_proto/src/gen/xmtp.identity.associations.rs +++ b/xmtp_proto/src/gen/xmtp.identity.associations.rs @@ -102,8 +102,8 @@ pub mod member_identifier { pub struct CreateInbox { #[prost(string, tag="1")] pub initial_address: ::prost::alloc::string::String, - #[prost(uint32, tag="2")] - pub nonce: u32, + #[prost(uint64, tag="2")] + pub nonce: u64, /// Must be an addressable member #[prost(message, optional, tag="3")] pub initial_address_signature: ::core::option::Option, @@ -400,7 +400,7 @@ pub const FILE_DESCRIPTOR_SET: &[u8] = &[ 0x6f, 0x78, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, - 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, + 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x61, 0x0a, 0x19, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x6d, 0x74, 0x70, 0x2e, 0x69, 0x64, 0x65, 0x6e, diff --git a/xmtp_proto/src/gen/xmtp.identity.associations.serde.rs b/xmtp_proto/src/gen/xmtp.identity.associations.serde.rs index a9c3dc440..39f223289 100644 --- a/xmtp_proto/src/gen/xmtp.identity.associations.serde.rs +++ b/xmtp_proto/src/gen/xmtp.identity.associations.serde.rs @@ -295,7 +295,8 @@ impl serde::Serialize for CreateInbox { struct_ser.serialize_field("initialAddress", &self.initial_address)?; } if self.nonce != 0 { - struct_ser.serialize_field("nonce", &self.nonce)?; + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("nonce", ToString::to_string(&self.nonce).as_str())?; } if let Some(v) = self.initial_address_signature.as_ref() { struct_ser.serialize_field("initialAddressSignature", v)?;