signing.rs (17325B)
1 use std::collections::BTreeSet; 2 3 use crate::{ 4 GroupAuthority, GroupError, GroupId, GroupMetadata, GroupOutboxPayload, GroupProjection, 5 GroupState, KIND_GROUP_ADMINS, KIND_GROUP_MEMBERS, KIND_GROUP_METADATA, KIND_GROUP_PUT_USER, 6 KIND_GROUP_REMOVE_USER, MemberStatus, RoleName, SupportedKinds, 7 }; 8 use pocket_types::{ 9 Kind as PocketKind, OwnedEvent as PocketOwnedEvent, OwnedTags as PocketOwnedTags, 10 Time as PocketTime, 11 }; 12 use tangle_crypto::RelaySigner; 13 use tangle_protocol::{PublicKeyHex, UnixTimestamp}; 14 15 pub struct GroupGeneratedEventBuilder { 16 signer: RelaySigner, 17 } 18 19 impl GroupGeneratedEventBuilder { 20 pub fn new(signer: RelaySigner) -> Self { 21 Self { signer } 22 } 23 24 pub fn relay_pubkey(&self) -> &PublicKeyHex { 25 self.signer.public_key() 26 } 27 28 pub fn metadata_snapshot_payload( 29 group: &GroupState, 30 created_at: UnixTimestamp, 31 ) -> Result<GroupOutboxPayload, GroupError> { 32 Ok(GroupOutboxPayload::new( 33 KIND_GROUP_METADATA, 34 created_at, 35 metadata_tags(group.id(), group.metadata())?, 36 "", 37 )) 38 } 39 40 pub fn admin_list_snapshot_payload( 41 group_id: &GroupId, 42 projection: &GroupProjection, 43 authority: &GroupAuthority, 44 created_at: UnixTimestamp, 45 ) -> Result<GroupOutboxPayload, GroupError> { 46 let mut admins = BTreeSet::new(); 47 admins.extend(authority.owner_pubkeys().iter().cloned()); 48 admins.extend(authority.admin_pubkeys().iter().cloned()); 49 for ((candidate_group, pubkey), member) in projection.members() { 50 if candidate_group == group_id 51 && member.status() == MemberStatus::Member 52 && member 53 .roles() 54 .contains(&RoleName::permanent_relay_override()) 55 { 56 admins.insert(pubkey.clone()); 57 } 58 } 59 let mut tags = vec![tag_values(["d".to_owned(), group_id.as_str().to_owned()])]; 60 tags.extend( 61 admins 62 .into_iter() 63 .map(|pubkey| tag_values(["p".to_owned(), pubkey.as_str().to_owned()])), 64 ); 65 Ok(GroupOutboxPayload::new( 66 KIND_GROUP_ADMINS, 67 created_at, 68 tags, 69 "", 70 )) 71 } 72 73 pub fn member_list_snapshot_payload( 74 group_id: &GroupId, 75 projection: &GroupProjection, 76 created_at: UnixTimestamp, 77 cap: u32, 78 ) -> Result<Option<GroupOutboxPayload>, GroupError> { 79 let mut members = projection 80 .members() 81 .iter() 82 .filter(|((candidate_group, _), member)| { 83 candidate_group == group_id && member.status() == MemberStatus::Member 84 }) 85 .map(|((_, pubkey), _)| pubkey.clone()) 86 .collect::<Vec<_>>(); 87 members.sort(); 88 if members.len() > usize::try_from(cap).expect("u32 fits in usize on supported targets") { 89 return Ok(None); 90 } 91 let mut tags = vec![tag_values(["d".to_owned(), group_id.as_str().to_owned()])]; 92 tags.extend( 93 members 94 .into_iter() 95 .map(|pubkey| tag_values(["p".to_owned(), pubkey.as_str().to_owned()])), 96 ); 97 Ok(Some(GroupOutboxPayload::new( 98 KIND_GROUP_MEMBERS, 99 created_at, 100 tags, 101 "", 102 ))) 103 } 104 105 pub fn join_accepted_payload( 106 group_id: &GroupId, 107 target_pubkey: &PublicKeyHex, 108 created_at: UnixTimestamp, 109 ) -> GroupOutboxPayload { 110 membership_payload(KIND_GROUP_PUT_USER, group_id, target_pubkey, created_at) 111 } 112 113 pub fn leave_accepted_payload( 114 group_id: &GroupId, 115 target_pubkey: &PublicKeyHex, 116 created_at: UnixTimestamp, 117 ) -> GroupOutboxPayload { 118 membership_payload(KIND_GROUP_REMOVE_USER, group_id, target_pubkey, created_at) 119 } 120 121 pub fn sign_payload_pocket( 122 &self, 123 payload: &GroupOutboxPayload, 124 ) -> Result<PocketOwnedEvent, GroupError> { 125 let kind = PocketKind::from_u16( 126 u16::try_from(payload.generated_kind()) 127 .map_err(|_| GroupError::internal("generated event kind exceeds Pocket kind"))?, 128 ); 129 let tags = PocketOwnedTags::new(payload.tags()).map_err(|error| { 130 GroupError::internal(format!("generated Pocket tags are invalid: {error}")) 131 })?; 132 let event = self 133 .signer 134 .sign_pocket_event( 135 kind, 136 &tags, 137 PocketTime::from_u64(payload.generated_created_at().as_u64()), 138 payload.content().as_bytes(), 139 ) 140 .map_err(GroupError::internal)?; 141 event.verify().map_err(|error| { 142 GroupError::internal(format!( 143 "generated Pocket event failed verification: {error}" 144 )) 145 })?; 146 Ok(event) 147 } 148 } 149 150 fn metadata_tags( 151 group_id: &GroupId, 152 metadata: &GroupMetadata, 153 ) -> Result<Vec<Vec<String>>, GroupError> { 154 let mut tags = vec![tag_values(["d".to_owned(), group_id.as_str().to_owned()])]; 155 if let Some(name) = metadata.name() { 156 tags.push(tag_values(["name".to_owned(), name.to_owned()])); 157 } 158 if let Some(picture) = metadata.picture() { 159 tags.push(tag_values(["picture".to_owned(), picture.to_owned()])); 160 } 161 if let Some(about) = metadata.about() { 162 tags.push(tag_values(["about".to_owned(), about.to_owned()])); 163 } 164 if metadata.private() { 165 tags.push(tag_values(["private".to_owned()])); 166 } 167 if metadata.restricted() { 168 tags.push(tag_values(["restricted".to_owned()])); 169 } 170 if metadata.hidden() { 171 tags.push(tag_values(["hidden".to_owned()])); 172 } 173 if metadata.closed() { 174 tags.push(tag_values(["closed".to_owned()])); 175 } 176 match metadata.supported_kinds() { 177 SupportedKinds::UnspecifiedAll => {} 178 SupportedKinds::None => tags.push(tag_values(["supported_kinds".to_owned()])), 179 SupportedKinds::Only(kinds) => { 180 let mut tag = vec!["supported_kinds".to_owned()]; 181 tag.extend(kinds.iter().map(|kind| kind.as_u32().to_string())); 182 tags.push(tag); 183 } 184 } 185 validate_pocket_tags(&tags)?; 186 Ok(tags) 187 } 188 189 fn validate_pocket_tags(tags: &[Vec<String>]) -> Result<(), GroupError> { 190 PocketOwnedTags::new(tags).map(|_| ()).map_err(|error| { 191 GroupError::internal(format!("generated Pocket tags are invalid: {error}")) 192 }) 193 } 194 195 fn membership_payload( 196 kind: u32, 197 group_id: &GroupId, 198 target_pubkey: &PublicKeyHex, 199 created_at: UnixTimestamp, 200 ) -> GroupOutboxPayload { 201 GroupOutboxPayload::new( 202 kind, 203 created_at, 204 vec![ 205 tag_values(["h".to_owned(), group_id.as_str().to_owned()]), 206 tag_values(["p".to_owned(), target_pubkey.as_str().to_owned()]), 207 ], 208 "", 209 ) 210 } 211 212 fn tag_values<const N: usize>(values: [String; N]) -> Vec<String> { 213 values.into_iter().collect() 214 } 215 216 #[cfg(test)] 217 mod tests { 218 use super::GroupGeneratedEventBuilder; 219 use crate::{ 220 GroupAuthority, GroupId, GroupMetadata, GroupProjection, GroupState, KIND_GROUP_ADMINS, 221 KIND_GROUP_MEMBERS, KIND_GROUP_METADATA, KIND_GROUP_PUT_USER, KIND_GROUP_REMOVE_USER, 222 MemberState, MemberStatus, ProjectionOrderTuple, StoreOffset, 223 }; 224 use pocket_types::Event as PocketEvent; 225 use tangle_crypto::RelaySigner; 226 use tangle_protocol::{EventId, PublicKeyHex, UnixTimestamp}; 227 228 #[test] 229 fn generated_metadata_event_is_relay_signed() { 230 let builder = builder(); 231 let group = group_state("Farm", GroupMetadata::empty()); 232 let event = builder 233 .sign_payload_pocket( 234 &GroupGeneratedEventBuilder::metadata_snapshot_payload( 235 &group, 236 UnixTimestamp::new(20), 237 ) 238 .expect("payload"), 239 ) 240 .expect("event"); 241 242 assert_eq!(u32::from(event.kind().as_u16()), KIND_GROUP_METADATA); 243 assert_eq!( 244 event.pubkey().as_hex_string(), 245 builder.relay_pubkey().as_str() 246 ); 247 assert!(has_pocket_tag(&event, &["d", "Farm"])); 248 event.verify().expect("signature"); 249 } 250 251 #[test] 252 fn generated_admin_event_includes_configured_and_override_admins() { 253 let builder = builder(); 254 let group_id = GroupId::new("Farm").expect("group"); 255 let owner = pubkey("1"); 256 let admin = pubkey("2"); 257 let override_member = pubkey("3"); 258 let mut projection = GroupProjection::new(); 259 projection.put_member( 260 group_id.clone(), 261 MemberState::new( 262 override_member.clone(), 263 MemberStatus::Member, 264 [crate::RoleName::permanent_relay_override()] 265 .into_iter() 266 .collect(), 267 event_id("30"), 268 tuple(30, "30", 3), 269 ), 270 ); 271 let event = builder 272 .sign_payload_pocket( 273 &GroupGeneratedEventBuilder::admin_list_snapshot_payload( 274 &group_id, 275 &projection, 276 &GroupAuthority::new([owner.clone()], [admin.clone()]), 277 UnixTimestamp::new(20), 278 ) 279 .expect("payload"), 280 ) 281 .expect("event"); 282 283 assert_eq!(u32::from(event.kind().as_u16()), KIND_GROUP_ADMINS); 284 for pubkey in [owner, admin, override_member] { 285 assert!(has_pocket_tag(&event, &["p", pubkey.as_str()])); 286 } 287 event.verify().expect("signature"); 288 } 289 290 #[test] 291 fn generated_member_snapshot_is_capped() { 292 let group_id = GroupId::new("Farm").expect("group"); 293 let mut projection = GroupProjection::new(); 294 projection.put_member( 295 group_id.clone(), 296 MemberState::new( 297 pubkey("1"), 298 MemberStatus::Member, 299 Default::default(), 300 event_id("10"), 301 tuple(10, "10", 1), 302 ), 303 ); 304 305 let payload = GroupGeneratedEventBuilder::member_list_snapshot_payload( 306 &group_id, 307 &projection, 308 UnixTimestamp::new(20), 309 1, 310 ) 311 .expect("payload") 312 .expect("under cap"); 313 assert_eq!(payload.generated_kind(), KIND_GROUP_MEMBERS); 314 assert!( 315 GroupGeneratedEventBuilder::member_list_snapshot_payload( 316 &group_id, 317 &projection, 318 UnixTimestamp::new(20), 319 0 320 ) 321 .expect("payload") 322 .is_none() 323 ); 324 } 325 326 #[test] 327 fn generated_membership_events_use_group_and_target_tags() { 328 let builder = builder(); 329 let group_id = GroupId::new("Farm").expect("group"); 330 let member = pubkey("4"); 331 let join = builder 332 .sign_payload_pocket(&GroupGeneratedEventBuilder::join_accepted_payload( 333 &group_id, 334 &member, 335 UnixTimestamp::new(20), 336 )) 337 .expect("join"); 338 let leave = builder 339 .sign_payload_pocket(&GroupGeneratedEventBuilder::leave_accepted_payload( 340 &group_id, 341 &member, 342 UnixTimestamp::new(21), 343 )) 344 .expect("leave"); 345 346 assert_eq!(u32::from(join.kind().as_u16()), KIND_GROUP_PUT_USER); 347 assert_eq!(u32::from(leave.kind().as_u16()), KIND_GROUP_REMOVE_USER); 348 for event in [&join, &leave] { 349 assert!(has_pocket_tag(event, &["h", "Farm"])); 350 assert!(has_pocket_tag(event, &["p", member.as_str()])); 351 event.verify().expect("signature"); 352 } 353 } 354 355 #[test] 356 fn generated_pocket_events_have_stable_ids_and_verify() { 357 let builder = builder(); 358 let group_id = GroupId::new("Farm").expect("group"); 359 let group = group_state("Farm", GroupMetadata::empty()); 360 let member = pubkey("4"); 361 let owner = pubkey("1"); 362 let admin = pubkey("2"); 363 let metadata = builder 364 .sign_payload_pocket( 365 &GroupGeneratedEventBuilder::metadata_snapshot_payload( 366 &group, 367 UnixTimestamp::new(20), 368 ) 369 .expect("payload"), 370 ) 371 .expect("metadata"); 372 let admins = builder 373 .sign_payload_pocket( 374 &GroupGeneratedEventBuilder::admin_list_snapshot_payload( 375 &group_id, 376 &GroupProjection::new(), 377 &GroupAuthority::new([owner.clone()], [admin.clone()]), 378 UnixTimestamp::new(20), 379 ) 380 .expect("payload"), 381 ) 382 .expect("admins"); 383 let mut projection = GroupProjection::new(); 384 projection.put_member( 385 group_id.clone(), 386 MemberState::new( 387 member.clone(), 388 MemberStatus::Member, 389 Default::default(), 390 event_id("30"), 391 tuple(30, "30", 3), 392 ), 393 ); 394 let members = builder 395 .sign_payload_pocket( 396 &GroupGeneratedEventBuilder::member_list_snapshot_payload( 397 &group_id, 398 &projection, 399 UnixTimestamp::new(20), 400 1, 401 ) 402 .expect("payload") 403 .expect("members"), 404 ) 405 .expect("members"); 406 let join = builder 407 .sign_payload_pocket(&GroupGeneratedEventBuilder::join_accepted_payload( 408 &group_id, 409 &member, 410 UnixTimestamp::new(20), 411 )) 412 .expect("join"); 413 let leave = builder 414 .sign_payload_pocket(&GroupGeneratedEventBuilder::leave_accepted_payload( 415 &group_id, 416 &member, 417 UnixTimestamp::new(21), 418 )) 419 .expect("leave"); 420 421 for (event, kind, event_id, expected_tags) in [ 422 ( 423 metadata, 424 KIND_GROUP_METADATA, 425 "b107997a285780bc383ee5aadc0a0eefc46734914103d80f765a46543622782a", 426 vec![vec!["d", "Farm"]], 427 ), 428 ( 429 admins, 430 KIND_GROUP_ADMINS, 431 "f7a2e2a721877794dbd367208eec08bd487cf1955ad60cb615ad77e67b0f66e3", 432 vec![ 433 vec!["d", "Farm"], 434 vec!["p", owner.as_str()], 435 vec!["p", admin.as_str()], 436 ], 437 ), 438 ( 439 members, 440 KIND_GROUP_MEMBERS, 441 "19aa593a5e6e34cda72286e75aef520c05b56eed07fdee71f0d63b3efee3f814", 442 vec![vec!["d", "Farm"], vec!["p", member.as_str()]], 443 ), 444 ( 445 join, 446 KIND_GROUP_PUT_USER, 447 "fcea9360ebfcae11580ce179bffd235dbcdf8093c223986780c0635c9fd720e3", 448 vec![vec!["h", "Farm"], vec!["p", member.as_str()]], 449 ), 450 ( 451 leave, 452 KIND_GROUP_REMOVE_USER, 453 "bcba4eb36d55752f9274bf8a3118822a5ac3479fdd23b86b592514c945bd7ee8", 454 vec![vec!["h", "Farm"], vec!["p", member.as_str()]], 455 ), 456 ] { 457 event.verify().expect("verify"); 458 assert_eq!(event.id().as_hex_string(), event_id); 459 assert_eq!(u32::from(event.kind().as_u16()), kind); 460 assert_eq!( 461 event.pubkey().as_hex_string(), 462 builder.relay_pubkey().as_str() 463 ); 464 assert_eq!(event.content(), b""); 465 for expected in expected_tags { 466 assert!(has_pocket_tag(&event, &expected)); 467 } 468 } 469 } 470 471 fn has_pocket_tag(event: &PocketEvent, expected: &[&str]) -> bool { 472 event.tags().expect("tags").iter().any(|tag| { 473 tag.map(|value| std::str::from_utf8(value).expect("tag")) 474 .eq(expected.iter().copied()) 475 }) 476 } 477 478 fn builder() -> GroupGeneratedEventBuilder { 479 GroupGeneratedEventBuilder::new(RelaySigner::from_secret_hex(&"7".repeat(64)).expect("key")) 480 } 481 482 fn group_state(group_id: &str, metadata: GroupMetadata) -> GroupState { 483 GroupState::new( 484 GroupId::new(group_id).expect("group"), 485 metadata, 486 pubkey("9"), 487 event_id("10"), 488 tuple(10, "10", 1), 489 ) 490 } 491 492 fn pubkey(suffix: &str) -> PublicKeyHex { 493 PublicKeyHex::new(&suffix.repeat(64)).expect("pubkey") 494 } 495 496 fn tuple(created_at: u64, suffix: &str, offset: u64) -> ProjectionOrderTuple { 497 ProjectionOrderTuple::new( 498 UnixTimestamp::new(created_at), 499 event_id(suffix), 500 StoreOffset::new(offset), 501 ) 502 } 503 504 fn event_id(suffix: &str) -> EventId { 505 let mut value = "0".repeat(64 - suffix.len()); 506 value.push_str(suffix); 507 EventId::new(&value).expect("event") 508 } 509 }