Skip to content

Commit d742829

Browse files
committed
Protect metadata with Oblivious HTTP
1 parent 515e8e1 commit d742829

File tree

9 files changed

+687
-62
lines changed

9 files changed

+687
-62
lines changed

Cargo.lock

Lines changed: 421 additions & 36 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

payjoin-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ v2 = ["payjoin/v2", "tokio/full", "tokio-tungstenite", "futures-util/sink", "fut
1818
[dependencies]
1919
anyhow = "1.0.70"
2020
base64 = "0.13.0"
21+
bhttp = { version = "0.4.0", features = ["http", "bhttp"] }
2122
bip21 = "0.3.1"
2223
bitcoincore-rpc = "0.17.0"
2324
clap = "4.1.4"
@@ -26,6 +27,7 @@ env_logger = "0.9.0"
2627
futures = "0.3.28"
2728
futures-util = { version = "0.3.28", default-features = false }
2829
log = "0.4.7"
30+
ohttp = "0.4.0"
2931
payjoin = { path = "../payjoin", features = ["send", "receive"] }
3032
reqwest = { version = "0.11.4", features = ["blocking"] }
3133
rcgen = { version = "0.11.1", optional = true }

payjoin-cli/src/app.rs

Lines changed: 107 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ use bitcoincore_rpc::jsonrpc::serde_json;
99
use bitcoincore_rpc::RpcApi;
1010
use clap::ArgMatches;
1111
use config::{Config, File, FileFormat};
12+
use ohttp::ClientResponse;
1213
use payjoin::bitcoin::psbt::Psbt;
1314
use payjoin::receive::{Error, PayjoinProposal, UncheckedProposal};
1415
use payjoin::{bitcoin, PjUriExt, UriExt};
16+
use reqwest::Request;
1517
#[cfg(not(feature = "v2"))]
1618
use rouille::{Request, Response};
1719
use serde::{Deserialize, Serialize};
@@ -50,30 +52,44 @@ impl App {
5052
.danger_accept_invalid_certs(self.config.danger_accept_invalid_certs)
5153
.build()
5254
.with_context(|| "Failed to build reqwest http client")?;
53-
let _ = client
55+
dbg!(&req.body);
56+
let pre_req = client
5457
.post(req.url.as_str())
5558
.body(req.body)
5659
.header("Content-Type", "text/plain")
57-
.send()
58-
.await
59-
.with_context(|| "HTTP request failed")?;
60+
.build()
61+
.with_context(|| "Failed to build HTTP request")?;
62+
let (ohttp_req, _) = self.ohttp_encapsulate_req(pre_req);
63+
dbg!(&ohttp_req);
64+
let _ = client.post(&self.config.ohttp_proxy).body(ohttp_req).send().await?;
6065

6166
log::debug!("Awaiting response");
62-
let res = Self::long_poll(&client, req.url.as_str()).await?;
67+
let res = self.long_poll(&client, req.url.as_str()).await?;
6368
let mut res = std::io::Cursor::new(&res);
6469
self.process_pj_response(ctx, &mut res)?;
6570
Ok(())
6671
}
6772

6873
#[cfg(feature = "v2")]
69-
async fn long_poll(client: &reqwest::Client, url: &str) -> Result<Vec<u8>, reqwest::Error> {
74+
async fn long_poll(
75+
&self,
76+
client: &reqwest::Client,
77+
url: &str,
78+
) -> Result<Vec<u8>, reqwest::Error> {
7079
loop {
71-
let response = client.get(url).send().await?;
72-
73-
if response.status().is_success() {
74-
let body = response.bytes().await?;
80+
let req = client.get(url).build()?;
81+
let (ohttp_req, ctx) = self.ohttp_encapsulate_req(req);
82+
83+
let ohttp_response =
84+
client.post(&self.config.ohttp_proxy).body(ohttp_req).send().await?;
85+
log::debug!("Response: {:?}", ohttp_response);
86+
if ohttp_response.status().is_success() {
87+
let body = ohttp_response.bytes().await?;
7588
if !body.is_empty() {
76-
return Ok(body.to_vec());
89+
let bhttp_response = ctx.decapsulate(&body).unwrap();
90+
let mut r = std::io::Cursor::new(bhttp_response);
91+
let response = bhttp::Message::read_bhttp(&mut r).unwrap();
92+
return Ok(response.content().to_vec());
7793
} else {
7894
log::info!("No response yet for payjoin request, retrying in 5 seconds");
7995
}
@@ -83,6 +99,33 @@ impl App {
8399
}
84100
}
85101

102+
fn ohttp_encapsulate_req(&self, req: Request) -> (Vec<u8>, ClientResponse) {
103+
let ohttp_config = payjoin::bitcoin::base64::decode_config(
104+
&self.config.ohttp_config,
105+
payjoin::bitcoin::base64::URL_SAFE,
106+
)
107+
.unwrap();
108+
let ctx = ohttp::ClientRequest::from_encoded_config(&ohttp_config).unwrap();
109+
110+
let mut bhttp_message = bhttp::Message::request(
111+
req.method().as_str().as_bytes().to_vec(),
112+
req.url().scheme().as_bytes().to_vec(),
113+
req.url().authority().as_bytes().to_vec(),
114+
req.url().path().as_bytes().to_vec(),
115+
);
116+
match req.body() {
117+
Some(body) => {
118+
bhttp_message.write_content(body.as_bytes().unwrap());
119+
}
120+
None => (),
121+
}
122+
// let req = serialize_request_to_bytes(req);
123+
// let http_message = bhttp::Message::read_http(&mut std::io::Cursor::new(&req)).unwrap();
124+
let mut bhttp_req = Vec::new();
125+
let _ = bhttp_message.write_bhttp(bhttp::Mode::KnownLength, &mut bhttp_req);
126+
ctx.encapsulate(&bhttp_req).unwrap()
127+
}
128+
86129
#[cfg(not(feature = "v2"))]
87130
pub fn send_payjoin(&self, bip21: &str) -> Result<()> {
88131
let (req, ctx) = self.create_pj_request(bip21)?;
@@ -102,6 +145,23 @@ impl App {
102145
Ok(())
103146
}
104147

148+
// fn create_v2_pj_request(&self, bip21: &str,
149+
// ) -> Result<(payjoin::send::Request, payjoin::send::Context)> {
150+
// let (req, ctx) = self.create_pj_request(bip21)?;
151+
// let config = base64::decode(&self.config.ohttp_config)?;
152+
// let req_ctx = ohttp::ClientRequest::from_encoded_config(&config)
153+
// .with_context(|| "Failed to decode ohttp config")?;
154+
// let (enc_req, req_ctx) = req_ctx.encapsulate(&req.body).with_context(|| "Failed to encapsulate request")?;
155+
156+
// Ok((payjoin::send::Request {
157+
// url: req.url,
158+
// body: enc_req,
159+
// }, payjoin::send::Context {
160+
// ohttp_ctx: req_ctx,
161+
// ..ctx
162+
// }))
163+
// }
164+
105165
fn create_pj_request(
106166
&self,
107167
bip21: &str,
@@ -221,7 +281,7 @@ impl App {
221281
.with_context(|| "Failed to build reqwest http client")?;
222282
log::debug!("Awaiting request");
223283
let receive_endpoint = format!("{}/{}", self.config.pj_endpoint, context.receive_subdir());
224-
let mut buffer = Self::long_poll(&client, &receive_endpoint).await?;
284+
let mut buffer = self.long_poll(&client, &receive_endpoint).await?;
225285

226286
log::debug!("Received request");
227287
let (proposal, e) = context
@@ -232,9 +292,15 @@ impl App {
232292
.map_err(|e| anyhow!("Failed to process UncheckedProposal {}", e))?;
233293
let mut payjoin_bytes = payjoin_psbt.serialize();
234294
let payjoin = payjoin::v2::encrypt_message_b(&mut payjoin_bytes, e);
235-
let _ = client
295+
let req = client
236296
.post(receive_endpoint)
237297
.body(payjoin)
298+
.build()
299+
.with_context(|| "Failed to build HTTP request")?;
300+
let (req, _) = self.ohttp_encapsulate_req(req);
301+
let _ = client
302+
.post(&self.config.ohttp_proxy)
303+
.body(req)
238304
.send()
239305
.await
240306
.with_context(|| "HTTP request failed")?;
@@ -274,14 +340,15 @@ impl App {
274340
let amount = Amount::from_sat(amount_arg.parse()?);
275341
//let subdir = self.config.pj_endpoint + pubkey.map_or(&String::from(""), |s| &format!("/{}", s));
276342
let pj_uri_string = format!(
277-
"{}?amount={}&pj={}",
343+
"{}?amount={}&pj={}&ohttp={}",
278344
pj_receiver_address.to_qr_uri(),
279345
amount.to_btc(),
280346
format!(
281347
"{}{}",
282348
self.config.pj_endpoint,
283349
pubkey.map_or(String::from(""), |s| format!("/{}", s))
284-
)
350+
),
351+
self.config.ohttp_config,
285352
);
286353
let pj_uri = payjoin::Uri::from_str(&pj_uri_string)
287354
.map_err(|e| anyhow!("Constructed a bad URI string from args: {}", e))?;
@@ -465,6 +532,19 @@ impl App {
465532
}
466533
}
467534

535+
fn serialize_request_to_bytes(req: reqwest::Request) -> Vec<u8> {
536+
let mut serialized_request =
537+
format!("{} {} HTTP/1.1\r\n", req.method(), req.url()).into_bytes();
538+
539+
for (name, value) in req.headers().iter() {
540+
let header_line = format!("{}: {}\r\n", name.as_str(), value.to_str().unwrap());
541+
serialized_request.extend(header_line.as_bytes());
542+
}
543+
544+
serialized_request.extend(b"\r\n");
545+
serialized_request
546+
}
547+
468548
struct SeenInputs {
469549
set: OutPointSet,
470550
file: std::fs::File,
@@ -504,6 +584,8 @@ pub(crate) struct AppConfig {
504584
pub bitcoind_cookie: Option<String>,
505585
pub bitcoind_rpcuser: String,
506586
pub bitcoind_rpcpass: String,
587+
pub ohttp_config: String,
588+
pub ohttp_proxy: String,
507589

508590
// send-only
509591
pub danger_accept_invalid_certs: bool,
@@ -537,6 +619,16 @@ impl AppConfig {
537619
"bitcoind_rpcpass",
538620
matches.get_one::<String>("rpcpass").map(|s| s.as_str()),
539621
)?
622+
.set_default("ohttp_config", "")?
623+
.set_override_option(
624+
"ohttp_config",
625+
matches.get_one::<String>("ohttp_config").map(|s| s.as_str()),
626+
)?
627+
.set_default("ohttp_proxy", "")?
628+
.set_override_option(
629+
"ohttp_proxy",
630+
matches.get_one::<String>("ohttp_proxy").map(|s| s.as_str()),
631+
)?
540632
// Subcommand defaults without which file serialization fails.
541633
.set_default("danger_accept_invalid_certs", false)?
542634
.set_default("pj_host", "0.0.0.0:3000")?

payjoin-cli/src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ fn cli() -> ArgMatches {
7373
.long("rpcpass")
7474
.help("The password for the bitcoin node"))
7575
.subcommand_required(true)
76+
.arg(Arg::new("ohttp_config")
77+
.long("ohttp-config")
78+
.help("The ohttp config file"))
79+
.arg(Arg::new("ohttp_proxy")
80+
.long("ohttp-proxy")
81+
.help("The ohttp proxy url"))
7682
.subcommand(
7783
Command::new("send")
7884
.arg_required_else_help(true)

payjoin-relay/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@ edition = "2021"
99
axum = "0.6.2"
1010
anyhow = "1.0.71"
1111
futures-util = { version = "0.3.28", default-features = false, features = ["sink", "std"] }
12+
hex = "0.4.3"
13+
hyper = "0.14.27"
14+
http = "0.2.4"
15+
# ohttp = "0.4.0"
16+
httparse = "1.8.0"
17+
ohttp = { path = "../../ohttp/ohttp" }
18+
bhttp = { version = "0.4.0", features = ["http"] }
1219
payjoin = { path = "../payjoin", features = ["v2"] }
1320
sqlx = { version = "0.7.1", features = ["postgres", "runtime-tokio"] }
1421
tokio = { version = "1.12.0", features = ["full"] }
1522
tokio-tungstenite = "0.20.0"
23+
tower-service = "0.3.2"
1624
tracing = "0.1.37"
1725
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }

payjoin-relay/src/main.rs

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@ use std::error::Error;
22
use std::sync::Arc;
33
use std::time::Duration;
44

5-
use anyhow::Result;
5+
use anyhow::{Result, Context};
66
use axum::body::Bytes;
77
use axum::extract::Path;
8-
use axum::http::StatusCode;
8+
use axum::http::{StatusCode, Request};
99
use axum::routing::{get, post};
1010
use axum::Router;
1111
use futures_util::stream::{SplitSink, SplitStream};
1212
use futures_util::{SinkExt, StreamExt};
1313
use payjoin::v2::{MAX_BUFFER_SIZE, RECEIVE};
1414
use tokio::net::{TcpListener, TcpStream};
15-
use tokio_tungstenite::tungstenite::handshake::server::Request;
1615
use tokio_tungstenite::tungstenite::Message;
1716
use tokio_tungstenite::{accept_hdr_async, WebSocketStream};
1817
use tracing::{debug, error, info, info_span, Instrument};
@@ -26,8 +25,9 @@ use crate::db::DbPool;
2625
async fn main() -> Result<(), Box<dyn std::error::Error>> {
2726
init_logging();
2827
let pool = DbPool::new(std::time::Duration::from_secs(30)).await?;
29-
30-
let app = Router::new()
28+
let ohttp = Arc::new(init_ohttp()?);
29+
let ohttp_config = ohttp_config(&*ohttp)?;
30+
let target_resource = Router::new()
3131
.route(
3232
"/:id",
3333
post({
@@ -50,9 +50,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5050
move |id, body| post_payjoin(id, body, pool)
5151
}),
5252
);
53+
54+
let ohttp_gateway = Router::new()
55+
.route("/", post( move |body| handle_ohttp(body, target_resource, ohttp)))
56+
.route("/ohttp-keys", get({
57+
move || get_ohttp_config(ohttp_config)}));
58+
5359

5460
println!("Serverless payjoin relay awaiting HTTP connection on port 8080");
55-
axum::Server::bind(&"0.0.0.0:8080".parse()?).serve(app.into_make_service()).await?;
61+
axum::Server::bind(&"0.0.0.0:8080".parse()?).serve(ohttp_gateway.into_make_service()).await?;
62+
//hyper::Server::bind(&"0.0.0.0:8080").serve()
5663
Ok(())
5764
}
5865

@@ -65,6 +72,72 @@ fn init_logging() {
6572
println!("Logging initialized");
6673
}
6774

75+
fn init_ohttp() -> Result<ohttp::Server> {
76+
use ohttp::hpke::{Aead, Kdf, Kem};
77+
use ohttp::{KeyId, SymmetricSuite};
78+
79+
const KEY_ID: KeyId = 1;
80+
const KEM: Kem = Kem::X25519Sha256;
81+
const SYMMETRIC: &[SymmetricSuite] =
82+
&[SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)];
83+
84+
// create or read from file
85+
let server_config = ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap();
86+
let encoded_config = server_config.encode().unwrap();
87+
let b64_config = payjoin::bitcoin::base64::encode_config(&encoded_config, payjoin::bitcoin::base64::Config::new(payjoin::bitcoin::base64::CharacterSet::UrlSafe, false));
88+
info!("ohttp server config base64 UrlSafe: {:?}", b64_config);
89+
ohttp::Server::new(server_config).with_context(|| "Failed to initialize ohttp server")
90+
}
91+
92+
async fn handle_ohttp(enc_request: Bytes, mut target: Router, ohttp: Arc<ohttp::Server>) -> (StatusCode, Vec<u8>) {
93+
use tower_service::Service;
94+
use axum::body::Body;
95+
use http::Uri;
96+
97+
// decapsulate
98+
let (bhttp_req, res_ctx) = ohttp.decapsulate(&enc_request).unwrap();
99+
let mut cursor = std::io::Cursor::new(bhttp_req);
100+
let req = bhttp::Message::read_bhttp(&mut cursor).unwrap();
101+
// let parsed_request: httparse::Request = httparse::Request::new(&mut vec![]).parse(cursor).unwrap();
102+
// // handle request
103+
// Request::new
104+
let uri = Uri::builder()
105+
.scheme(req.control().scheme().unwrap())
106+
.authority(req.control().authority().unwrap())
107+
.path_and_query(req.control().path().unwrap())
108+
.build()
109+
.unwrap();
110+
let body = req.content().to_vec();
111+
let mut request = Request::builder()
112+
.uri(uri)
113+
.method(req.control().method().unwrap());
114+
for header in req.header().fields() {
115+
request = request.header(header.name(), header.value())
116+
}
117+
let request = request
118+
.body(Body::from(body))
119+
.unwrap();
120+
121+
let response = target.call(request).await.unwrap();
122+
123+
let (parts, body) = response.into_parts();
124+
let mut bhttp_res = bhttp::Message::response(parts.status.as_u16());
125+
let full_body = hyper::body::to_bytes(body).await.unwrap();
126+
bhttp_res.write_content(&full_body);
127+
let mut bhttp_bytes = Vec::new();
128+
bhttp_res.write_bhttp(bhttp::Mode::KnownLength, &mut bhttp_bytes).unwrap();
129+
let ohttp_res = res_ctx.encapsulate(&bhttp_bytes).unwrap();
130+
(StatusCode::OK, ohttp_res)
131+
}
132+
133+
fn ohttp_config(server: &ohttp::Server) -> Result<String> {
134+
use payjoin::bitcoin::base64;
135+
136+
let b64_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
137+
let encoded_config = server.config().encode().with_context(|| "Failed to encode ohttp config")?;
138+
Ok(base64::encode_config(&encoded_config, b64_config))
139+
}
140+
68141
async fn post_fallback(Path(id): Path<String>, body: Bytes, pool: DbPool) -> (StatusCode, String) {
69142
let id = shorten_string(&id);
70143
let body = body.to_vec();
@@ -78,6 +151,10 @@ async fn post_fallback(Path(id): Path<String>, body: Bytes, pool: DbPool) -> (St
78151
}
79152
}
80153

154+
async fn get_ohttp_config(config: String) -> (StatusCode, String) {
155+
(StatusCode::OK, config)
156+
}
157+
81158
async fn get_request(Path(id): Path<String>, pool: DbPool) -> (StatusCode, Vec<u8>) {
82159
let id = shorten_string(&id);
83160
match pool.peek_req(&id).await {

0 commit comments

Comments
 (0)