diff --git a/payjoin-cli/src/app/v2/ohttp.rs b/payjoin-cli/src/app/v2/ohttp.rs index e438540ec..b4a227284 100644 --- a/payjoin-cli/src/app/v2/ohttp.rs +++ b/payjoin-cli/src/app/v2/ohttp.rs @@ -1,5 +1,7 @@ //! OHTTP relay and payjoin directory selection / key bootstrapping for the payjoin-cli. //! +//! See the `payjoin::io` module for relay-selection and health-check guidance. +//! //! [`MailroomManager`] tracks relays and directories that have failed, //! excluding them from future selections for the lifetime of the [`MailroomManager`]. //! diff --git a/payjoin-ffi/csharp/Payjoin.Http.cs b/payjoin-ffi/csharp/Payjoin.Http.cs index 0ce5136ca..f32e631fc 100644 --- a/payjoin-ffi/csharp/Payjoin.Http.cs +++ b/payjoin-ffi/csharp/Payjoin.Http.cs @@ -6,6 +6,43 @@ namespace Payjoin.Http { + /// + /// Fetch OHTTP keys from a payjoin directory through an OHTTP relay. + /// + /// + /// When multiple relays are configured, callers should pick one at random per request + /// to avoid a fixed contact pattern at the network layer. + /// + /// Random selection only helps if the relay list itself is not identifying: prefer a shared + /// relay list and discourage isolated infrastructure that other apps don't use, since a + /// distinctive list fingerprints the client regardless of how a relay is picked from it. + /// + /// Sender and receiver have distinct request patterns: + /// - Receiver: long-poll GETs, then a POST + /// - Sender: a POST, then long-poll GETs + /// + /// OHTTP does not hide the client IP from the relay. A relay that sees the same + /// client repeatedly can observe its access patterns to infer whether + /// the IP is associated with a sender or receiver, potentially linking to identity or + /// location. Based on when a session ends it may be easier to correctly guess + /// whether a transaction is a PayJoin. The IP address linked information may + /// additionally aid in cluster analysis, for example whether a cluster's temporal + /// patterns are consistent with a location guess for the IP address. + /// + /// Health checks: some clients call periodically to + /// verify that the directory and relay infrastructure is reachable. Given the threat + /// model above, this is acceptable only when: + /// - The call is not triggered on any deterministic, recurring event + /// (e.g. app startup, periodic timer). Prefer user-initiated actions + /// (e.g. opening a settings/status screen) or piggybacking on operations + /// the user already triggered (e.g. resuming an existing session). + /// - The caller throttles invocations so they don't produce a recurring + /// timing pattern observable by the relay. + /// + /// A health check has a distinct traffic pattern from a real payjoin request + /// and is not temporally tied to any onchain broadcast, but repeated calls + /// still expose the client IP to the relay. + /// internal sealed class OhttpKeysClient : IDisposable { private readonly HttpClient _client; diff --git a/payjoin-ffi/dart/lib/http.dart b/payjoin-ffi/dart/lib/http.dart index f313f2d43..e6d10d991 100644 --- a/payjoin-ffi/dart/lib/http.dart +++ b/payjoin-ffi/dart/lib/http.dart @@ -1,3 +1,40 @@ +/// Fetch OHTTP keys from a payjoin directory through an OHTTP relay. +/// +/// When multiple relays are configured, callers **should pick one at random per request** +/// to avoid a fixed contact pattern at the network layer. +/// +/// Random selection only helps if the relay list itself is not identifying: prefer a shared +/// relay list and discourage isolated infrastructure that other apps don't use, since a +/// distinctive list fingerprints the client regardless of how a relay is picked from it. +/// +/// Sender and receiver have distinct request patterns: +/// - Receiver: long-poll GETs, then a POST +/// - Sender: a POST, then long-poll GETs +/// +/// OHTTP does not hide the client IP from the relay. A relay that sees the same +/// client repeatedly can observe its access patterns to infer whether +/// the IP is associated with a sender or receiver, potentially linking to identity or +/// location. Based on when a session ends it may be easier to correctly guess +/// whether a transaction is a PayJoin. The IP address linked information may +/// additionally aid in cluster analysis, for example whether a cluster's temporal +/// patterns are consistent with a location guess for the IP address. +/// +/// ## Health checks +/// +/// Some clients call `fetchOhttpKeys` periodically to verify that the +/// directory and relay infrastructure is reachable. Given the threat model +/// above, this is acceptable only when: +/// +/// - The call is **not** triggered on any deterministic, recurring event +/// (e.g. app startup, periodic timer). Prefer user-initiated actions +/// (e.g. opening a settings/status screen) or piggybacking on operations +/// the user already triggered (e.g. resuming an existing session). +/// - The caller throttles invocations so they don't produce a recurring +/// timing pattern observable by the relay. +/// +/// A health check has a distinct traffic pattern from a real payjoin request +/// and is not temporally tied to any onchain broadcast, but repeated calls +/// still expose the client IP to the relay. library http; import 'dart:async'; diff --git a/payjoin-ffi/python/src/payjoin/http.py b/payjoin-ffi/python/src/payjoin/http.py index 70c391700..019d45c40 100644 --- a/payjoin-ffi/python/src/payjoin/http.py +++ b/payjoin-ffi/python/src/payjoin/http.py @@ -1,3 +1,42 @@ +"""Fetch OHTTP keys from a payjoin directory through an OHTTP relay. + +When multiple relays are configured, callers **should pick one at random per request** +to avoid a fixed contact pattern at the network layer. + +Random selection only helps if the relay list itself is not identifying: prefer a shared +relay list and discourage isolated infrastructure that other apps don't use, since a +distinctive list fingerprints the client regardless of how a relay is picked from it. + +Sender and receiver have distinct request patterns: +- Receiver: long-poll GETs, then a POST +- Sender: a POST, then long-poll GETs + +OHTTP does not hide the client IP from the relay. A relay that sees the same +client repeatedly can observe its access patterns to infer whether +the IP is associated with a sender or receiver, potentially linking to identity or +location. Based on when a session ends it may be easier to correctly guess +whether a transaction is a PayJoin. The IP address linked information may +additionally aid in cluster analysis, for example whether a cluster's temporal +patterns are consistent with a location guess for the IP address. + +## Health checks + +Some clients call `fetch_ohttp_keys` periodically to verify that the +directory and relay infrastructure is reachable. Given the threat model +above, this is acceptable only when: + +- The call is **not** triggered on any deterministic, recurring event + (e.g. app startup, periodic timer). Prefer user-initiated actions + (e.g. opening a settings/status screen) or piggybacking on operations + the user already triggered (e.g. resuming an existing session). +- The caller throttles invocations so they don't produce a recurring + timing pattern observable by the relay. + +A health check has a distinct traffic pattern from a real payjoin request +and is not temporally tied to any onchain broadcast, but repeated calls +still expose the client IP to the relay. +""" + import ssl from urllib.parse import urljoin diff --git a/payjoin/src/core/io.rs b/payjoin/src/core/io.rs index 2cbc0010b..2585434fa 100644 --- a/payjoin/src/core/io.rs +++ b/payjoin/src/core/io.rs @@ -1,4 +1,40 @@ //! IO-related types and functions. Specifically, fetching OHTTP keys from a payjoin directory. +//! +//! When multiple relays are configured, callers **should pick one at random per request** +//! to avoid a fixed contact pattern at the network layer. +//! +//! Random selection only helps if the relay list itself is not identifying: prefer a shared +//! relay list and discourage isolated infrastructure that other apps don't use, since a +//! distinctive list fingerprints the client regardless of how a relay is picked from it. +//! +//! Sender and receiver have distinct request patterns: +//! - Receiver: long-poll GETs, then a POST +//! - Sender: a POST, then long-poll GETs +//! +//! OHTTP does not hide the client IP from the relay. A relay that sees the same +//! client repeatedly can observe its access patterns to infer whether +//! the IP is associated with a sender or receiver, potentially linking to identity or +//! location. Based on when a session ends it may be easier to correctly guess +//! whether a transaction is a PayJoin. The IP address linked information may +//! additionally aid in cluster analysis, for example whether a cluster's temporal +//! patterns are consistent with a location guess for the IP address. +//! +//! ## Health checks +//! +//! Some clients call [`fetch_ohttp_keys`] periodically to verify that the +//! directory and relay infrastructure is reachable. Given the threat model +//! above, this is acceptable only when: +//! +//! - The call is **not** triggered on any deterministic, recurring event +//! (e.g. app startup, periodic timer). Prefer user-initiated actions +//! (e.g. opening a settings/status screen) or piggybacking on operations +//! the user already triggered (e.g. resuming an existing session). +//! - The caller throttles invocations so they don't produce a recurring +//! timing pattern observable by the relay. +//! +//! A health check has a distinct traffic pattern from a real payjoin request +//! and is not temporally tied to any onchain broadcast, but repeated calls +//! still expose the client IP to the relay. use std::time::Duration; use http::header::ACCEPT;