tangle


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

auth.rs (28872B)


      1 #![forbid(unsafe_code)]
      2 
      3 use crate::{
      4     errors::BaseRelayError,
      5     pocket_event_validation::{
      6         pocket_event_created_at, pocket_event_kind, pocket_event_pubkey,
      7         verify_pocket_event_signature,
      8     },
      9 };
     10 use std::collections::BTreeSet;
     11 use std::str;
     12 #[cfg(test)]
     13 use tangle_crypto::verify_event_signature;
     14 #[cfg(test)]
     15 use tangle_protocol::Event;
     16 use tangle_protocol::{PublicKeyHex, RelayMessage, UnixTimestamp};
     17 use tangle_store_pocket::PocketEvent;
     18 
     19 pub fn generate_auth_challenge() -> Result<String, BaseRelayError> {
     20     let mut bytes = [0_u8; 32];
     21     getrandom::fill(&mut bytes).map_err(|error| {
     22         BaseRelayError::error(format!("auth challenge generation failed: {error}"))
     23     })?;
     24     Ok(lower_hex(&bytes))
     25 }
     26 
     27 #[derive(Debug, Clone, PartialEq, Eq)]
     28 pub struct BaseAuthState {
     29     relay_url: String,
     30     accepted_relay_urls: BTreeSet<String>,
     31     challenge_ttl_seconds: u64,
     32     created_at_skew_seconds: u64,
     33     challenge: Option<BaseAuthChallenge>,
     34     authenticated_pubkeys: BTreeSet<PublicKeyHex>,
     35 }
     36 
     37 impl BaseAuthState {
     38     pub fn new(
     39         relay_url: impl Into<String>,
     40         challenge_ttl_seconds: u64,
     41         created_at_skew_seconds: u64,
     42     ) -> Result<Self, BaseRelayError> {
     43         let relay_url = relay_url.into();
     44         if relay_url.trim().is_empty() {
     45             return Err(BaseRelayError::invalid("auth relay URL must not be empty"));
     46         }
     47         if challenge_ttl_seconds == 0 {
     48             return Err(BaseRelayError::invalid(
     49                 "auth challenge ttl must be greater than zero",
     50             ));
     51         }
     52         if created_at_skew_seconds == 0 {
     53             return Err(BaseRelayError::invalid(
     54                 "auth created_at skew must be greater than zero",
     55             ));
     56         }
     57         Ok(Self {
     58             relay_url,
     59             accepted_relay_urls: BTreeSet::new(),
     60             challenge_ttl_seconds,
     61             created_at_skew_seconds,
     62             challenge: None,
     63             authenticated_pubkeys: BTreeSet::new(),
     64         })
     65     }
     66 
     67     pub fn accept_relay_url(&mut self, relay_url: impl Into<String>) -> Result<(), BaseRelayError> {
     68         let relay_url = relay_url.into();
     69         if relay_url.trim().is_empty() {
     70             return Err(BaseRelayError::invalid("auth relay URL must not be empty"));
     71         }
     72         self.accepted_relay_urls.insert(relay_url);
     73         Ok(())
     74     }
     75 
     76     pub fn issue_challenge(
     77         &mut self,
     78         challenge: impl Into<String>,
     79         issued_at: UnixTimestamp,
     80     ) -> Result<RelayMessage, BaseRelayError> {
     81         let challenge = challenge.into();
     82         if challenge.is_empty() {
     83             return Err(BaseRelayError::invalid("auth challenge must not be empty"));
     84         }
     85         self.challenge = Some(BaseAuthChallenge {
     86             value: challenge.clone(),
     87             issued_at,
     88         });
     89         Ok(RelayMessage::Auth(challenge))
     90     }
     91 
     92     #[cfg(test)]
     93     pub fn authenticate(
     94         &mut self,
     95         event: &Event,
     96         now: UnixTimestamp,
     97     ) -> Result<PublicKeyHex, BaseRelayError> {
     98         verify_event_signature(event).map_err(BaseRelayError::invalid)?;
     99         let auth = parse_base_relay_auth_event(event)
    100             .map_err(BaseRelayError::invalid)?
    101             .ok_or_else(|| BaseRelayError::invalid("AUTH message must contain kind 22242"))?;
    102         let challenge = self
    103             .challenge
    104             .as_ref()
    105             .ok_or_else(|| BaseRelayError::auth_required("auth challenge is missing"))?;
    106         if !self.accepts_relay_url(auth.relay()) {
    107             return Err(BaseRelayError::auth_required(
    108                 "auth relay does not match accepted relay URL",
    109             ));
    110         }
    111         if auth.challenge() != challenge.value {
    112             return Err(BaseRelayError::auth_required(
    113                 "auth challenge does not match",
    114             ));
    115         }
    116         if now.as_u64()
    117             > challenge
    118                 .issued_at
    119                 .as_u64()
    120                 .saturating_add(self.challenge_ttl_seconds)
    121         {
    122             return Err(BaseRelayError::auth_required("auth challenge expired"));
    123         }
    124         if auth
    125             .created_at()
    126             .as_u64()
    127             .saturating_add(self.created_at_skew_seconds)
    128             < now.as_u64()
    129             || auth.created_at().as_u64()
    130                 > now.as_u64().saturating_add(self.created_at_skew_seconds)
    131         {
    132             return Err(BaseRelayError::auth_required(
    133                 "auth event created_at is outside configured skew",
    134             ));
    135         }
    136         let pubkey = auth.pubkey().clone();
    137         self.authenticated_pubkeys.insert(pubkey.clone());
    138         Ok(pubkey)
    139     }
    140 
    141     pub fn authenticate_pocket(
    142         &mut self,
    143         event: &PocketEvent,
    144         now: UnixTimestamp,
    145     ) -> Result<PublicKeyHex, BaseRelayError> {
    146         verify_pocket_event_signature(event)?;
    147         let auth = parse_base_relay_pocket_auth_event(event)
    148             .map_err(BaseRelayError::invalid)?
    149             .ok_or_else(|| BaseRelayError::invalid("AUTH message must contain kind 22242"))?;
    150         let challenge = self
    151             .challenge
    152             .as_ref()
    153             .ok_or_else(|| BaseRelayError::auth_required("auth challenge is missing"))?;
    154         if !self.accepts_relay_url(auth.relay()) {
    155             return Err(BaseRelayError::auth_required(
    156                 "auth relay does not match accepted relay URL",
    157             ));
    158         }
    159         if auth.challenge() != challenge.value {
    160             return Err(BaseRelayError::auth_required(
    161                 "auth challenge does not match",
    162             ));
    163         }
    164         if now.as_u64()
    165             > challenge
    166                 .issued_at
    167                 .as_u64()
    168                 .saturating_add(self.challenge_ttl_seconds)
    169         {
    170             return Err(BaseRelayError::auth_required("auth challenge expired"));
    171         }
    172         if auth
    173             .created_at()
    174             .as_u64()
    175             .saturating_add(self.created_at_skew_seconds)
    176             < now.as_u64()
    177             || auth.created_at().as_u64()
    178                 > now.as_u64().saturating_add(self.created_at_skew_seconds)
    179         {
    180             return Err(BaseRelayError::auth_required(
    181                 "auth event created_at is outside configured skew",
    182             ));
    183         }
    184         let pubkey = auth.pubkey().clone();
    185         self.authenticated_pubkeys.insert(pubkey.clone());
    186         Ok(pubkey)
    187     }
    188 
    189     pub fn authenticated_pubkeys(&self) -> &BTreeSet<PublicKeyHex> {
    190         &self.authenticated_pubkeys
    191     }
    192 
    193     fn accepts_relay_url(&self, relay_url: &str) -> bool {
    194         relay_url == self.relay_url || self.accepted_relay_urls.contains(relay_url)
    195     }
    196 }
    197 
    198 #[derive(Debug, Clone, PartialEq, Eq)]
    199 struct BaseAuthChallenge {
    200     value: String,
    201     issued_at: UnixTimestamp,
    202 }
    203 
    204 #[derive(Debug, Clone, PartialEq, Eq)]
    205 struct BaseRelayAuthEvent {
    206     pubkey: PublicKeyHex,
    207     created_at: UnixTimestamp,
    208     relay: String,
    209     challenge: String,
    210 }
    211 
    212 impl BaseRelayAuthEvent {
    213     fn pubkey(&self) -> &PublicKeyHex {
    214         &self.pubkey
    215     }
    216 
    217     fn created_at(&self) -> UnixTimestamp {
    218         self.created_at
    219     }
    220 
    221     fn relay(&self) -> &str {
    222         &self.relay
    223     }
    224 
    225     fn challenge(&self) -> &str {
    226         &self.challenge
    227     }
    228 }
    229 
    230 #[cfg(test)]
    231 fn parse_base_relay_auth_event(event: &Event) -> Result<Option<BaseRelayAuthEvent>, String> {
    232     if event.unsigned().kind().as_u32() != 22_242 {
    233         return Ok(None);
    234     }
    235     let relay = required_single_tag_value(event, "relay")?;
    236     let challenge = required_single_tag_value(event, "challenge")?;
    237     if relay.is_empty() {
    238         return Err("relay auth relay tag must not be empty".to_owned());
    239     }
    240     if challenge.is_empty() {
    241         return Err("relay auth challenge tag must not be empty".to_owned());
    242     }
    243     Ok(Some(BaseRelayAuthEvent {
    244         pubkey: event.unsigned().pubkey().clone(),
    245         created_at: event.unsigned().created_at(),
    246         relay,
    247         challenge,
    248     }))
    249 }
    250 
    251 fn parse_base_relay_pocket_auth_event(
    252     event: &PocketEvent,
    253 ) -> Result<Option<BaseRelayAuthEvent>, String> {
    254     if pocket_event_kind(event)
    255         .map_err(|error| error.message().to_owned())?
    256         .as_u32()
    257         != 22_242
    258     {
    259         return Ok(None);
    260     }
    261     let relay = required_single_pocket_tag_value(event, "relay")?;
    262     let challenge = required_single_pocket_tag_value(event, "challenge")?;
    263     if relay.is_empty() {
    264         return Err("relay auth relay tag must not be empty".to_owned());
    265     }
    266     if challenge.is_empty() {
    267         return Err("relay auth challenge tag must not be empty".to_owned());
    268     }
    269     Ok(Some(BaseRelayAuthEvent {
    270         pubkey: pocket_event_pubkey(event).map_err(|error| error.message().to_owned())?,
    271         created_at: pocket_event_created_at(event),
    272         relay,
    273         challenge,
    274     }))
    275 }
    276 
    277 #[cfg(test)]
    278 fn required_single_tag_value(event: &Event, name: &str) -> Result<String, String> {
    279     let mut matches = event
    280         .unsigned()
    281         .tags()
    282         .iter()
    283         .filter(|tag| tag.name().as_str() == name);
    284     let tag = matches
    285         .next()
    286         .ok_or_else(|| format!("tag `{name}` is required"))?;
    287     if matches.next().is_some() {
    288         return Err(format!("tag `{name}` must not be repeated"));
    289     }
    290     tag.values()
    291         .get(1)
    292         .cloned()
    293         .ok_or_else(|| format!("tag `{name}` must include a value"))
    294 }
    295 
    296 fn required_single_pocket_tag_value(event: &PocketEvent, name: &str) -> Result<String, String> {
    297     let tags = event
    298         .tags()
    299         .map_err(|error| format!("malformed Pocket event tags: {error}"))?;
    300     let mut matched = None;
    301     for mut tag in tags.iter() {
    302         let Some(tag_name) = tag.next() else {
    303             continue;
    304         };
    305         let tag_name = str::from_utf8(tag_name).map_err(|error| error.to_string())?;
    306         if tag_name != name {
    307             continue;
    308         }
    309         if matched.is_some() {
    310             return Err(format!("tag `{name}` must not be repeated"));
    311         }
    312         let value = tag
    313             .next()
    314             .ok_or_else(|| format!("tag `{name}` must include a value"))
    315             .and_then(|value| str::from_utf8(value).map_err(|error| error.to_string()))?;
    316         matched = Some(value.to_owned());
    317     }
    318     matched.ok_or_else(|| format!("tag `{name}` is required"))
    319 }
    320 
    321 fn lower_hex(bytes: &[u8]) -> String {
    322     const HEX: &[u8; 16] = b"0123456789abcdef";
    323     let mut output = String::with_capacity(bytes.len() * 2);
    324     for byte in bytes {
    325         output.push(HEX[(byte >> 4) as usize] as char);
    326         output.push(HEX[(byte & 0x0f) as usize] as char);
    327     }
    328     output
    329 }
    330 
    331 #[cfg(test)]
    332 mod tests {
    333     use super::{BaseAuthState, generate_auth_challenge};
    334     use tangle_crypto::RelaySigner;
    335     use tangle_protocol::{Event, EventId, Kind, RelayMessage, Tag, UnixTimestamp, UnsignedEvent};
    336     use tangle_store_pocket::{PocketKind, PocketOwnedEvent, PocketOwnedTags, PocketTime};
    337 
    338     #[test]
    339     fn auth_state_issues_challenges_and_accepts_multiple_pubkeys() {
    340         let mut auth =
    341             BaseAuthState::new("wss://relay.radroots.test", 60, 600).expect("auth state");
    342         let issued = UnixTimestamp::new(100);
    343 
    344         assert_eq!(
    345             auth.issue_challenge("challenge-a", issued)
    346                 .expect("challenge"),
    347             RelayMessage::Auth("challenge-a".to_owned())
    348         );
    349 
    350         let first = signed_auth_event(7, "challenge-a", 120);
    351         let second = signed_auth_event(8, "challenge-a", 130);
    352 
    353         let first_pubkey = auth
    354             .authenticate(&first, UnixTimestamp::new(120))
    355             .expect("first");
    356         let second_pubkey = auth
    357             .authenticate(&second, UnixTimestamp::new(130))
    358             .expect("second");
    359 
    360         assert_ne!(first_pubkey, second_pubkey);
    361         assert!(auth.authenticated_pubkeys().contains(&first_pubkey));
    362         assert!(auth.authenticated_pubkeys().contains(&second_pubkey));
    363         assert_eq!(auth.authenticated_pubkeys().len(), 2);
    364         assert_eq!(
    365             auth.authenticate(&signed_auth_event(9, "wrong", 130), UnixTimestamp::new(130))
    366                 .expect_err("wrong")
    367                 .prefixed_message(),
    368             "auth-required: auth challenge does not match"
    369         );
    370     }
    371 
    372     #[test]
    373     fn auth_state_rejects_invalid_event_shape_and_signature() {
    374         let mut auth =
    375             BaseAuthState::new("wss://relay.radroots.test", 60, 600).expect("auth state");
    376         auth.issue_challenge("challenge-a", UnixTimestamp::new(100))
    377             .expect("challenge");
    378         let valid = signed_auth_event(7, "challenge-a", 120);
    379         let wrong_id = Event::new(
    380             EventId::new(&"0".repeat(EventId::HEX_LENGTH)).expect("id"),
    381             valid.unsigned().clone(),
    382             valid.sig().clone(),
    383         );
    384         assert!(
    385             auth.authenticate(&wrong_id, UnixTimestamp::new(120))
    386                 .expect_err("id")
    387                 .prefixed_message()
    388                 .starts_with("invalid: event id mismatch:")
    389         );
    390 
    391         let other = signed_auth_event(8, "challenge-a", 120);
    392         let wrong_signature = Event::new(
    393             valid.id().clone(),
    394             valid.unsigned().clone(),
    395             other.sig().clone(),
    396         );
    397         assert_eq!(
    398             auth.authenticate(&wrong_signature, UnixTimestamp::new(120))
    399                 .expect_err("signature")
    400                 .prefixed_message(),
    401             "invalid: event signature verification failed"
    402         );
    403 
    404         assert_eq!(
    405             auth.authenticate(
    406                 &signed_event(7, 1, auth_tags("challenge-a"), 120),
    407                 UnixTimestamp::new(120)
    408             )
    409             .expect_err("kind")
    410             .prefixed_message(),
    411             "invalid: AUTH message must contain kind 22242"
    412         );
    413 
    414         assert_eq!(
    415             auth.authenticate(
    416                 &signed_event(
    417                     7,
    418                     22_242,
    419                     vec![Tag::from_parts("challenge", &["challenge-a"]).expect("challenge")],
    420                     120
    421                 ),
    422                 UnixTimestamp::new(120)
    423             )
    424             .expect_err("relay")
    425             .prefixed_message(),
    426             "invalid: tag `relay` is required"
    427         );
    428 
    429         assert_eq!(
    430             auth.authenticate(
    431                 &signed_event(
    432                     7,
    433                     22_242,
    434                     vec![Tag::from_parts("relay", &["wss://relay.radroots.test"]).expect("relay")],
    435                     120
    436                 ),
    437                 UnixTimestamp::new(120)
    438             )
    439             .expect_err("challenge")
    440             .prefixed_message(),
    441             "invalid: tag `challenge` is required"
    442         );
    443     }
    444 
    445     #[test]
    446     fn auth_state_rejects_created_at_outside_configured_skew() {
    447         let mut auth = BaseAuthState::new("wss://relay.radroots.test", 60, 10).expect("auth state");
    448         auth.issue_challenge("challenge-a", UnixTimestamp::new(100))
    449             .expect("challenge");
    450 
    451         auth.authenticate(
    452             &signed_auth_event(7, "challenge-a", 90),
    453             UnixTimestamp::new(100),
    454         )
    455         .expect("lower boundary");
    456         auth.authenticate(
    457             &signed_auth_event(8, "challenge-a", 110),
    458             UnixTimestamp::new(100),
    459         )
    460         .expect("upper boundary");
    461 
    462         assert_eq!(
    463             auth.authenticate(
    464                 &signed_auth_event(9, "challenge-a", 89),
    465                 UnixTimestamp::new(100)
    466             )
    467             .expect_err("stale")
    468             .prefixed_message(),
    469             "auth-required: auth event created_at is outside configured skew"
    470         );
    471         assert_eq!(
    472             auth.authenticate(
    473                 &signed_auth_event(10, "challenge-a", 111),
    474                 UnixTimestamp::new(100)
    475             )
    476             .expect_err("future")
    477             .prefixed_message(),
    478             "auth-required: auth event created_at is outside configured skew"
    479         );
    480     }
    481 
    482     #[test]
    483     fn auth_state_accepts_explicit_additional_relay_urls() {
    484         let mut auth =
    485             BaseAuthState::new("wss://relay.radroots.test", 60, 600).expect("auth state");
    486         assert_eq!(
    487             auth.accept_relay_url("")
    488                 .expect_err("empty alias")
    489                 .prefixed_message(),
    490             "invalid: auth relay URL must not be empty"
    491         );
    492         auth.accept_relay_url("wss://relay.radroots.test/es")
    493             .expect("alias");
    494         auth.issue_challenge("challenge-a", UnixTimestamp::new(100))
    495             .expect("challenge");
    496 
    497         let alias_event = signed_event(
    498             7,
    499             22_242,
    500             auth_tags_for("wss://relay.radroots.test/es", "challenge-a"),
    501             105,
    502         );
    503         let base_event = signed_auth_event(8, "challenge-a", 106);
    504         let alias_pocket_event = signed_pocket_event(
    505             9,
    506             22_242,
    507             pocket_auth_tags_for("wss://relay.radroots.test/es", "challenge-a"),
    508             107,
    509         );
    510 
    511         let alias_pubkey = auth
    512             .authenticate(&alias_event, UnixTimestamp::new(105))
    513             .expect("alias event");
    514         let base_pubkey = auth
    515             .authenticate(&base_event, UnixTimestamp::new(106))
    516             .expect("base event");
    517         let alias_pocket_pubkey = auth
    518             .authenticate_pocket(&alias_pocket_event, UnixTimestamp::new(107))
    519             .expect("alias pocket event");
    520 
    521         assert!(auth.authenticated_pubkeys().contains(&alias_pubkey));
    522         assert!(auth.authenticated_pubkeys().contains(&base_pubkey));
    523         assert!(auth.authenticated_pubkeys().contains(&alias_pocket_pubkey));
    524         assert_eq!(auth.authenticated_pubkeys().len(), 3);
    525     }
    526 
    527     #[test]
    528     fn auth_state_preserves_chorus_auth_parity() {
    529         let mut auth = BaseAuthState::new("wss://relay.radroots.test", 20, 10).expect("auth state");
    530         auth.issue_challenge("challenge-a", UnixTimestamp::new(100))
    531             .expect("challenge");
    532         let owner = signed_auth_event(7, "challenge-a", 105);
    533         let admin = signed_auth_event(8, "challenge-a", 106);
    534 
    535         let owner_pubkey = auth
    536             .authenticate(&owner, UnixTimestamp::new(105))
    537             .expect("owner");
    538         let admin_pubkey = auth
    539             .authenticate(&admin, UnixTimestamp::new(106))
    540             .expect("admin");
    541         assert_ne!(owner_pubkey, admin_pubkey);
    542         assert!(auth.authenticated_pubkeys().contains(&owner_pubkey));
    543         assert!(auth.authenticated_pubkeys().contains(&admin_pubkey));
    544         assert_eq!(auth.authenticated_pubkeys().len(), 2);
    545 
    546         let wrong_id = Event::new(
    547             EventId::new(&"0".repeat(EventId::HEX_LENGTH)).expect("id"),
    548             owner.unsigned().clone(),
    549             owner.sig().clone(),
    550         );
    551         assert!(
    552             auth.authenticate(&wrong_id, UnixTimestamp::new(105))
    553                 .expect_err("id")
    554                 .prefixed_message()
    555                 .starts_with("invalid: event id mismatch:")
    556         );
    557 
    558         let wrong_signature = Event::new(
    559             owner.id().clone(),
    560             owner.unsigned().clone(),
    561             admin.sig().clone(),
    562         );
    563         assert_eq!(
    564             auth.authenticate(&wrong_signature, UnixTimestamp::new(105))
    565                 .expect_err("signature")
    566                 .prefixed_message(),
    567             "invalid: event signature verification failed"
    568         );
    569         assert_eq!(
    570             auth.authenticate(
    571                 &signed_event(9, 1, auth_tags("challenge-a"), 105),
    572                 UnixTimestamp::new(105)
    573             )
    574             .expect_err("kind")
    575             .prefixed_message(),
    576             "invalid: AUTH message must contain kind 22242"
    577         );
    578         assert_eq!(
    579             auth.authenticate(
    580                 &signed_event(
    581                     9,
    582                     22_242,
    583                     auth_tags_for("wss://other.radroots.test", "challenge-a"),
    584                     105
    585                 ),
    586                 UnixTimestamp::new(105)
    587             )
    588             .expect_err("relay")
    589             .prefixed_message(),
    590             "auth-required: auth relay does not match accepted relay URL"
    591         );
    592         assert_eq!(
    593             auth.authenticate(
    594                 &signed_event(
    595                     9,
    596                     22_242,
    597                     vec![Tag::from_parts("challenge", &["challenge-a"]).expect("challenge")],
    598                     105
    599                 ),
    600                 UnixTimestamp::new(105)
    601             )
    602             .expect_err("missing relay")
    603             .prefixed_message(),
    604             "invalid: tag `relay` is required"
    605         );
    606         assert_eq!(
    607             auth.authenticate(
    608                 &signed_event(
    609                     9,
    610                     22_242,
    611                     vec![Tag::from_parts("relay", &["wss://relay.radroots.test"]).expect("relay")],
    612                     105
    613                 ),
    614                 UnixTimestamp::new(105)
    615             )
    616             .expect_err("missing challenge")
    617             .prefixed_message(),
    618             "invalid: tag `challenge` is required"
    619         );
    620         assert_eq!(
    621             auth.authenticate(&signed_auth_event(9, "wrong", 105), UnixTimestamp::new(105))
    622                 .expect_err("challenge")
    623                 .prefixed_message(),
    624             "auth-required: auth challenge does not match"
    625         );
    626         assert_eq!(
    627             auth.authenticate(
    628                 &signed_auth_event(9, "challenge-a", 121),
    629                 UnixTimestamp::new(121)
    630             )
    631             .expect_err("expired")
    632             .prefixed_message(),
    633             "auth-required: auth challenge expired"
    634         );
    635         assert_eq!(
    636             auth.authenticate(
    637                 &signed_auth_event(9, "challenge-a", 94),
    638                 UnixTimestamp::new(105)
    639             )
    640             .expect_err("stale")
    641             .prefixed_message(),
    642             "auth-required: auth event created_at is outside configured skew"
    643         );
    644         assert_eq!(
    645             auth.authenticate(
    646                 &signed_auth_event(9, "challenge-a", 116),
    647                 UnixTimestamp::new(105)
    648             )
    649             .expect_err("future")
    650             .prefixed_message(),
    651             "auth-required: auth event created_at is outside configured skew"
    652         );
    653     }
    654 
    655     #[test]
    656     fn auth_state_authenticates_pocket_events_without_protocol_conversion() {
    657         let mut auth = BaseAuthState::new("wss://relay.radroots.test", 20, 10).expect("auth state");
    658         auth.issue_challenge("challenge-a", UnixTimestamp::new(100))
    659             .expect("challenge");
    660         let owner = signed_pocket_auth_event(7, "challenge-a", 105);
    661         let admin = signed_pocket_auth_event(8, "challenge-a", 106);
    662 
    663         let owner_pubkey = auth
    664             .authenticate_pocket(&owner, UnixTimestamp::new(105))
    665             .expect("owner");
    666         let admin_pubkey = auth
    667             .authenticate_pocket(&admin, UnixTimestamp::new(106))
    668             .expect("admin");
    669 
    670         assert_ne!(owner_pubkey, admin_pubkey);
    671         assert!(auth.authenticated_pubkeys().contains(&owner_pubkey));
    672         assert!(auth.authenticated_pubkeys().contains(&admin_pubkey));
    673         assert_eq!(auth.authenticated_pubkeys().len(), 2);
    674     }
    675 
    676     #[test]
    677     fn auth_state_rejects_invalid_pocket_auth_events_with_existing_semantics() {
    678         let mut auth = BaseAuthState::new("wss://relay.radroots.test", 20, 10).expect("auth state");
    679         auth.issue_challenge("challenge-a", UnixTimestamp::new(100))
    680             .expect("challenge");
    681         let owner = signed_pocket_auth_event(7, "challenge-a", 105);
    682         let admin = signed_pocket_auth_event(8, "challenge-a", 105);
    683 
    684         let id_source = signed_pocket_auth_event(7, "challenge-a", 106);
    685         let wrong_id = PocketOwnedEvent::new(
    686             id_source.id(),
    687             owner.kind(),
    688             owner.pubkey(),
    689             owner.sig(),
    690             owner.tags().expect("tags"),
    691             owner.created_at(),
    692             owner.content(),
    693         )
    694         .expect("wrong id pocket");
    695         assert!(
    696             auth.authenticate_pocket(&wrong_id, UnixTimestamp::new(105))
    697                 .expect_err("id")
    698                 .prefixed_message()
    699                 .starts_with("invalid:")
    700         );
    701 
    702         let wrong_signature = PocketOwnedEvent::new(
    703             owner.id(),
    704             owner.kind(),
    705             owner.pubkey(),
    706             admin.sig(),
    707             owner.tags().expect("tags"),
    708             owner.created_at(),
    709             owner.content(),
    710         )
    711         .expect("wrong signature pocket");
    712         assert!(
    713             auth.authenticate_pocket(&wrong_signature, UnixTimestamp::new(105))
    714                 .expect_err("signature")
    715                 .prefixed_message()
    716                 .starts_with("invalid:")
    717         );
    718 
    719         for (event, now, expected) in [
    720             (
    721                 signed_pocket_event(9, 1, pocket_auth_tags("challenge-a"), 105),
    722                 105,
    723                 "invalid: AUTH message must contain kind 22242",
    724             ),
    725             (
    726                 signed_pocket_event(
    727                     9,
    728                     22_242,
    729                     pocket_auth_tags_for("wss://other.radroots.test", "challenge-a"),
    730                     105,
    731                 ),
    732                 105,
    733                 "auth-required: auth relay does not match accepted relay URL",
    734             ),
    735             (
    736                 signed_pocket_auth_event(9, "wrong", 105),
    737                 105,
    738                 "auth-required: auth challenge does not match",
    739             ),
    740             (
    741                 signed_pocket_auth_event(9, "challenge-a", 121),
    742                 121,
    743                 "auth-required: auth challenge expired",
    744             ),
    745             (
    746                 signed_pocket_auth_event(9, "challenge-a", 94),
    747                 105,
    748                 "auth-required: auth event created_at is outside configured skew",
    749             ),
    750             (
    751                 signed_pocket_auth_event(9, "challenge-a", 116),
    752                 105,
    753                 "auth-required: auth event created_at is outside configured skew",
    754             ),
    755         ] {
    756             assert_eq!(
    757                 auth.authenticate_pocket(&event, UnixTimestamp::new(now))
    758                     .expect_err("invalid")
    759                     .prefixed_message(),
    760                 expected
    761             );
    762         }
    763     }
    764 
    765     #[test]
    766     fn generated_auth_challenge_is_lowercase_hex_nonce() {
    767         let first = generate_auth_challenge().expect("first");
    768         let second = generate_auth_challenge().expect("second");
    769 
    770         assert_eq!(first.len(), 64);
    771         assert_ne!(first, second);
    772         assert!(first.bytes().all(|byte| byte.is_ascii_hexdigit()));
    773         assert_eq!(first, first.to_ascii_lowercase());
    774     }
    775 
    776     fn signed_auth_event(secret_byte: u8, challenge: &str, created_at: u64) -> Event {
    777         signed_event(secret_byte, 22_242, auth_tags(challenge), created_at)
    778     }
    779 
    780     fn signed_pocket_auth_event(
    781         secret_byte: u8,
    782         challenge: &str,
    783         created_at: u64,
    784     ) -> PocketOwnedEvent {
    785         signed_pocket_event(secret_byte, 22_242, pocket_auth_tags(challenge), created_at)
    786     }
    787 
    788     fn signed_event(secret_byte: u8, kind: u64, tags: Vec<Tag>, created_at: u64) -> Event {
    789         let secret = format!("{:02x}", secret_byte).repeat(32);
    790         let signer = RelaySigner::from_secret_hex(&secret).expect("signer");
    791         let unsigned = UnsignedEvent::new(
    792             signer.public_key().clone(),
    793             UnixTimestamp::new(created_at),
    794             Kind::new(kind).expect("kind"),
    795             tags,
    796             "",
    797         );
    798         signer.sign_unsigned_event(unsigned)
    799     }
    800 
    801     fn signed_pocket_event(
    802         secret_byte: u8,
    803         kind: u16,
    804         tags: PocketOwnedTags,
    805         created_at: u64,
    806     ) -> PocketOwnedEvent {
    807         let secret = format!("{secret_byte:02x}").repeat(32);
    808         RelaySigner::from_secret_hex(&secret)
    809             .expect("signer")
    810             .sign_pocket_event(
    811                 PocketKind::from_u16(kind),
    812                 &tags,
    813                 PocketTime::from_u64(created_at),
    814                 b"",
    815             )
    816             .expect("pocket event")
    817     }
    818 
    819     fn auth_tags(challenge: &str) -> Vec<Tag> {
    820         auth_tags_for("wss://relay.radroots.test", challenge)
    821     }
    822 
    823     fn auth_tags_for(relay: &str, challenge: &str) -> Vec<Tag> {
    824         vec![
    825             Tag::from_parts("relay", &[relay]).expect("relay"),
    826             Tag::from_parts("challenge", &[challenge]).expect("challenge"),
    827         ]
    828     }
    829 
    830     fn pocket_auth_tags(challenge: &str) -> PocketOwnedTags {
    831         pocket_auth_tags_for("wss://relay.radroots.test", challenge)
    832     }
    833 
    834     fn pocket_auth_tags_for(relay: &str, challenge: &str) -> PocketOwnedTags {
    835         PocketOwnedTags::new(&[["relay", relay], ["challenge", challenge]]).expect("tags")
    836     }
    837 }