tangle


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

auth.rs (26525B)


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