Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
implement full NIP-42 AUTH support in the context of NIP-70 protected…
… events.
  • Loading branch information
fiatjaf committed Feb 19, 2025
commit cf43b3df169edb1a1594cfe463f6bf0cec83c05e
101 changes: 96 additions & 5 deletions 101 src/apps/relay/RelayIngester.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#include "RelayServer.h"
#include "jsonParseUtils.h"
#include <cstdlib>


void RelayServer::runIngester(ThreadPool<MsgIngester>::Thread &thr) {
secp256k1_context *secpCtx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY);
Decompressor decomp;
flat_hash_map<uint64_t, AuthStatus*> connIdToAuthStatus;

while(1) {
auto newMsgs = thr.inbox.pop_all();
Expand All @@ -29,12 +32,20 @@ void RelayServer::runIngester(ThreadPool<MsgIngester>::Thread &thr) {
if (cfg().relay__logging__dumpInEvents) LI << "[" << msg->connId << "] dumpInEvent: " << msg->payload;

try {
ingesterProcessEvent(txn, msg->connId, msg->ipAddr, secpCtx, arr[1], writerMsgs);
ingesterProcessEvent(txn, msg->connId, connIdToAuthStatus, msg->ipAddr, secpCtx, arr[1], writerMsgs);
} catch (std::exception &e) {
sendOKResponse(msg->connId, arr[1].is_object() && arr[1].at("id").is_string() ? arr[1].at("id").get_string() : "?",
false, std::string("invalid: ") + e.what());
if (cfg().relay__logging__invalidEvents) LI << "Rejected invalid event: " << e.what();
}
} else if (cmd == "AUTH") {
if (cfg().relay__logging__dumpInAll) LI << "[" << msg->connId << "] dumpInAuth: " << msg->payload;

try {
ingesterProcessAuth(msg->connId, connIdToAuthStatus, secpCtx, arr[1]);
} catch (std::exception &e) {
sendNoticeError(msg->connId, std::string("auth failed: ") + e.what());
}
} else if (cmd == "REQ") {
if (cfg().relay__logging__dumpInReqs) LI << "[" << msg->connId << "] dumpInReq: " << msg->payload;

Expand Down Expand Up @@ -85,7 +96,7 @@ void RelayServer::runIngester(ThreadPool<MsgIngester>::Thread &thr) {
}
}

void RelayServer::ingesterProcessEvent(lmdb::txn &txn, uint64_t connId, std::string ipAddr, secp256k1_context *secpCtx, const tao::json::value &origJson, std::vector<MsgWriter> &output) {
void RelayServer::ingesterProcessEvent(lmdb::txn &txn, uint64_t connId, flat_hash_map<uint64_t, AuthStatus*> &connIdToAuthStatus, std::string ipAddr, secp256k1_context *secpCtx, const tao::json::value &origJson, std::vector<MsgWriter> &output) {
std::string packedStr, jsonStr;

parseAndVerifyEvent(origJson, secpCtx, true, true, packedStr, jsonStr);
Expand All @@ -104,9 +115,38 @@ void RelayServer::ingesterProcessEvent(lmdb::txn &txn, uint64_t connId, std::str
});

if (foundProtected) {
LI << "Protected event, skipping";
sendOKResponse(connId, to_hex(packed.id()), false, "blocked: event marked as protected");
return;
// NIP-70 protected events must be rejected unless published by an authenticated public key
// that matches the event author, so we do all the AUTH flow here
if (cfg().relay__serviceUrl.empty()) {
// except if we don't have a serviceUrl, in that case just fail
LI << "Protected event and no serviceUrl configured, skipping";
sendOKResponse(connId, to_hex(packed.id()), false, "blocked: event marked as protected");
return;
}

auto as = connIdToAuthStatus.find(connId);
if (as == connIdToAuthStatus.end()) {
// we haven't sent an AUTH event for this, so first we generate a challenge for this connection
auto authStatus = new AuthStatus();
authStatus->challenge = std::to_string(int64_t(std::pow(packed.created_at(), connId + 1)));
connIdToAuthStatus.emplace(connId, authStatus);
LI << "Protected event, requesting AUTH";
sendAuthChallenge(connId, authStatus->challenge);
sendOKResponse(connId, to_hex(packed.id()), false, "auth-required: event marked as protected");
return;
}

const auto authed = (*as->second).authed;
if (authed.empty()) {
// not authenticated
sendOKResponse(connId, to_hex(packed.id()), false, "auth-required: event marked as protected");
return;
} else if (authed != packed.pubkey()) {
// authenticated as someone else
sendOKResponse(connId, to_hex(packed.id()), false, "restricted: must be published by the author");
return;
}
// otherwise we proceed to accept the event
}
}

Expand Down Expand Up @@ -137,6 +177,57 @@ void RelayServer::ingesterProcessClose(lmdb::txn &txn, uint64_t connId, const ta
tpReqWorker.dispatch(connId, MsgReqWorker{MsgReqWorker::RemoveSub{connId, SubId(jsonGetString(arr[1], "CLOSE subscription id was not a string"))}});
}

void RelayServer::ingesterProcessAuth(uint64_t connId, flat_hash_map<uint64_t, AuthStatus*> connIdToAuthStatus, secp256k1_context *secpCtx, const tao::json::value &eventJson) {
if (cfg().relay__serviceUrl.empty()) {
throw herr("relay needs serviceUrl to be configured before AUTH can work");
}

std::string packedStr, jsonStr;
parseAndVerifyEvent(eventJson, secpCtx, true, true, packedStr, jsonStr);

PackedEventView packed(packedStr);

if (packed.kind() != 22242) {
throw herr("wrong event kind, expected 22242");
}

auto as = connIdToAuthStatus.find(connId);
if (as == connIdToAuthStatus.end()) {
throw herr("no auth status available for connection");
}
if (!(*as->second).authed.empty()) {
throw herr("already authenticated");
}
const auto challenge = (*as->second).challenge;

bool foundChallenge = false;
bool foundCorrectRelayUrl = false;

for (const auto &tagj : eventJson.at("tags").get_array()) {
const auto &tag = tagj.get_array();
if (tag.size() < 2) continue;
const auto name = tag[0].as<std::string_view>();
const auto value = tag[1].as<std::string_view>();
if (name == "relay" && value == cfg().relay__serviceUrl) {
foundCorrectRelayUrl = true;
} else if (name == "challenge" && value == challenge) {
foundChallenge = true;
}
}

if (!foundChallenge) {
throw herr("challenge string mismatch");
}
if (!foundCorrectRelayUrl) {
throw herr("incorrect or missing relay tag, expected: " + cfg().relay__serviceUrl);
}

// set the connection as authenticated with this pubkey
(*as->second).authed = packed.pubkey();

sendOKResponse(connId, to_hex(packed.id()), true, "successfully authenticated");
}

void RelayServer::ingesterProcessNegentropy(lmdb::txn &txn, Decompressor &decomp, uint64_t connId, const tao::json::value &arr) {
const auto &subscriptionStr = jsonGetString(arr[1], "NEG-OPEN subscription id was not a string");

Expand Down
15 changes: 14 additions & 1 deletion 15 src/apps/relay/RelayServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ struct MsgNegentropy : NonCopyable {
MsgNegentropy(Var &&msg_) : msg(std::move(msg_)) {}
};

// NIP-42 stuff
struct AuthStatus {
std::string challenge;
std::string authed;
};


struct RelayServer {
uS::Async *hubTrigger = nullptr;
Expand All @@ -167,9 +173,10 @@ struct RelayServer {
void runWebsocket(ThreadPool<MsgWebsocket>::Thread &thr);

void runIngester(ThreadPool<MsgIngester>::Thread &thr);
void ingesterProcessEvent(lmdb::txn &txn, uint64_t connId, std::string ipAddr, secp256k1_context *secpCtx, const tao::json::value &origJson, std::vector<MsgWriter> &output);
void ingesterProcessEvent(lmdb::txn &txn, uint64_t connId, flat_hash_map<uint64_t, AuthStatus*> &connIdToAuthStatus, std::string ipAddr, secp256k1_context *secpCtx, const tao::json::value &origJson, std::vector<MsgWriter> &output);
void ingesterProcessReq(lmdb::txn &txn, uint64_t connId, const tao::json::value &origJson);
void ingesterProcessClose(lmdb::txn &txn, uint64_t connId, const tao::json::value &origJson);
void ingesterProcessAuth(uint64_t connId, flat_hash_map<uint64_t, AuthStatus*> connIdToAuthStatus, secp256k1_context *secpCtx, const tao::json::value &eventJson);
void ingesterProcessNegentropy(lmdb::txn &txn, Decompressor &decomp, uint64_t connId, const tao::json::value &origJson);

void runWriter(ThreadPool<MsgWriter>::Thread &thr);
Expand Down Expand Up @@ -228,4 +235,10 @@ struct RelayServer {
tpWebsocket.dispatch(0, MsgWebsocket{MsgWebsocket::Send{connId, std::move(tao::json::to_string(reply))}});
hubTrigger->send();
}

void sendAuthChallenge(uint64_t connId, std::string_view challenge) {
auto reply = tao::json::value::array({ "AUTH", challenge });
tpWebsocket.dispatch(0, MsgWebsocket{MsgWebsocket::Send{connId, std::move(tao::json::to_string(reply))}});
hubTrigger->send();
}
};
4 changes: 4 additions & 0 deletions 4 src/apps/relay/golpe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,7 @@ config:
- name: relay__negentropy__maxSyncEvents
desc: "Maximum records that sync will process before returning an error"
default: 1000000

- name: relay__serviceUrl
desc: "Relay URL (beginning with wss://) that will be used to check NIP-42 AUTH"
default: ""
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.