From 06bceb4d3e368bec47c4c2eaafdc1c1071625d30 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Sun, 9 Aug 2026 02:17:07 +0000 Subject: nostr: verify relay certificates -- wss:// was fully MITM-able SSL_CTX_new leaves verification at SSL_VERIFY_NONE and loads no trust store, and nothing here ever changed that. SNI was set, a certificate was received, and it was then ignored: every wss:// relay connection this node has ever made would have completed against an attacker's certificate. Signed events limit the damage -- a MITM cannot forge an announcement attributable to our key -- but it can drop the rendezvous registration so miners never find the pool, censor subscriptions, or answer them with arbitrary content, which is the input the allocation bounds exist to contain. Load the system trust store, require SSL_VERIFY_PEER, and bind the check to the host we dialed with SSL_set1_host, since chain validation alone would accept any certificate a trusted CA ever issued for any name. Floor the protocol at TLS 1.2. Verification failures report the specific X509 reason rather than a generic handshake error, and a missing CA store is a hard failure instead of a silent downgrade to trusting everything. (cherry picked from commit 6020740e68463deb1cc3c5b477372fcd1a245adb) --- src/nostr.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'src/nostr.cpp') diff --git a/src/nostr.cpp b/src/nostr.cpp index 47f001c..fbae1a6 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp @@ -6,6 +6,7 @@ #include "headers.h" #include #include +#include // We link libsecp256k1 statically (without this the header assumes DLL import). // Both makefiles also pass -DSECP256K1_STATIC; the guard keeps that from // warning while leaving this file correct if built without the flag. @@ -568,11 +569,39 @@ public: { sslctx = SSL_CTX_new(TLS_client_method()); if (!sslctx) return false; + + // SSL_CTX_new defaults to SSL_VERIFY_NONE with no trust store + // loaded, so until now every wss:// relay connection was accepted + // without looking at the certificate at all -- any on-path + // attacker could impersonate a relay. Our own events are signed, + // so a MITM cannot forge them, but it could drop the rendezvous + // registration (leaving the pool unreachable to miners) or answer + // subscriptions with whatever it liked. + if (!SSL_CTX_set_default_verify_paths(sslctx)) + return error("Nostr: no system CA store available, refusing %s", host.c_str()); + SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL); + SSL_CTX_set_min_proto_version(sslctx, TLS1_2_VERSION); + ssl = SSL_new(sslctx); + if (!ssl) return false; SSL_set_fd(ssl, (int)hSocket); SSL_set_tlsext_host_name(ssl, host.c_str()); // SNI + + // Verifying the chain is not enough on its own: without this the + // relay could present any certificate a trusted CA ever issued, + // for any name. SSL_set1_host binds the check to the host we asked + // for. + if (!SSL_set1_host(ssl, host.c_str())) + return error("Nostr: cannot pin verification to %s", host.c_str()); + if (SSL_connect(ssl) != 1) + { + long nVerify = SSL_get_verify_result(ssl); + if (nVerify != X509_V_OK) + return error("Nostr: certificate for %s rejected: %s", + host.c_str(), X509_verify_cert_error_string(nVerify)); return error("Nostr: TLS handshake with %s failed", host.c_str()); + } } // WebSocket handshake (HTTP Upgrade) -- cgit v1.2.3