From 5ecd8d6a434f97da94daef2f59166086fbaf5a6b Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 9 Jan 2026 17:04:06 +0000 Subject: feat: implement state event authorization per GRASP-01 spec Add comprehensive authorization checks to ensure state events are only accepted from maintainers of accepted repository announcements. This implements the core GRASP-01 requirement that pushes must match the latest state announcement "respecting the maintainer set." Changes: 1. StatePolicy authorization (src/nostr/policy/state.rs): - Check authorization BEFORE git data validation (fail-fast) - Reject if no announcement exists for repository - Reject if author not in maintainer set - Use existing helpers: fetch_repository_data() and pubkey_authorised_for_repo_owners() - Structured logging for all rejections 2. Purgatory invalidation (src/nostr/builder.rs): - New method: check_purgatory_state_events_for_identifier() - Called when announcements accepted (Accept and AcceptMaintainer) - Re-evaluates state events in purgatory for the identifier - Processes newly-authorized events (releases from purgatory) - Keeps unauthorized events for natural expiry (30 min) - Enables retroactive authorization when announcements arrive late 3. Purgatory sync authorization (src/git/sync.rs): - Check authorization BEFORE processing git data - Remove unauthorized events from purgatory (permanent rejection) - Prevents processing even if git data arrives first - Structured logging for monitoring 4. Rejected events tracking (src/sync/rejected_index.rs): - Add support for tracking rejected state events - New methods: add_state(), contains_state() - Separate metrics for state rejections - Enables sync to avoid re-fetching rejected states 5. Sync metrics (src/sync/metrics.rs, src/sync/mod.rs): - Add state-specific metrics (hot cache, cold index) - Track rejected states separately from announcements - Support monitoring of authorization rejections 6. Comprehensive tests (tests/state_authorization.rs): - test_reject_state_without_announcement - test_reject_state_from_unauthorized_author - test_accept_state_from_announcement_author - test_accept_state_from_maintainer Security Impact: - Before: State events could be published by anyone - After: Only maintainers can publish state events - Defense-in-depth: Authorization checked at 3 points: 1. On arrival (StatePolicy) 2. On announcement acceptance (purgatory re-evaluation) 3. On git data arrival (purgatory sync) All tests pass: - 248 unit tests - 51 NIP-34 announcement tests - 4 new state authorization tests - 9 rejected index tests Closes: State authorization requirement from GRASP-01 spec --- src/git/sync.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) (limited to 'src/git') diff --git a/src/git/sync.rs b/src/git/sync.rs index b05e2d0..e8e9655 100644 --- a/src/git/sync.rs +++ b/src/git/sync.rs @@ -951,6 +951,38 @@ async fn process_purgatory_state_events( } }; + // CRITICAL: Check authorization before processing + // State events MUST be rejected if author is not in maintainer set + let authorized_owners = crate::git::authorization::pubkey_authorised_for_repo_owners( + &entry.event.pubkey, + &db_repo_data, + ); + + if authorized_owners.is_empty() { + warn!( + identifier = %identifier, + event_id = %entry.event.id, + author = %entry.event.pubkey.to_hex(), + "Rejecting state event from purgatory: author not in maintainer set" + ); + // Remove from purgatory - this event will never be authorized + purgatory.remove_state_event(identifier, &entry.event.id); + result.errors.push(format!( + "State event {} rejected: author {} not in maintainer set", + entry.event.id, + entry.event.pubkey.to_hex() + )); + continue; + } + + debug!( + identifier = %identifier, + event_id = %entry.event.id, + author = %entry.event.pubkey.to_hex(), + authorized_for_owners = ?authorized_owners, + "State event author authorized via maintainer set" + ); + // Use unified processing function let process_result = crate::git::process::process_state_with_git_data( &state, -- cgit v1.2.3