tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

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 }