diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-09 02:17:07 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 04:37:36 +0000 |
| commit | 89964636055e0856b2ff138edc4b7f45e0099f1b (patch) | |
| tree | d37f3fab481315f6ae51f0f6db3ffb61bb6f2849 | |
| parent | 7297216f20e81935dec4e612f90f3a5f0e9ebea9 (diff) | |
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)
| -rw-r--r-- | src/nostr.cpp | 29 |
1 files changed, 29 insertions, 0 deletions
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 @@ | |||
| 6 | #include "headers.h" | 6 | #include "headers.h" |
| 7 | #include <openssl/ssl.h> | 7 | #include <openssl/ssl.h> |
| 8 | #include <openssl/err.h> | 8 | #include <openssl/err.h> |
| 9 | #include <openssl/x509.h> | ||
| 9 | // We link libsecp256k1 statically (without this the header assumes DLL import). | 10 | // We link libsecp256k1 statically (without this the header assumes DLL import). |
| 10 | // Both makefiles also pass -DSECP256K1_STATIC; the guard keeps that from | 11 | // Both makefiles also pass -DSECP256K1_STATIC; the guard keeps that from |
| 11 | // warning while leaving this file correct if built without the flag. | 12 | // warning while leaving this file correct if built without the flag. |
| @@ -568,11 +569,39 @@ public: | |||
| 568 | { | 569 | { |
| 569 | sslctx = SSL_CTX_new(TLS_client_method()); | 570 | sslctx = SSL_CTX_new(TLS_client_method()); |
| 570 | if (!sslctx) return false; | 571 | if (!sslctx) return false; |
| 572 | |||
| 573 | // SSL_CTX_new defaults to SSL_VERIFY_NONE with no trust store | ||
| 574 | // loaded, so until now every wss:// relay connection was accepted | ||
| 575 | // without looking at the certificate at all -- any on-path | ||
| 576 | // attacker could impersonate a relay. Our own events are signed, | ||
| 577 | // so a MITM cannot forge them, but it could drop the rendezvous | ||
| 578 | // registration (leaving the pool unreachable to miners) or answer | ||
| 579 | // subscriptions with whatever it liked. | ||
| 580 | if (!SSL_CTX_set_default_verify_paths(sslctx)) | ||
| 581 | return error("Nostr: no system CA store available, refusing %s", host.c_str()); | ||
| 582 | SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL); | ||
| 583 | SSL_CTX_set_min_proto_version(sslctx, TLS1_2_VERSION); | ||
| 584 | |||
| 571 | ssl = SSL_new(sslctx); | 585 | ssl = SSL_new(sslctx); |
| 586 | if (!ssl) return false; | ||
| 572 | SSL_set_fd(ssl, (int)hSocket); | 587 | SSL_set_fd(ssl, (int)hSocket); |
| 573 | SSL_set_tlsext_host_name(ssl, host.c_str()); // SNI | 588 | SSL_set_tlsext_host_name(ssl, host.c_str()); // SNI |
| 589 | |||
| 590 | // Verifying the chain is not enough on its own: without this the | ||
| 591 | // relay could present any certificate a trusted CA ever issued, | ||
| 592 | // for any name. SSL_set1_host binds the check to the host we asked | ||
| 593 | // for. | ||
| 594 | if (!SSL_set1_host(ssl, host.c_str())) | ||
| 595 | return error("Nostr: cannot pin verification to %s", host.c_str()); | ||
| 596 | |||
| 574 | if (SSL_connect(ssl) != 1) | 597 | if (SSL_connect(ssl) != 1) |
| 598 | { | ||
| 599 | long nVerify = SSL_get_verify_result(ssl); | ||
| 600 | if (nVerify != X509_V_OK) | ||
| 601 | return error("Nostr: certificate for %s rejected: %s", | ||
| 602 | host.c_str(), X509_verify_cert_error_string(nVerify)); | ||
| 575 | return error("Nostr: TLS handshake with %s failed", host.c_str()); | 603 | return error("Nostr: TLS handshake with %s failed", host.c_str()); |
| 604 | } | ||
| 576 | } | 605 | } |
| 577 | 606 | ||
| 578 | // WebSocket handshake (HTTP Upgrade) | 607 | // WebSocket handshake (HTTP Upgrade) |