Nostr WoT
NostrRelaysNIP-42Security

Citrine 3.1.1 fecha um bypass de autenticação NIP-42 no relay para Android

Citrine conferia o kind, o challenge, a tag relay e o horário de um evento AUTH, mas nunca a assinatura, então um cliente podia se autenticar como qualquer pubkey e ler os kinds privados que o relay protege com NIP-42.

Nostr WoT Newsroom

Matéria5 min de leitura

Elaborado pela Redação da Nostr WoT a partir das fontes primárias citadas, e publicado automaticamente sem revisão humana individual.

Citrine 3.1.1 fecha um bypass de autenticação NIP-42 no relay para Android

Citrine 3.1.1, o relay que roda como aplicativo Android, foi publicado em 11 de setembro de 2026. A primeira entrada do changelog é uma correção de segurança, e o restante é uma varredura de reparos menores de conformidade.

A comparação com 3.1.0 tem três commits e onze arquivos. Quase todo o código chega em um deles, 829f0396, intitulado "Enforce NIP spec conformance across the relay".

AUTH sem assinatura

NIP-42 permite que um relay peça a um cliente que prove qual pubkey ele controla. O relay envia uma string de challenge, e o cliente responde com um evento assinado de kind 22242 contendo esse challenge e a URL do relay.

Na 3.1.0, validateAuthEvent conferia se o evento era de kind 22242, se trazia um challenge e uma tag relay, se o challenge batia com o enviado naquela conexão, se a URL do relay podia ser normalizada e se created_at estava a menos de dez minutos do horário atual. São as verificações que a NIP-42 lista. Se passassem, o handler adicionava event.pubKey ao conjunto de usuários autenticados da conexão.

Nada nesse caminho verificava a assinatura. O challenge é enviado para quem abre a conexão, então um cliente podia montar um evento de kind 22242 com o challenge certo, colocar qualquer pubkey nele, deixar a assinatura inválida e ser tratado como essa pubkey pelo resto da conexão.

A versão 3.1.1 adiciona o passo que faltava depois das verificações existentes:

kotlin
// 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
}

A mensagem do commit nomeia o que o conjunto de autenticados protegia: escritas de eventos protegidos e leituras de kinds privados. Do lado da leitura, AuthGate recusa filtros de cinco kinds a menos que uma das pubkeys autenticadas da conexão seja autora no filtro ou apareça nos seus valores #p. Entre os cinco estão as mensagens diretas de kind 4, os gift wraps de kind 1059 e os dados de aplicativo de kind 30078. Com um AUTH forjado, um cliente podia informar a pubkey do dono e passar por esse filtro.

O alcance depende da configuração. Por padrão, Citrine escuta em 127.0.0.1, porta 4869, então numa instalação padrão os clientes que conseguem abrir uma conexão são os do mesmo aparelho. O endereço de escuta é uma configuração, e um relay configurado para escutar em outro endereço ficava exposto ao mesmo AUTH forjado vindo de qualquer coisa que conseguisse alcançá-lo. Do lado da escrita, a verificação de eventos protegidos da NIP-70 já isentava conexões vindas de localhost, então ali o bypass só importava para conexões remotas.

Os ids de assinatura eram globais

NIP-01 diz que relays devem gerenciar ids de assinatura de forma independente para cada conexão WebSocket. Citrine guardava todas as assinaturas em um único cache indexado pelo id enviado pelo cliente. Um CLOSE de "feed" em um socket cancelava o "feed" de todas as conexões, e um novo REQ com o mesmo id o substituía.

A chave do cache agora é o nome da conexão junto com o id. O id que trafega na rede continua sendo o escolhido pelo cliente.

COUNT é uma resposta só

NIP-45 diz que os filtros de um COUNT são combinados com OR e agregados em um único resultado. Na 3.1.0 um COUNT seguia o mesmo caminho de um REQ: uma mensagem de contagem por filtro, depois um EOSE, e a assinatura ficava registrada, então também recebia eventos ao vivo que batessem com ela depois.

A versão 3.1.1 dá ao COUNT um caminho próprio. Ele nunca é registrado, então não substitui um REQ com o mesmo id nem recebe eventos ao vivo, e envia uma única mensagem COUNT sem EOSE. O total é a soma das contagens de cada filtro, então um evento que bate com dois filtros na mesma requisição é contado duas vezes.

Reparos menores

  • A chave #p. Para filtros sem kinds, AuthGate procurava destinatários sob a chave "#p", mas as tags de filtro são guardadas sem o #. Um usuário autenticado que pedia {"#p":[<sua pubkey>]} recebia restricted. O ramo de kinds já usava "p"; este agora também.
  • Desempate de substituíveis. NIP-01 diz que, entre eventos substituíveis com o mesmo horário, fica o de id mais baixo. Citrine rejeitava qualquer evento recebido se houvesse um guardado com created_at igual ou posterior, então vencia o primeiro a chegar. As duas consultas agora deixam um evento recebido com o mesmo horário e id mais baixo substituir o guardado.
  • ids e authors exatos. O filtro em memória ainda aceitava correspondência por prefixo, o que nem o caminho SQL nem a NIP-01 aceitam. Agora só valores exatos correspondem.
  • Prefixos de OK. Recusas por política agora usam blocked: em vez de invalid:, um evento apagado recebe deleted: e um substituível mais antigo recebe duplicate:.
  • NIP-11. O documento de informações do relay era montado por interpolação de strings, então um nome ou descrição com aspas gerava JSON inválido. Agora ele é montado com Jackson. A NIP-04 deixou de ser anunciada em supported_nips, e os cabeçalhos CORS agora incluem Access-Control-Allow-Headers.
  • NIP-86. Foram adicionados unbanpubkey e unallowpubkey, e allowpubkey não remove mais uma pubkey da lista de banidos como efeito colateral.

O que os artefatos estabelecem

A versão 3.1.1 é um lançamento publicado com seis pacotes Android e um manifesto de checksums assinado. A mensagem do commit relata assembleDebug, ktlintCheck, test e lintDebug passando e 24 verificações de comportamento executadas em um aparelho; o diff em si não adiciona arquivos de teste. Citrine 3.2.0 veio em 18 de setembro e contém todos os commits da 3.1.1, então atualizar para a versão atual inclui a correção. Nada disso estabelece qual versão um celular específico está rodando, e quem depende do Citrine para mensagens diretas deve conferir.

Fontes

Toda afirmação nesta matéria tem um link para uma fonte primária.

  1. Lançamento do Citrine v3.1.1 — greenart7c3/Citrine (11 de setembro de 2026)
  2. Comparação v3.1.0...v3.1.1 — greenart7c3/Citrine (11 de setembro de 2026)
  3. Enforce NIP spec conformance across the relay (829f0396) — greenart7c3/Citrine (9 de setembro de 2026)
  4. NIP-42 Authentication of clients to relays — nostr-protocol/nips (7 de outubro de 2026)
  5. NIP-45 Event Counts — nostr-protocol/nips (7 de outubro de 2026)

Fique por dentro

Receba notícias sobre as versões publicadas do Nostr WoT, novos recursos e integrações.

Você receberá a newsletter em português.

Armazenamos seu endereço de e-mail e seu idioma preferido para enviar a newsletter.

Boletins