Citrine 3.1.1 закрывает обход аутентификации NIP-42 в реле для Android
Citrine проверял у AUTH-события kind, challenge, тег relay и время создания, но не подпись, поэтому клиент мог аутентифицироваться под любым pubkey и читать приватные kinds, которые реле закрывает за NIP-42.
Nostr WoT Newsroom
Материал подготовлен редакцией Nostr WoT на основе указанных первоисточников и опубликован автоматически, без индивидуальной проверки человеком.
Citrine 3.1.1, реле, которое работает как приложение для Android, вышел 11 сентября 2026 года. Первая запись в его журнале изменений описывает исправление безопасности, остальное составляет серия более мелких исправлений соответствия спецификациям.
Сравнение с 3.1.0 включает три коммита и одиннадцать файлов. Почти весь код приходит в одном из них, 829f0396, с заголовком "Enforce NIP spec conformance across the relay".
AUTH без подписи
NIP-42 позволяет реле попросить клиента доказать, каким pubkey он владеет. Реле отправляет строку challenge, а клиент отвечает подписанным событием kind 22242, в котором есть этот challenge и URL реле.
В 3.1.0 функция validateAuthEvent проверяла, что событие имеет kind 22242, что в нём есть challenge и тег relay, что challenge совпадает с отправленным в этом соединении, что URL реле нормализуется и что created_at отличается от текущего времени не более чем на десять минут. Это те проверки, которые перечисляет NIP-42. Если они проходили, обработчик добавлял event.pubKey в набор аутентифицированных пользователей соединения.
Ничто на этом пути не проверяло подпись. Challenge отправляется любому, кто открыл соединение, поэтому клиент мог собрать событие kind 22242 с правильным challenge, указать в нём любой pubkey, оставить подпись недействительной и до конца соединения считаться владельцем этого pubkey.
Версия 3.1.1 добавляет недостающий шаг после существующих проверок:
// NIP-42: an AUTH event must carry a valid signature, otherwise any
// client could authenticate as any pubkey.
if (!event.verify()) {
connection?.send(CommandResult.invalid(event, "auth event signature verification failed").toJson())
return
}Сообщение коммита называет, что защищал набор аутентифицированных: запись защищённых событий и чтение приватных kinds. На стороне чтения AuthGate отклоняет фильтры по пяти kinds, если ни один из аутентифицированных pubkey соединения не указан автором в фильтре и не встречается в его значениях #p. Среди этих пяти личные сообщения kind 4, gift wrap kind 1059 и данные приложений kind 30078. С поддельным AUTH клиент мог указать pubkey владельца и пройти эту проверку.
Насколько далеко это распространялось, зависит от настроек. По умолчанию Citrine слушает 127.0.0.1, порт 4869, поэтому при установке по умолчанию открыть соединение могут только клиенты на том же устройстве. Адрес прослушивания задаётся в настройках, и реле, настроенное слушать другой адрес, было открыто для того же поддельного AUTH со стороны всего, что могло до него достучаться. На стороне записи проверка защищённых событий NIP-70 уже освобождала соединения с localhost, так что там обход имел значение только для удалённых соединений.
Идентификаторы подписок были глобальными
NIP-01 требует, чтобы реле управляли идентификаторами подписок независимо для каждого WebSocket-соединения. Citrine хранил все подписки в одном кэше с ключом, равным идентификатору от клиента. CLOSE для "feed" в одном сокете отменял "feed" во всех соединениях, а новый REQ с тем же идентификатором заменял его.
Теперь ключ кэша состоит из имени соединения и идентификатора. Идентификатор в протоколе остаётся тем, который выбрал клиент.
COUNT даёт один ответ
NIP-45 говорит, что фильтры в COUNT объединяются через OR и агрегируются в один результат. В 3.1.0 COUNT шёл тем же путём, что и REQ: одно сообщение со счётчиком на каждый фильтр, затем EOSE, и подписка оставалась зарегистрированной, так что получала и живые события, совпавшие с ней позже.
Версия 3.1.1 выделяет для COUNT отдельный путь. Он никогда не регистрируется, поэтому не заменяет REQ с тем же идентификатором и не получает живых событий, и отправляет одно сообщение COUNT без EOSE. Итог равен сумме счётчиков по каждому фильтру, поэтому событие, совпавшее с двумя фильтрами в одном запросе, учитывается дважды.
Мелкие исправления
- Ключ
#p. Для фильтров без kindsAuthGateискал получателей под ключом"#p", а теги фильтра хранятся без#. Аутентифицированный пользователь, запрашивавший{"#p":[<свой pubkey>]}, получалrestricted. Ветка с kinds уже использовала"p"; теперь и эта тоже. - Выбор среди заменяемых событий. NIP-01 говорит, что из заменяемых событий с одинаковым временем сохраняется то, у которого наименьший id. Citrine отклонял любое входящее событие, если в базе было событие с равным или более поздним
created_at, так что побеждало пришедшее первым. Оба запроса теперь позволяют входящему событию с тем же временем и меньшим id заменить сохранённое. - Точные
idsиauthors. Фильтр в памяти всё ещё принимал совпадения по префиксу, которые не допускают ни путь через SQL, ни NIP-01. Теперь совпадают только точные значения. - Префиксы OK. Отказы по политике реле теперь используют
blocked:вместоinvalid:, удалённое событие получаетdeleted:, а более старое заменяемое получаетduplicate:. - NIP-11. Информационный документ реле собирался подстановкой в строку, поэтому имя или описание с кавычкой давали недействительный JSON. Теперь он собирается через Jackson. NIP-04 больше не объявляется в
supported_nips, а заголовки CORS теперь включаютAccess-Control-Allow-Headers. - NIP-86. Добавлены
unbanpubkeyиunallowpubkey, аallowpubkeyбольше не убирает pubkey из списка заблокированных как побочный эффект.
Что устанавливают артефакты
Версия 3.1.1 опубликована как релиз с шестью пакетами для Android и подписанным манифестом контрольных сумм. Сообщение коммита сообщает об успешных assembleDebug, ktlintCheck, test и lintDebug и о 24 поведенческих проверках на устройстве; сам дифф не добавляет тестовых файлов. Citrine 3.2.0 вышел 18 сентября и содержит все коммиты 3.1.1, так что обновление до текущей версии включает исправление. Ничто из этого не говорит о том, какая версия работает на конкретном телефоне, и тем, кто полагается на Citrine для личных сообщений, стоит это проверить.
Источники
Каждое утверждение в этом материале ссылается на первоисточник.
- Релиз Citrine v3.1.1 — greenart7c3/Citrine (11 сентября 2026 г.)
- Сравнение v3.1.0...v3.1.1 — greenart7c3/Citrine (11 сентября 2026 г.)
- Enforce NIP spec conformance across the relay (829f0396) — greenart7c3/Citrine (9 сентября 2026 г.)
- NIP-42 Authentication of clients to relays — nostr-protocol/nips (7 октября 2026 г.)
- NIP-45 Event Counts — nostr-protocol/nips (7 октября 2026 г.)