fix: route login/authorize callback POSTs over Tor, drop goblin-wallet UA
The login (kind 22242) and authorize callback POSTs carry a signed Nostr event containing the user's identity pubkey. They went out via crate::http::HttpClient::send (clearnet even when Tor is on) with an identifying "User-Agent: goblin-wallet" header, so the callback server saw the user's real IP tied to their Nostr identity — a deanonymization leak. Reroute both post_login_event and post_authorize_event through crate::tor::http_request_bytes, which honors route_over_tor(): Tor when the open wallet routes over Tor, clearnet otherwise. Drop the goblin-wallet UA; the helper sets a browser-like default User-Agent, so the traffic is not trivially fingerprintable as Goblin at the destination. Content-Type is preserved. Behavior with Tor off is unchanged (clearnet POST via the helper's clearnet branch).
This commit is contained in:
+18
-17
@@ -432,12 +432,16 @@ pub fn build_authorize_event(keys: &Keys, template: &Template) -> Result<Event,
|
||||
}
|
||||
|
||||
/// POST the signed authorize event to the callback as
|
||||
/// `{"c": <nonce>, "d": <domain>, "event": <event-json>}`. Goes through the
|
||||
/// app's shared [`crate::http::HttpClient`], so it follows the exact same
|
||||
/// transport policy (proxy settings included) as every other clearnet call.
|
||||
/// The `c` and `d` fields correlate the delivery to the request the server
|
||||
/// minted, since the arbitrary event carries no challenge or domain tag of its
|
||||
/// own. The caller wraps this in its own timeout.
|
||||
/// `{"c": <nonce>, "d": <domain>, "event": <event-json>}`. Goes through
|
||||
/// [`crate::tor::http_request_bytes`], the transport-aware helper: over Tor when
|
||||
/// the open wallet routes over Tor, clearnet otherwise. Routing over Tor when it
|
||||
/// is on keeps the callback server from tying the user's real IP to the identity
|
||||
/// pubkey in the signed event. The helper sets a browser-like default
|
||||
/// `User-Agent`, so the request is not trivially fingerprintable as Goblin at the
|
||||
/// destination (no identifying `goblin-wallet` header). The `c` and `d` fields
|
||||
/// correlate the delivery to the request the server minted, since the arbitrary
|
||||
/// event carries no challenge or domain tag of its own. The caller wraps this in
|
||||
/// its own timeout.
|
||||
pub async fn post_authorize_event(
|
||||
callback: &str,
|
||||
challenge: &str,
|
||||
@@ -445,17 +449,14 @@ pub async fn post_authorize_event(
|
||||
event: &Event,
|
||||
) -> Result<(), String> {
|
||||
let body = serde_json::json!({ "c": challenge, "d": domain, "event": event }).to_string();
|
||||
let req = hyper::Request::builder()
|
||||
.method(hyper::Method::POST)
|
||||
.uri(callback)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("User-Agent", "goblin-wallet")
|
||||
.body(http_body_util::Full::new(bytes::Bytes::from(body)))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let resp = crate::http::HttpClient::send(req)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let status = resp.status().as_u16();
|
||||
let (status, _) = crate::tor::http_request_bytes(
|
||||
"POST",
|
||||
callback.to_string(),
|
||||
Some(body.into_bytes()),
|
||||
vec![("Content-Type".to_string(), "application/json".to_string())],
|
||||
)
|
||||
.await
|
||||
.ok_or_else(|| "callback request failed".to_string())?;
|
||||
if (200..300).contains(&status) {
|
||||
Ok(())
|
||||
} else {
|
||||
|
||||
+15
-14
@@ -241,22 +241,23 @@ pub fn build_login_event(keys: &Keys, challenge: &str, domain: &str) -> Result<E
|
||||
}
|
||||
|
||||
/// POST the signed login event to the callback as `{"event": <event-json>}`.
|
||||
/// Goes through the app's shared [`crate::http::HttpClient`], so it follows
|
||||
/// the exact same transport policy (proxy settings included) as every other
|
||||
/// clearnet call. The caller wraps this in its own timeout.
|
||||
/// Goes through [`crate::tor::http_request_bytes`], the transport-aware helper:
|
||||
/// over Tor when the open wallet routes over Tor, clearnet otherwise. Routing
|
||||
/// over Tor when it is on keeps the callback server from tying the user's real
|
||||
/// IP to the identity pubkey in the signed event. The helper sets a browser-like
|
||||
/// default `User-Agent`, so the request is not trivially fingerprintable as
|
||||
/// Goblin at the destination (no identifying `goblin-wallet` header). The caller
|
||||
/// wraps this in its own timeout.
|
||||
pub async fn post_login_event(callback: &str, event: &Event) -> Result<(), String> {
|
||||
let body = serde_json::json!({ "event": event }).to_string();
|
||||
let req = hyper::Request::builder()
|
||||
.method(hyper::Method::POST)
|
||||
.uri(callback)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("User-Agent", "goblin-wallet")
|
||||
.body(http_body_util::Full::new(bytes::Bytes::from(body)))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let resp = crate::http::HttpClient::send(req)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let status = resp.status().as_u16();
|
||||
let (status, _) = crate::tor::http_request_bytes(
|
||||
"POST",
|
||||
callback.to_string(),
|
||||
Some(body.into_bytes()),
|
||||
vec![("Content-Type".to_string(), "application/json".to_string())],
|
||||
)
|
||||
.await
|
||||
.ok_or_else(|| "callback request failed".to_string())?;
|
||||
if (200..300).contains(&status) {
|
||||
Ok(())
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user