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 }