From ab88b8fa8b52c81563dadd9e6624afcf4afc2558 Mon Sep 17 00:00:00 2001 From: Nicholas Molnar <65710+neekolas@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:15:42 -0700 Subject: [PATCH] Add Unsigned Identity Updates --- xmtp_id/src/associations/association_log.rs | 11 +- xmtp_id/src/associations/mod.rs | 64 ++++--- xmtp_id/src/associations/unsigned_actions.rs | 167 +++++++++++++++++++ 3 files changed, 212 insertions(+), 30 deletions(-) create mode 100644 xmtp_id/src/associations/unsigned_actions.rs diff --git a/xmtp_id/src/associations/association_log.rs b/xmtp_id/src/associations/association_log.rs index 74580bab3..d0c8dde72 100644 --- a/xmtp_id/src/associations/association_log.rs +++ b/xmtp_id/src/associations/association_log.rs @@ -90,7 +90,6 @@ impl IdentityAction for CreateInbox { /// AddAssociation Action pub struct AddAssociation { - pub client_timestamp_ns: u64, pub new_member_signature: Box, pub new_member_identifier: MemberIdentifier, pub existing_member_signature: Box, @@ -187,7 +186,6 @@ impl IdentityAction for AddAssociation { /// RevokeAssociation Action pub struct RevokeAssociation { - pub client_timestamp_ns: u64, pub recovery_address_signature: Box, pub revoked_member: MemberIdentifier, } @@ -240,7 +238,6 @@ impl IdentityAction for RevokeAssociation { /// ChangeRecoveryAddress Action pub struct ChangeRecoveryAddress { - pub client_timestamp_ns: u64, pub recovery_address_signature: Box, pub new_recovery_address: String, } @@ -306,12 +303,16 @@ impl IdentityAction for Action { /// An `IdentityUpdate` contains one or more Actions that can be applied to the AssociationState pub struct IdentityUpdate { + pub client_timestamp_ns: u64, pub actions: Vec, } impl IdentityUpdate { - pub fn new(actions: Vec) -> Self { - Self { actions } + pub fn new(actions: Vec, client_timestamp_ns: u64) -> Self { + Self { + actions, + client_timestamp_ns, + } } } diff --git a/xmtp_id/src/associations/mod.rs b/xmtp_id/src/associations/mod.rs index 439e5e802..d868f6d35 100644 --- a/xmtp_id/src/associations/mod.rs +++ b/xmtp_id/src/associations/mod.rs @@ -5,6 +5,7 @@ mod signature; mod state; #[cfg(test)] mod test_utils; +mod unsigned_actions; pub use self::association_log::*; pub use self::member::{Member, MemberIdentifier, MemberKind}; @@ -46,6 +47,12 @@ mod tests { signature_nonce: u64, } + impl IdentityUpdate { + pub fn new_test(actions: Vec) -> Self { + Self::new(actions, rand_u64()) + } + } + impl MockSignature { pub fn new_boxed( is_valid: bool, @@ -88,7 +95,6 @@ mod tests { let existing_member = rand_string(); let new_member = rand_vec(); return Self { - client_timestamp_ns: rand_u64(), existing_member_signature: MockSignature::new_boxed( true, existing_member.into(), @@ -127,7 +133,6 @@ mod tests { fn default() -> Self { let signer = rand_string(); return Self { - client_timestamp_ns: rand_u64(), recovery_address_signature: MockSignature::new_boxed( true, signer.into(), @@ -141,7 +146,7 @@ mod tests { fn new_test_inbox() -> AssociationState { let create_request = CreateInbox::default(); - let identity_update = IdentityUpdate::new(vec![Action::CreateInbox(create_request)]); + let identity_update = IdentityUpdate::new_test(vec![Action::CreateInbox(create_request)]); get_state(vec![identity_update]).unwrap() } @@ -161,14 +166,14 @@ mod tests { ..Default::default() }); - apply_update(initial_state, IdentityUpdate::new(vec![update])).unwrap() + apply_update(initial_state, IdentityUpdate::new_test(vec![update])).unwrap() } #[test] fn test_create_inbox() { let create_request = CreateInbox::default(); let account_address = create_request.account_address.clone(); - let identity_update = IdentityUpdate::new(vec![Action::CreateInbox(create_request)]); + let identity_update = IdentityUpdate::new_test(vec![Action::CreateInbox(create_request)]); let state = get_state(vec![identity_update]).unwrap(); assert_eq!(state.members().len(), 1); @@ -199,7 +204,8 @@ mod tests { ..Default::default() }); - let new_state = apply_update(initial_state, IdentityUpdate::new(vec![update])).unwrap(); + let new_state = + apply_update(initial_state, IdentityUpdate::new_test(vec![update])).unwrap(); assert_eq!(new_state.members().len(), 2); let new_member = new_state.get(&new_installation_identifier).unwrap(); @@ -228,7 +234,7 @@ mod tests { new_member_identifier: new_member_identifier.clone(), ..Default::default() }; - let identity_update = IdentityUpdate::new(vec![ + let identity_update = IdentityUpdate::new_test(vec![ Action::CreateInbox(create_action), Action::AddAssociation(add_action), ]); @@ -253,7 +259,7 @@ mod tests { Some(0), ), }; - let state = get_state(vec![IdentityUpdate::new(vec![Action::CreateInbox( + let state = get_state(vec![IdentityUpdate::new_test(vec![Action::CreateInbox( create_action, )])]) .unwrap(); @@ -270,7 +276,7 @@ mod tests { ), ..Default::default() }); - let update_result = apply_update(state, IdentityUpdate::new(vec![update])); + let update_result = apply_update(state, IdentityUpdate::new_test(vec![update])); assert!(update_result.is_err()); assert_eq!(update_result.err().unwrap(), AssociationError::Replay); } @@ -303,8 +309,11 @@ mod tests { ..Default::default() }); - let new_state = apply_update(initial_state, IdentityUpdate::new(vec![add_association])) - .expect("expected update to succeed"); + let new_state = apply_update( + initial_state, + IdentityUpdate::new_test(vec![add_association]), + ) + .expect("expected update to succeed"); assert_eq!(new_state.members().len(), 3); } @@ -317,7 +326,9 @@ mod tests { ..Default::default() }; - let state_result = get_state(vec![IdentityUpdate::new(vec![Action::CreateInbox(action)])]); + let state_result = get_state(vec![IdentityUpdate::new_test(vec![Action::CreateInbox( + action, + )])]); assert!(state_result.is_err()); assert_eq!( state_result.err().unwrap(), @@ -338,7 +349,7 @@ mod tests { let update_result = apply_update( initial_state.clone(), - IdentityUpdate::new(vec![update_with_bad_existing_member]), + IdentityUpdate::new_test(vec![update_with_bad_existing_member]), ); assert!(update_result.is_err()); assert_eq!( @@ -359,7 +370,7 @@ mod tests { let update_result_2 = apply_update( initial_state, - IdentityUpdate::new(vec![update_with_bad_new_member]), + IdentityUpdate::new_test(vec![update_with_bad_new_member]), ); assert!(update_result_2.is_err()); assert_eq!( @@ -383,7 +394,7 @@ mod tests { ..Default::default() }); - let state_result = get_state(vec![IdentityUpdate::new(vec![create_request, update])]); + let state_result = get_state(vec![IdentityUpdate::new_test(vec![create_request, update])]); assert!(state_result.is_err()); assert_eq!( state_result.err().unwrap(), @@ -415,7 +426,7 @@ mod tests { ..Default::default() }); - let update_result = apply_update(existing_state, IdentityUpdate::new(vec![update])); + let update_result = apply_update(existing_state, IdentityUpdate::new_test(vec![update])); assert!(update_result.is_err()); assert_eq!( update_result.err().unwrap(), @@ -446,7 +457,7 @@ mod tests { ..Default::default() }); - let new_state = apply_update(initial_state, IdentityUpdate::new(vec![update])) + let new_state = apply_update(initial_state, IdentityUpdate::new_test(vec![update])) .expect("expected update to succeed"); assert!(new_state.get(&installation_id).is_none()); } @@ -473,7 +484,7 @@ mod tests { let new_state = apply_update( initial_state, - IdentityUpdate::new(vec![add_second_installation]), + IdentityUpdate::new_test(vec![add_second_installation]), ) .expect("expected update to succeed"); assert_eq!(new_state.members().len(), 3); @@ -490,7 +501,7 @@ mod tests { }); // With this revocation the original wallet + both installations should be gone - let new_state = apply_update(new_state, IdentityUpdate::new(vec![revocation])) + let new_state = apply_update(new_state, IdentityUpdate::new_test(vec![revocation])) .expect("expected update to succeed"); assert_eq!(new_state.members().len(), 0); } @@ -536,7 +547,7 @@ mod tests { let state_after_remove = apply_update( initial_state, - IdentityUpdate::new(vec![add_second_wallet, revoke_second_wallet]), + IdentityUpdate::new_test(vec![add_second_wallet, revoke_second_wallet]), ) .expect("expected update to succeed"); assert_eq!(state_after_remove.members().len(), 1); @@ -560,7 +571,7 @@ mod tests { let state_after_re_add = apply_update( state_after_remove, - IdentityUpdate::new(vec![add_second_wallet_again]), + IdentityUpdate::new_test(vec![add_second_wallet_again]), ) .expect("expected update to succeed"); assert_eq!(state_after_re_add.members().len(), 2); @@ -573,7 +584,6 @@ mod tests { initial_state.recovery_address().clone().into(); let new_recovery_address = rand_string(); let update_recovery = Action::ChangeRecoveryAddress(ChangeRecoveryAddress { - client_timestamp_ns: rand_u64(), new_recovery_address: new_recovery_address.clone(), recovery_address_signature: MockSignature::new_boxed( true, @@ -583,8 +593,11 @@ mod tests { ), }); - let new_state = apply_update(initial_state, IdentityUpdate::new(vec![update_recovery])) - .expect("expected update to succeed"); + let new_state = apply_update( + initial_state, + IdentityUpdate::new_test(vec![update_recovery]), + ) + .expect("expected update to succeed"); assert_eq!(new_state.recovery_address(), &new_recovery_address); let attempted_revoke = Action::RevokeAssociation(RevokeAssociation { @@ -598,7 +611,8 @@ mod tests { ..Default::default() }); - let revoke_result = apply_update(new_state, IdentityUpdate::new(vec![attempted_revoke])); + let revoke_result = + apply_update(new_state, IdentityUpdate::new_test(vec![attempted_revoke])); assert!(revoke_result.is_err()); assert_eq!( revoke_result.err().unwrap(), diff --git a/xmtp_id/src/associations/unsigned_actions.rs b/xmtp_id/src/associations/unsigned_actions.rs new file mode 100644 index 000000000..ce6523e94 --- /dev/null +++ b/xmtp_id/src/associations/unsigned_actions.rs @@ -0,0 +1,167 @@ +use crate::associations::hashes::generate_inbox_id; + +use super::MemberIdentifier; + +pub trait SignatureTextCreator { + fn signature_text(&self) -> String; +} + +#[derive(Clone)] +pub struct UnsignedCreateInbox { + pub nonce: u64, + pub account_address: String, +} + +impl SignatureTextCreator for UnsignedCreateInbox { + fn signature_text(&self) -> String { + format!( + // TODO: Finalize text + "Create Inbox: {}", + generate_inbox_id(&self.account_address, &self.nonce) + ) + } +} + +#[derive(Clone)] +pub struct UnsignedAddAssociation { + pub inbox_id: String, + pub new_member_identifier: MemberIdentifier, +} + +impl SignatureTextCreator for UnsignedAddAssociation { + fn signature_text(&self) -> String { + format!( + // TODO: Finalize text + "Add {} to Inbox {}", + self.new_member_identifier, self.inbox_id + ) + } +} + +#[derive(Clone)] +pub struct UnsignedRevokeAssociation { + pub inbox_id: String, + pub revoked_member: MemberIdentifier, +} + +impl SignatureTextCreator for UnsignedRevokeAssociation { + fn signature_text(&self) -> String { + format!( + // TODO: Finalize text + "Remove {} from Inbox {}", + self.revoked_member, self.inbox_id + ) + } +} + +#[derive(Clone)] +pub struct UnsignedChangeRecoveryAddress { + pub inbox_id: String, + pub new_recovery_address: String, +} + +impl SignatureTextCreator for UnsignedChangeRecoveryAddress { + fn signature_text(&self) -> String { + format!( + // TODO: Finalize text + "Change Recovery Address for Inbox {} to {}", + self.inbox_id, self.new_recovery_address + ) + } +} + +#[allow(dead_code)] +#[derive(Clone)] +pub enum UnsignedAction { + CreateInbox(UnsignedCreateInbox), + AddAssociation(UnsignedAddAssociation), + RevokeAssociation(UnsignedRevokeAssociation), + ChangeRecoveryAddress(UnsignedChangeRecoveryAddress), +} + +impl SignatureTextCreator for UnsignedAction { + fn signature_text(&self) -> String { + match self { + UnsignedAction::CreateInbox(action) => action.signature_text(), + UnsignedAction::AddAssociation(action) => action.signature_text(), + UnsignedAction::RevokeAssociation(action) => action.signature_text(), + UnsignedAction::ChangeRecoveryAddress(action) => action.signature_text(), + } + } +} + +#[derive(Clone)] +pub struct UnsignedIdentityUpdate { + pub client_timestamp_ns: u64, + pub actions: Vec, +} + +impl SignatureTextCreator for UnsignedIdentityUpdate { + fn signature_text(&self) -> String { + let all_signatures = self + .actions + .iter() + .map(|action| action.signature_text()) + .collect::>(); + format!( + "I authorize the following actions on XMTP:\n\n{}\n\nAuthorized at: {}", + all_signatures.join("\n\n"), + // TODO: Pretty up date + self.client_timestamp_ns + ) + } +} + +#[cfg(test)] +mod tests { + use crate::associations::test_utils::{rand_string, rand_u64}; + + use super::*; + + #[test] + fn create_signatures() { + let create_inbox = UnsignedCreateInbox { + nonce: rand_u64(), + account_address: rand_string(), + }; + let inbox_id = generate_inbox_id(&create_inbox.account_address, &create_inbox.nonce); + + let add_association = UnsignedAddAssociation { + inbox_id: inbox_id.clone(), + new_member_identifier: MemberIdentifier::Address(rand_string()), + }; + + let revoke_association = UnsignedRevokeAssociation { + inbox_id: inbox_id.clone(), + revoked_member: MemberIdentifier::Address(rand_string()), + }; + + let change_recovery_address = UnsignedChangeRecoveryAddress { + inbox_id: inbox_id.clone(), + new_recovery_address: rand_string(), + }; + + let identity_update = UnsignedIdentityUpdate { + client_timestamp_ns: rand_u64(), + actions: vec![ + UnsignedAction::CreateInbox(create_inbox.clone()), + UnsignedAction::AddAssociation(add_association.clone()), + UnsignedAction::RevokeAssociation(revoke_association.clone()), + UnsignedAction::ChangeRecoveryAddress(change_recovery_address.clone()), + ], + }; + + let signature_text = identity_update.signature_text(); + let expected_text = format!("I authorize the following actions on XMTP:\n\nCreate Inbox: {}\n\nAdd {} to Inbox {}\n\nRemove {} from Inbox {}\n\nChange Recovery Address for Inbox {} to {}\n\nAuthorized at: {}", + inbox_id, + add_association.new_member_identifier, + inbox_id, + revoke_association.revoked_member, + inbox_id, + inbox_id, + change_recovery_address.new_recovery_address, + identity_update.client_timestamp_ns, + ); + assert_eq!(signature_text, expected_text) + } +}