tangle


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

commit 5ed4d80f2470e3d5693c5800a3ed6595d34a2f41
parent 94a60f76952af42db453ba2d127c103b871b73ac
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 17:49:12 -0700

runtime: prove auth chorus parity

Diffstat:
Mcrates/tangle_runtime/src/relay/auth.rs | 134++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/tangle_runtime/src/runtime.rs | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 210 insertions(+), 1 deletion(-)

diff --git a/crates/tangle_runtime/src/relay/auth.rs b/crates/tangle_runtime/src/relay/auth.rs @@ -351,6 +351,134 @@ mod tests { } #[test] + fn auth_state_preserves_chorus_auth_parity() { + let mut auth = BaseAuthState::new("wss://relay.radroots.test", 20, 10).expect("auth state"); + auth.issue_challenge("challenge-a", UnixTimestamp::new(100)) + .expect("challenge"); + let owner = signed_auth_event(7, "challenge-a", 105); + let admin = signed_auth_event(8, "challenge-a", 106); + + let owner_pubkey = auth + .authenticate(&owner, UnixTimestamp::new(105)) + .expect("owner"); + let admin_pubkey = auth + .authenticate(&admin, UnixTimestamp::new(106)) + .expect("admin"); + assert_ne!(owner_pubkey, admin_pubkey); + assert!(auth.authenticated_pubkeys().contains(&owner_pubkey)); + assert!(auth.authenticated_pubkeys().contains(&admin_pubkey)); + assert_eq!(auth.authenticated_pubkeys().len(), 2); + + let wrong_id = Event::new( + EventId::new(&"0".repeat(EventId::HEX_LENGTH)).expect("id"), + owner.unsigned().clone(), + owner.sig().clone(), + ); + assert!( + auth.authenticate(&wrong_id, UnixTimestamp::new(105)) + .expect_err("id") + .prefixed_message() + .starts_with("invalid: event id mismatch:") + ); + + let wrong_signature = Event::new( + owner.id().clone(), + owner.unsigned().clone(), + admin.sig().clone(), + ); + assert_eq!( + auth.authenticate(&wrong_signature, UnixTimestamp::new(105)) + .expect_err("signature") + .prefixed_message(), + "invalid: event signature verification failed" + ); + assert_eq!( + auth.authenticate( + &signed_event(9, 1, auth_tags("challenge-a"), 105), + UnixTimestamp::new(105) + ) + .expect_err("kind") + .prefixed_message(), + "invalid: AUTH message must contain kind 22242" + ); + assert_eq!( + auth.authenticate( + &signed_event( + 9, + 22_242, + auth_tags_for("wss://other.radroots.test", "challenge-a"), + 105 + ), + UnixTimestamp::new(105) + ) + .expect_err("relay") + .prefixed_message(), + "auth-required: auth relay does not match canonical relay URL" + ); + assert_eq!( + auth.authenticate( + &signed_event( + 9, + 22_242, + vec![Tag::from_parts("challenge", &["challenge-a"]).expect("challenge")], + 105 + ), + UnixTimestamp::new(105) + ) + .expect_err("missing relay") + .prefixed_message(), + "invalid: tag `relay` is required" + ); + assert_eq!( + auth.authenticate( + &signed_event( + 9, + 22_242, + vec![Tag::from_parts("relay", &["wss://relay.radroots.test"]).expect("relay")], + 105 + ), + UnixTimestamp::new(105) + ) + .expect_err("missing challenge") + .prefixed_message(), + "invalid: tag `challenge` is required" + ); + assert_eq!( + auth.authenticate(&signed_auth_event(9, "wrong", 105), UnixTimestamp::new(105)) + .expect_err("challenge") + .prefixed_message(), + "auth-required: auth challenge does not match" + ); + assert_eq!( + auth.authenticate( + &signed_auth_event(9, "challenge-a", 121), + UnixTimestamp::new(121) + ) + .expect_err("expired") + .prefixed_message(), + "auth-required: auth challenge expired" + ); + assert_eq!( + auth.authenticate( + &signed_auth_event(9, "challenge-a", 94), + UnixTimestamp::new(105) + ) + .expect_err("stale") + .prefixed_message(), + "auth-required: auth event created_at is outside configured skew" + ); + assert_eq!( + auth.authenticate( + &signed_auth_event(9, "challenge-a", 116), + UnixTimestamp::new(105) + ) + .expect_err("future") + .prefixed_message(), + "auth-required: auth event created_at is outside configured skew" + ); + } + + #[test] fn generated_auth_challenge_is_lowercase_hex_nonce() { let first = generate_auth_challenge().expect("first"); let second = generate_auth_challenge().expect("second"); @@ -379,8 +507,12 @@ mod tests { } fn auth_tags(challenge: &str) -> Vec<Tag> { + auth_tags_for("wss://relay.radroots.test", challenge) + } + + fn auth_tags_for(relay: &str, challenge: &str) -> Vec<Tag> { vec![ - Tag::from_parts("relay", &["wss://relay.radroots.test"]).expect("relay"), + Tag::from_parts("relay", &[relay]).expect("relay"), Tag::from_parts("challenge", &[challenge]).expect("challenge"), ] } diff --git a/crates/tangle_runtime/src/runtime.rs b/crates/tangle_runtime/src/runtime.rs @@ -2235,6 +2235,83 @@ mod tests { } #[tokio::test] + async fn runtime_preserves_chorus_auth_failure_rate_limit_parity() { + let root = temp_root("runtime-chorus-auth-rate-limit-parity"); + let _ = std::fs::remove_dir_all(&root); + let runtime = TangleRuntime::open(runtime_config(&root, 8)).expect("runtime"); + let pubkey_event = + tangle_v2_event(FixtureKey::Member, 1_714_124_433, 22_242, Vec::new(), "") + .expect("pubkey auth event"); + let pubkey_rule = runtime.config().rate_limits().auth().failures(); + let pubkey_key = + TangleRateLimitKey::auth_failure(None, Some(pubkey_event.unsigned().pubkey().clone())); + for _ in 0..pubkey_rule.max_hits() { + runtime.rate_limiter().record( + pubkey_key.clone(), + pubkey_rule, + UnixTimestamp::new(1_714_124_433), + ); + } + let peer_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 41)); + let peer_event = tangle_v2_event(FixtureKey::Admin, 1_714_124_434, 22_242, Vec::new(), "") + .expect("peer auth event"); + let peer_rule = runtime.config().rate_limits().auth().failures_per_ip(); + let peer_key = TangleRateLimitKey::auth_failure(Some(peer_ip), None); + for _ in 0..peer_rule.max_hits() { + runtime.rate_limiter().record( + peer_key.clone(), + peer_rule, + UnixTimestamp::new(1_714_124_434), + ); + } + let handle = TangleRuntimeHandle::new(runtime); + let mut auth = handle.auth_state().await.expect("auth"); + + assert_eq!( + handle + .handle_client_message( + ClientMessage::Auth(pubkey_event.clone()), + &mut auth, + UnixTimestamp::new(1_714_124_433) + ) + .await + .expect("pubkey failure"), + vec![RelayMessage::Ok { + event_id: pubkey_event.id().clone(), + accepted: false, + message: "rate-limited: auth failure rate limit exceeded until 1714124733" + .to_owned() + }] + ); + assert_eq!( + handle + .handle_client_message_with_rate_limit_context( + ClientMessage::Auth(peer_event.clone()), + &mut auth, + TangleClientRateLimitContext::new(Some(peer_ip), None), + UnixTimestamp::new(1_714_124_434) + ) + .await + .expect("peer failure"), + vec![RelayMessage::Ok { + event_id: peer_event.id().clone(), + accepted: false, + message: "rate-limited: auth failure ip rate limit exceeded until 1714124734" + .to_owned() + }] + ); + assert!(auth.authenticated_pubkeys().is_empty()); + let snapshot = handle.metrics().snapshot(); + assert_eq!(snapshot.client_messages(), 2); + assert_eq!(snapshot.auth_messages(), 2); + assert_eq!(snapshot.rate_limit_rejections(), 2); + assert_eq!(handle.metrics().auth_successes(), 0); + assert_eq!(handle.metrics().auth_failures(), 2); + + let _ = std::fs::remove_dir_all(root); + } + + #[tokio::test] async fn runtime_rate_limits_group_writes_by_pubkey() { let root = temp_root("runtime-group-pubkey-rate-limit"); let _ = std::fs::remove_dir_all(&root);