mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 07:48:27 +00:00
revert(amethyst): drop GitStatusIndex auth change; defer to separate proposal
The isAuthoritative guard added to GitStatusIndex read the repository owner from the status event's own `a` tag (GitStatusEvent.repositoryAddress() -> first a tag, no kind filter, no cross-check against the target's repo). That value is attacker-controlled: a forged kind:1632 carrying `["a", "30617:<attacker-pubkey>:anything"]` makes `status.pubKey == repoAddress.pubKeyHex` pass, so the spoof it meant to block still succeeds. It also regressed reads: reduceLatestByTarget only re-runs on a new kind 1630-1633, so a status dropped while the 30617 was uncached stayed dropped, leaving genuinely-closed items in the Open tab with wrong counts. Keep this series focused on the quartz + cli NIP-34 parity work. The Android status-authorization hardening (resolve the repo from the target item, load the cached 30617, authorize against repo.pubKey + maintainers() + the target author, and re-reduce when a 30617 arrives) will land as its own proposal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
This commit is contained in:
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.model
|
||||
import com.vitorpamplona.amethyst.model.LocalCache.observeEvents
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.status.GitStatusAppliedEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.status.GitStatusClosedEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.status.GitStatusEvent
|
||||
@@ -72,7 +71,6 @@ object GitStatusIndex {
|
||||
val latest = HashMap<HexKey, GitStatusEvent>()
|
||||
for (event in events) {
|
||||
val target = event.rootEventId() ?: continue
|
||||
if (!isAuthoritative(event, target)) continue
|
||||
val current = latest[target]
|
||||
if (current == null || event.createdAt > current.createdAt) {
|
||||
latest[target] = event
|
||||
@@ -81,29 +79,6 @@ object GitStatusIndex {
|
||||
return latest
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-34: only the repository owner, a declared maintainer, or the target
|
||||
* item's own author may set its status — "the newest Status from the root
|
||||
* author or a maintainer is valid". Without this filter any pubkey could
|
||||
* publish a kind-1632 and make someone else's issue render closed.
|
||||
*
|
||||
* The owner is read from the status's own `a` tag (so it holds even before
|
||||
* the announcement is cached); the maintainer set needs the cached kind-30617
|
||||
* (falling back to owner/author-only until it arrives — the safe default is to
|
||||
* treat an unverifiable status as absent, i.e. the item stays open).
|
||||
*/
|
||||
private fun isAuthoritative(
|
||||
status: GitStatusEvent,
|
||||
targetId: HexKey,
|
||||
): Boolean {
|
||||
status.repositoryAddress()?.let { repoAddress ->
|
||||
if (status.pubKey == repoAddress.pubKeyHex) return true
|
||||
val repo = LocalCache.getAddressableNoteIfExists(repoAddress)?.event as? GitRepositoryEvent
|
||||
if (repo != null && status.pubKey in repo.maintainers()) return true
|
||||
}
|
||||
return status.pubKey == LocalCache.getNoteIfExists(targetId)?.event?.pubKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the latest status for [targetId] marks it as closed (kind 1632)
|
||||
* or applied/resolved/merged (kind 1631). Items with no status event, or
|
||||
|
||||
Reference in New Issue
Block a user