From 062ab0f012ce1799d3261af2331f4c39e9fa9c3e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 03:38:20 +0000 Subject: [PATCH] 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::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 Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr --- .../amethyst/model/GitStatusIndex.kt | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitStatusIndex.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitStatusIndex.kt index 67c4935592..a19dce396f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitStatusIndex.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/GitStatusIndex.kt @@ -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() 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