|
| 1 | +use super::hashes::generate_xid; |
| 2 | +use super::member::{Member, MemberIdentifier, MemberKind}; |
| 3 | +use super::signature::{Signature, SignatureError, SignatureKind}; |
| 4 | +use super::state::AssociationState; |
| 5 | + |
| 6 | +use thiserror::Error; |
| 7 | + |
| 8 | +// const ALLOWED_CREATE_ENTITY_ROLES: [EntityRole; 2] = [EntityRole::LegacyKey, EntityRole::Address]; |
| 9 | + |
| 10 | +#[derive(Debug, Error, PartialEq)] |
| 11 | +pub enum AssociationError { |
| 12 | + #[error("Error creating association {0}")] |
| 13 | + Generic(String), |
| 14 | + #[error("Multiple create operations detected")] |
| 15 | + MultipleCreate, |
| 16 | + #[error("XID not yet created")] |
| 17 | + NotCreated, |
| 18 | + #[error("Signature validation failed {0}")] |
| 19 | + Signature(#[from] SignatureError), |
| 20 | + #[error("Missing existing member")] |
| 21 | + MissingExistingMember, |
| 22 | + #[error("Legacy key is only allowed to be associated using a legacy signature with nonce 0")] |
| 23 | + LegacySignatureReuse, |
| 24 | + #[error("The new member identifier does not match the signer")] |
| 25 | + NewMemberIdSignatureMismatch, |
| 26 | + #[error("Signature not allowed for role {0:?} {1:?}")] |
| 27 | + SignatureNotAllowed(MemberKind, SignatureKind), |
| 28 | + #[error("Replay detected")] |
| 29 | + Replay, |
| 30 | +} |
| 31 | + |
| 32 | +pub trait IdentityAction { |
| 33 | + fn update_state( |
| 34 | + &self, |
| 35 | + existing_state: Option<AssociationState>, |
| 36 | + ) -> Result<AssociationState, AssociationError>; |
| 37 | + fn signatures(&self) -> Vec<Vec<u8>>; |
| 38 | + fn replay_check(&self, state: &AssociationState) -> Result<(), AssociationError> { |
| 39 | + let signatures = self.signatures(); |
| 40 | + for signature in signatures { |
| 41 | + if state.has_seen(&signature) { |
| 42 | + return Err(AssociationError::Replay); |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + Ok(()) |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +pub struct CreateInbox { |
| 51 | + pub nonce: u64, |
| 52 | + pub account_address: String, |
| 53 | + pub initial_address_signature: Box<dyn Signature>, |
| 54 | +} |
| 55 | + |
| 56 | +impl IdentityAction for CreateInbox { |
| 57 | + fn update_state( |
| 58 | + &self, |
| 59 | + existing_state: Option<AssociationState>, |
| 60 | + ) -> Result<AssociationState, AssociationError> { |
| 61 | + if existing_state.is_some() { |
| 62 | + return Err(AssociationError::MultipleCreate); |
| 63 | + } |
| 64 | + |
| 65 | + let account_address = self.account_address.clone(); |
| 66 | + let recovered_signer = self.initial_address_signature.recover_signer()?; |
| 67 | + if recovered_signer.ne(&MemberIdentifier::Address(account_address.clone())) { |
| 68 | + return Err(AssociationError::MissingExistingMember); |
| 69 | + } |
| 70 | + |
| 71 | + if self.initial_address_signature.signature_kind() == SignatureKind::LegacyDelegated |
| 72 | + && self.nonce != 0 |
| 73 | + { |
| 74 | + return Err(AssociationError::LegacySignatureReuse); |
| 75 | + } |
| 76 | + |
| 77 | + Ok(AssociationState::new(account_address, self.nonce)) |
| 78 | + } |
| 79 | + |
| 80 | + fn signatures(&self) -> Vec<Vec<u8>> { |
| 81 | + vec![self.initial_address_signature.bytes()] |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +pub struct AddAssociation { |
| 86 | + pub client_timestamp_ns: u64, |
| 87 | + pub new_member_signature: Box<dyn Signature>, |
| 88 | + pub new_member_identifier: MemberIdentifier, |
| 89 | + pub existing_member_signature: Box<dyn Signature>, |
| 90 | +} |
| 91 | + |
| 92 | +impl IdentityAction for AddAssociation { |
| 93 | + fn update_state( |
| 94 | + &self, |
| 95 | + maybe_existing_state: Option<AssociationState>, |
| 96 | + ) -> Result<AssociationState, AssociationError> { |
| 97 | + let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?; |
| 98 | + self.replay_check(&existing_state)?; |
| 99 | + |
| 100 | + let new_member_address = self.new_member_signature.recover_signer()?; |
| 101 | + if new_member_address.ne(&self.new_member_identifier) { |
| 102 | + return Err(AssociationError::NewMemberIdSignatureMismatch); |
| 103 | + } |
| 104 | + |
| 105 | + let existing_member_identifier = self.existing_member_signature.recover_signer()?; |
| 106 | + let recovery_address = existing_state.recovery_address(); |
| 107 | + |
| 108 | + if new_member_address.ne(&self.new_member_identifier) { |
| 109 | + return Err(AssociationError::Generic( |
| 110 | + "new member identifier does not match signature".to_string(), |
| 111 | + )); |
| 112 | + } |
| 113 | + |
| 114 | + if new_member_address.ne(&self.new_member_identifier) { |
| 115 | + return Err(AssociationError::Generic( |
| 116 | + "new member identifier does not match signature".to_string(), |
| 117 | + )); |
| 118 | + } |
| 119 | + |
| 120 | + // You cannot add yourself |
| 121 | + if new_member_address == existing_member_identifier { |
| 122 | + return Err(AssociationError::Generic("tried to add self".to_string())); |
| 123 | + } |
| 124 | + |
| 125 | + // Only allow LegacyDelegated signatures on XIDs with a nonce of 0 |
| 126 | + // Otherwise the client should use the regular wallet signature to create |
| 127 | + if self.new_member_signature.signature_kind() == SignatureKind::LegacyDelegated { |
| 128 | + if existing_state |
| 129 | + .xid() |
| 130 | + .ne(&generate_xid(&existing_member_identifier.to_string(), &0)) |
| 131 | + { |
| 132 | + return Err(AssociationError::LegacySignatureReuse); |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + // Make sure that the signature type lines up with the role |
| 137 | + if !allowed_signature_for_kind( |
| 138 | + &self.new_member_identifier.kind(), |
| 139 | + &self.new_member_signature.signature_kind(), |
| 140 | + ) { |
| 141 | + return Err(AssociationError::SignatureNotAllowed( |
| 142 | + self.new_member_identifier.kind(), |
| 143 | + self.new_member_signature.signature_kind(), |
| 144 | + )); |
| 145 | + } |
| 146 | + |
| 147 | + let existing_member = existing_state.get(&existing_member_identifier); |
| 148 | + |
| 149 | + let existing_entity_id = match existing_member { |
| 150 | + // If there is an existing member of the XID, use that member's ID |
| 151 | + Some(member) => member.identifier, |
| 152 | + None => { |
| 153 | + let recovery_identifier = MemberIdentifier::Address(recovery_address.clone()); |
| 154 | + // Check if it is a signature from the recovery address, which is allowed to add members |
| 155 | + if existing_member_identifier.ne(&recovery_identifier) { |
| 156 | + return Err(AssociationError::MissingExistingMember); |
| 157 | + } |
| 158 | + // BUT, the recovery address has to be used with a real wallet signature, can't be delegated |
| 159 | + if self.existing_member_signature.signature_kind() == SignatureKind::LegacyDelegated |
| 160 | + { |
| 161 | + return Err(AssociationError::LegacySignatureReuse); |
| 162 | + } |
| 163 | + // If it is a real wallet signature, then it is allowed to add members |
| 164 | + recovery_identifier |
| 165 | + } |
| 166 | + }; |
| 167 | + |
| 168 | + let new_member = Member::new(new_member_address, Some(existing_entity_id)); |
| 169 | + |
| 170 | + println!("Adding new entity to state {:?}", &new_member); |
| 171 | + |
| 172 | + Ok(existing_state.add(new_member)) |
| 173 | + } |
| 174 | + |
| 175 | + fn signatures(&self) -> Vec<Vec<u8>> { |
| 176 | + vec![ |
| 177 | + self.existing_member_signature.bytes(), |
| 178 | + self.new_member_signature.bytes(), |
| 179 | + ] |
| 180 | + } |
| 181 | +} |
| 182 | + |
| 183 | +pub struct RevokeAssociation { |
| 184 | + pub client_timestamp_ns: u64, |
| 185 | + pub recovery_address_signature: Box<dyn Signature>, |
| 186 | + pub revoked_member: MemberIdentifier, |
| 187 | +} |
| 188 | + |
| 189 | +impl IdentityAction for RevokeAssociation { |
| 190 | + fn update_state( |
| 191 | + &self, |
| 192 | + maybe_existing_state: Option<AssociationState>, |
| 193 | + ) -> Result<AssociationState, AssociationError> { |
| 194 | + let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?; |
| 195 | + self.replay_check(&existing_state)?; |
| 196 | + |
| 197 | + if self.recovery_address_signature.signature_kind() == SignatureKind::LegacyDelegated { |
| 198 | + return Err(AssociationError::SignatureNotAllowed( |
| 199 | + MemberKind::Address, |
| 200 | + SignatureKind::LegacyDelegated, |
| 201 | + )); |
| 202 | + } |
| 203 | + // Don't need to check for replay here since revocation is idempotent |
| 204 | + let recovery_signer = self.recovery_address_signature.recover_signer()?; |
| 205 | + // Make sure there is a recovery address set on the state |
| 206 | + let state_recovery_address = existing_state.recovery_address(); |
| 207 | + |
| 208 | + // Ensure this message is signed by the recovery address |
| 209 | + if recovery_signer.ne(&MemberIdentifier::Address(state_recovery_address.clone())) { |
| 210 | + return Err(AssociationError::MissingExistingMember); |
| 211 | + } |
| 212 | + |
| 213 | + let installations_to_remove: Vec<Member> = existing_state |
| 214 | + .members_by_parent(&self.revoked_member) |
| 215 | + .into_iter() |
| 216 | + // Only remove children if they are installations |
| 217 | + .filter(|child| child.kind() == MemberKind::Installation) |
| 218 | + .collect(); |
| 219 | + |
| 220 | + // Actually apply the revocation to the parent |
| 221 | + let new_state = existing_state.remove(&self.revoked_member); |
| 222 | + |
| 223 | + Ok(installations_to_remove |
| 224 | + .iter() |
| 225 | + .fold(new_state, |state, installation| { |
| 226 | + state.remove(&installation.identifier) |
| 227 | + })) |
| 228 | + } |
| 229 | + |
| 230 | + fn signatures(&self) -> Vec<Vec<u8>> { |
| 231 | + vec![self.recovery_address_signature.bytes()] |
| 232 | + } |
| 233 | +} |
| 234 | + |
| 235 | +pub enum Action { |
| 236 | + CreateInbox(CreateInbox), |
| 237 | + AddAssociation(AddAssociation), |
| 238 | + RevokeAssociation(RevokeAssociation), |
| 239 | +} |
| 240 | + |
| 241 | +impl IdentityAction for Action { |
| 242 | + fn update_state( |
| 243 | + &self, |
| 244 | + existing_state: Option<AssociationState>, |
| 245 | + ) -> Result<AssociationState, AssociationError> { |
| 246 | + match self { |
| 247 | + Action::CreateInbox(event) => event.update_state(existing_state), |
| 248 | + Action::AddAssociation(event) => event.update_state(existing_state), |
| 249 | + Action::RevokeAssociation(event) => event.update_state(existing_state), |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + fn signatures(&self) -> Vec<Vec<u8>> { |
| 254 | + match self { |
| 255 | + Action::CreateInbox(event) => event.signatures(), |
| 256 | + Action::AddAssociation(event) => event.signatures(), |
| 257 | + Action::RevokeAssociation(event) => event.signatures(), |
| 258 | + } |
| 259 | + } |
| 260 | +} |
| 261 | + |
| 262 | +pub struct IdentityUpdate { |
| 263 | + pub actions: Vec<Action>, |
| 264 | +} |
| 265 | + |
| 266 | +impl IdentityUpdate { |
| 267 | + pub fn new(actions: Vec<Action>) -> Self { |
| 268 | + Self { actions } |
| 269 | + } |
| 270 | +} |
| 271 | + |
| 272 | +impl IdentityAction for IdentityUpdate { |
| 273 | + fn update_state( |
| 274 | + &self, |
| 275 | + existing_state: Option<AssociationState>, |
| 276 | + ) -> Result<AssociationState, AssociationError> { |
| 277 | + let mut state = existing_state.clone(); |
| 278 | + for action in &self.actions { |
| 279 | + state = Some(action.update_state(state)?); |
| 280 | + } |
| 281 | + |
| 282 | + let new_state = state.ok_or(AssociationError::NotCreated)?; |
| 283 | + |
| 284 | + // After all the updates in the LogEntry have been processed, add the list of signatures to the state |
| 285 | + // so that the signatures can not be re-used in subsequent updates |
| 286 | + Ok(new_state.add_seen_signatures(self.signatures())) |
| 287 | + } |
| 288 | + |
| 289 | + fn signatures(&self) -> Vec<Vec<u8>> { |
| 290 | + self.actions |
| 291 | + .iter() |
| 292 | + .flat_map(|action| action.signatures()) |
| 293 | + .collect() |
| 294 | + } |
| 295 | +} |
| 296 | + |
| 297 | +// Ensure that the type of signature matches the new entity's role. |
| 298 | +pub fn allowed_signature_for_kind(role: &MemberKind, signature_kind: &SignatureKind) -> bool { |
| 299 | + match role { |
| 300 | + MemberKind::Address => match signature_kind { |
| 301 | + SignatureKind::Erc191 => true, |
| 302 | + SignatureKind::Erc1271 => true, |
| 303 | + SignatureKind::InstallationKey => false, |
| 304 | + SignatureKind::LegacyDelegated => true, |
| 305 | + }, |
| 306 | + MemberKind::Installation => match signature_kind { |
| 307 | + SignatureKind::Erc191 => false, |
| 308 | + SignatureKind::Erc1271 => false, |
| 309 | + SignatureKind::InstallationKey => true, |
| 310 | + SignatureKind::LegacyDelegated => false, |
| 311 | + }, |
| 312 | + } |
| 313 | +} |
0 commit comments