mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
fix: extract NIP-OA owner on open relays for observer frame auth (#513)
This commit is contained in:
@@ -40,13 +40,15 @@ pub mod relay_members {
|
||||
|
||||
/// Enforce relay membership for a pubkey, with NIP-OA agent delegation fallback.
|
||||
///
|
||||
/// - If `config.require_relay_membership` is false → always Ok (no-op).
|
||||
/// - If enabled → checks `relay_members` table for the pubkey.
|
||||
/// - If not a direct member and NIP-OA is enabled → verifies the `auth_tag_header`
|
||||
/// to check if the agent's owner is a relay member.
|
||||
/// Returns `Ok(Some(owner_pubkey))` when the agent is not a direct member but
|
||||
/// its NIP-OA owner *is* — access is granted via delegation.
|
||||
///
|
||||
/// Returns `Ok(None)` for direct members, `Ok(Some(owner_pubkey))` when
|
||||
/// access was granted via NIP-OA owner delegation.
|
||||
/// On open relays (`require_relay_membership = false`), returns `Ok(None)`
|
||||
/// immediately — no membership check is performed. Callers that need NIP-OA
|
||||
/// owner extraction on open relays should call [`extract_nip_oa_owner`] directly.
|
||||
///
|
||||
/// Returns `Ok(None)` when the caller is a direct member (closed relay) or when
|
||||
/// no NIP-OA tag is present/applicable (open relay without auth tag).
|
||||
pub async fn enforce_relay_membership(
|
||||
state: &AppState,
|
||||
pubkey_bytes: &[u8],
|
||||
@@ -107,4 +109,70 @@ pub mod relay_members {
|
||||
})),
|
||||
))
|
||||
}
|
||||
|
||||
/// Extract NIP-OA owner from an auth tag without membership enforcement.
|
||||
///
|
||||
/// Used on open relays (`require_relay_membership = false`) to opportunistically
|
||||
/// extract the owner pubkey for agent→owner backfill. The NIP-OA signature is
|
||||
/// cryptographically self-proving, so no feature flag is needed — if the tag
|
||||
/// verifies, the owner relationship is authentic. Returns `None` if the tag
|
||||
/// is absent or invalid.
|
||||
pub fn extract_nip_oa_owner(
|
||||
pubkey_bytes: &[u8],
|
||||
auth_tag_header: Option<&str>,
|
||||
) -> Option<nostr::PublicKey> {
|
||||
let tag_json = auth_tag_header?;
|
||||
let agent_pubkey = nostr::PublicKey::from_slice(pubkey_bytes).ok()?;
|
||||
match sprout_sdk::nip_oa::verify_auth_tag(tag_json, &agent_pubkey) {
|
||||
Ok(owner) => Some(owner),
|
||||
Err(e) => {
|
||||
debug!("extract_nip_oa_owner: invalid auth tag: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nostr::Keys;
|
||||
use sprout_sdk::nip_oa::compute_auth_tag;
|
||||
|
||||
/// Valid NIP-OA auth tag → returns Some(owner_pubkey).
|
||||
#[test]
|
||||
fn valid_nip_oa_returns_owner() {
|
||||
let owner_keys = Keys::generate();
|
||||
let agent_keys = Keys::generate();
|
||||
let agent_pubkey = agent_keys.public_key();
|
||||
|
||||
let tag_json = compute_auth_tag(&owner_keys, &agent_pubkey, "")
|
||||
.expect("compute_auth_tag must succeed");
|
||||
|
||||
let result = extract_nip_oa_owner(&agent_pubkey.to_bytes(), Some(&tag_json));
|
||||
|
||||
assert_eq!(result, Some(owner_keys.public_key()));
|
||||
}
|
||||
|
||||
/// No auth tag → returns None.
|
||||
#[test]
|
||||
fn no_auth_tag_returns_none() {
|
||||
let agent_keys = Keys::generate();
|
||||
let agent_pubkey = agent_keys.public_key();
|
||||
|
||||
let result = extract_nip_oa_owner(&agent_pubkey.to_bytes(), None);
|
||||
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
/// Invalid auth tag → returns None.
|
||||
#[test]
|
||||
fn invalid_auth_tag_returns_none() {
|
||||
let agent_keys = Keys::generate();
|
||||
let agent_pubkey = agent_keys.public_key();
|
||||
|
||||
let result = extract_nip_oa_owner(&agent_pubkey.to_bytes(), Some("not valid json"));
|
||||
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,11 @@ pub struct Config {
|
||||
/// bearing a valid NIP-OA `auth` tag can authenticate by proving their
|
||||
/// owner is a relay member. The agent gets session-scoped access.
|
||||
///
|
||||
/// On open relays (`require_relay_membership = false`), NIP-OA owner
|
||||
/// extraction for agent→owner backfill happens unconditionally (the
|
||||
/// signature is cryptographically self-proving). This flag only controls
|
||||
/// whether NIP-OA can grant membership access on closed relays.
|
||||
///
|
||||
/// Default: `false`. Set via `SPROUT_ALLOW_NIP_OA_AUTH=true`.
|
||||
pub allow_nip_oa_auth: bool,
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
//!
|
||||
//! Relay membership enforcement uses the shared
|
||||
//! [`crate::api::relay_members::enforce_relay_membership`] helper, which supports
|
||||
//! NIP-OA owner-delegation fallback. For WebSocket auth, the NIP-OA `auth` tag
|
||||
//! is extracted from the signed AUTH event itself (the tag is integrity-protected
|
||||
//! by the event signature).
|
||||
//! NIP-OA owner-delegation fallback on closed relays. On open relays, the auth
|
||||
//! handler calls [`crate::api::relay_members::extract_nip_oa_owner`] directly to
|
||||
//! extract the owner pubkey for agent→owner backfill (observer frame auth).
|
||||
//!
|
||||
//! For WebSocket auth, the NIP-OA `auth` tag is extracted from the signed AUTH
|
||||
//! event itself (the tag is integrity-protected by the event signature).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -129,6 +132,21 @@ pub async fn handle_auth(event: nostr::Event, conn: Arc<ConnectionState>, state:
|
||||
}
|
||||
};
|
||||
|
||||
// Open relay NIP-OA backfill: extract owner for agent→owner DB mapping
|
||||
// (needed for observer frame auth). Only runs on open relays — on closed
|
||||
// relays, enforce_relay_membership already handles NIP-OA delegation.
|
||||
// No feature flag needed: NIP-OA is cryptographically self-proving.
|
||||
let nip_oa_owner = nip_oa_owner.or_else(|| {
|
||||
if !state.config.require_relay_membership && auth_tag_json.is_some() {
|
||||
crate::api::relay_members::extract_nip_oa_owner(
|
||||
&pubkey.serialize(),
|
||||
auth_tag_json.as_deref(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// Stash NIP-OA owner on the auth context (session-scoped) only if
|
||||
// the DB confirms this owner relationship (first-write-wins).
|
||||
if let Some(owner) = nip_oa_owner {
|
||||
|
||||
Reference in New Issue
Block a user