commit 5ed4d80f2470e3d5693c5800a3ed6595d34a2f41
parent 94a60f76952af42db453ba2d127c103b871b73ac
Author: triesap <tyson@radroots.org>
Date: Sun, 14 Jun 2026 17:49:12 -0700
runtime: prove auth chorus parity
Diffstat:
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);