Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions payjoin-cli/src/app/v2/ohttp.rs
Original file line number Diff line number Diff line change
@@ -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`].
//!
Expand Down
37 changes: 37 additions & 0 deletions payjoin-ffi/csharp/Payjoin.Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,43 @@

namespace Payjoin.Http
{
/// <summary>
/// Fetch OHTTP keys from a payjoin directory through an OHTTP relay.
/// </summary>
/// <remarks>
/// 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 <see cref="GetOhttpKeysAsync"/> 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.
/// </remarks>
internal sealed class OhttpKeysClient : IDisposable
{
private readonly HttpClient _client;
Expand Down
37 changes: 37 additions & 0 deletions payjoin-ffi/dart/lib/http.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
39 changes: 39 additions & 0 deletions payjoin-ffi/python/src/payjoin/http.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
36 changes: 36 additions & 0 deletions payjoin/src/core/io.rs
Original file line number Diff line number Diff line change
@@ -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
Comment thread
bc1cindy marked this conversation as resolved.
//! 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;
Expand Down
Loading