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 }