From 6282ffb2c76a93e1849ecc1a84c9f54ccf152cc5 Mon Sep 17 00:00:00 2001 From: Yuval Kogman Date: Sat, 22 Feb 2025 01:00:24 +0100 Subject: [PATCH] Announce allowed purposes in gateway This change signals to relays (see payjoin/ohttp-relay#58) that even if the gateway is not known to the relay, requests are still allowed for the purposes of BIP 77. Because receivers choose the directory (which is its own gateway), but senders choose the relay, before introducing this functionality it this would have been the responsibility of the sender (or more realistically their wallet vendor) to choose an appropriate relay, one that accepts requests on behalf of the receiver's chosen directory. Rationale for adding a UUID: although arguably 64 bits of randomness is more than enough, and even just the string "BIP 77" is likely to be unique and unambiguous, adding a UUID practically ensures an opt-in is really an opt-in as 128 random bits would not be repeated except intentionally. Rationale for format: we just need a set of opaque identifiers, encoding the same way as TLS ALPN does makes sense since that's well specified, binary safe, easy to parse and places readonable limits on string lengths. --- payjoin-directory/src/lib.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/payjoin-directory/src/lib.rs b/payjoin-directory/src/lib.rs index 7287752b1..3dd1fb18a 100644 --- a/payjoin-directory/src/lib.rs +++ b/payjoin-directory/src/lib.rs @@ -187,7 +187,8 @@ async fn serve_payjoin_directory( let mut response = match (parts.method, path_segments.as_slice()) { (Method::POST, ["", ".well-known", "ohttp-gateway"]) => handle_ohttp_gateway(body, pool, ohttp).await, - (Method::GET, ["", ".well-known", "ohttp-gateway"]) => get_ohttp_keys(&ohttp).await, + (Method::GET, ["", ".well-known", "ohttp-gateway"]) => + handle_ohttp_gateway_get(&ohttp, &query).await, (Method::POST, ["", ""]) => handle_ohttp_gateway(body, pool, ohttp).await, (Method::GET, ["", "ohttp-keys"]) => get_ohttp_keys(&ohttp).await, (Method::POST, ["", id]) => post_fallback_v1(id, query, body, pool).await, @@ -426,6 +427,16 @@ fn not_found() -> Response> { res } +async fn handle_ohttp_gateway_get( + ohttp: &Arc>, + query: &str, +) -> Result>, HandlerError> { + match query { + "allowed_purposes" => Ok(get_ohttp_allowed_purposes().await), + _ => get_ohttp_keys(ohttp).await, + } +} + async fn get_ohttp_keys( ohttp: &Arc>, ) -> Result>, HandlerError> { @@ -440,6 +451,23 @@ async fn get_ohttp_keys( Ok(res) } +async fn get_ohttp_allowed_purposes() -> Response> { + // Encode the magic string in the same format as a TLS ALPN protocol list (a + // U16BE length encoded list of U8 length encoded strings). + // + // The string is just "BIP77" followed by a UUID, that signals to relays + // that this OHTTP gateway will accept any requests associated with this + // purpose. + let mut res = Response::new(full(Bytes::from_static( + b"\x00\x01\x2aBIP77 454403bb-9f7b-4385-b31f-acd2dae20b7e", + ))); + + res.headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("application/x-ohttp-allowed-purposes")); + + res +} + fn empty() -> BoxBody { Empty::::new().map_err(|never| match never {}).boxed() }