Skip to content

Commit 7ddabb1

Browse files
committed
Protect metadata with Oblivious HTTP
1 parent 86a64e6 commit 7ddabb1

File tree

15 files changed

+1029
-326
lines changed

15 files changed

+1029
-326
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[workspace]
2+
resolver = "2"
23
members = ["payjoin", "payjoin-cli", "payjoin-relay"]
34

payjoin-cli/src/app.rs

Lines changed: 77 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ use clap::ArgMatches;
1111
use config::{Config, File, FileFormat};
1212
use payjoin::bitcoin::psbt::Psbt;
1313
use payjoin::bitcoin::{self, base64};
14-
use payjoin::receive::{Error, PayjoinProposal, ProvisionalProposal, UncheckedProposal};
14+
use payjoin::receive::{
15+
EnrollContext, Error, PayjoinProposal, ProvisionalProposal, UncheckedProposal,
16+
};
17+
use payjoin::send::RequestContext;
1518
#[cfg(not(feature = "v2"))]
1619
use rouille::{Request, Response};
1720
use serde::{Deserialize, Serialize};
@@ -44,64 +47,61 @@ impl App {
4447

4548
#[cfg(feature = "v2")]
4649
pub fn send_payjoin(&self, bip21: &str, fee_rate: &f32) -> Result<()> {
47-
let (req, ctx) = self.create_pj_request(bip21, fee_rate)?;
50+
let req_ctx = self.create_pj_request(bip21, fee_rate)?;
4851

4952
let client = reqwest::blocking::Client::builder()
5053
.danger_accept_invalid_certs(self.config.danger_accept_invalid_certs)
5154
.build()
5255
.with_context(|| "Failed to build reqwest http client")?;
5356

5457
log::debug!("Awaiting response");
55-
let res = Self::long_poll_post(&client, req)?;
56-
let mut res = std::io::Cursor::new(&res);
57-
self.process_pj_response(ctx, &mut res)?;
58+
let res = self.long_poll_post(&client, req_ctx)?;
59+
self.process_pj_response(res)?;
5860
Ok(())
5961
}
6062

6163
#[cfg(feature = "v2")]
6264
fn long_poll_post(
65+
&self,
6366
client: &reqwest::blocking::Client,
64-
req: payjoin::send::Request,
65-
) -> Result<Vec<u8>, reqwest::Error> {
67+
req_ctx: payjoin::send::RequestContext<'_>,
68+
) -> Result<Psbt> {
6669
loop {
70+
let (req, ctx) = req_ctx.extract_v2(&self.config.ohttp_proxy)?;
6771
let response = client
68-
.post(req.url.as_str())
72+
.post(req.url)
6973
.body(req.body.clone())
7074
.header("Content-Type", "text/plain")
71-
.header("Async", "true")
7275
.send()?;
7376

74-
if response.status() == reqwest::StatusCode::OK {
75-
let body = response.bytes()?.to_vec();
76-
return Ok(body);
77-
} else if response.status() == reqwest::StatusCode::ACCEPTED {
77+
let bytes = response.bytes()?;
78+
let mut cursor = std::io::Cursor::new(bytes);
79+
let psbt = ctx.process_response(&mut cursor)?;
80+
if let Some(psbt) = psbt {
81+
return Ok(psbt);
82+
} else {
7883
log::info!("No response yet for POST payjoin request, retrying some seconds");
7984
std::thread::sleep(std::time::Duration::from_secs(5));
80-
} else {
81-
log::error!("Unexpected response status: {}", response.status());
82-
// TODO handle error
83-
panic!("Unexpected response status: {}", response.status())
8485
}
8586
}
8687
}
8788

8889
#[cfg(feature = "v2")]
8990
fn long_poll_get(
91+
&self,
9092
client: &reqwest::blocking::Client,
91-
url: &str,
92-
) -> Result<reqwest::blocking::Response, reqwest::Error> {
93+
enroll_context: &mut EnrollContext,
94+
) -> Result<UncheckedProposal, reqwest::Error> {
9395
loop {
94-
let response = client.get(url).send()?;
95-
96-
if response.status() == reqwest::StatusCode::OK {
97-
return Ok(response);
98-
} else if response.status() == reqwest::StatusCode::ACCEPTED {
99-
log::info!("No response yet for GET payjoin request, retrying in 5 seconds");
100-
std::thread::sleep(std::time::Duration::from_secs(5));
101-
} else {
102-
log::error!("Unexpected response status: {}", response.status());
103-
// TODO handle error
104-
panic!("Unexpected response status: {}", response.status())
96+
let (payjoin_get_body, context) = enroll_context.payjoin_get_body();
97+
let ohttp_response =
98+
client.post(&self.config.ohttp_proxy).body(payjoin_get_body).send()?;
99+
let ohttp_response = ohttp_response.bytes()?;
100+
let proposal =
101+
enroll_context.parse_relay_response(ohttp_response.as_ref(), context).unwrap();
102+
match proposal {
103+
Some(proposal) => return Ok(proposal),
104+
None => std::thread::sleep(std::time::Duration::from_secs(5)),
105105
}
106106
}
107107
}
@@ -125,11 +125,7 @@ impl App {
125125
Ok(())
126126
}
127127

128-
fn create_pj_request(
129-
&self,
130-
bip21: &str,
131-
fee_rate: &f32,
132-
) -> Result<(payjoin::send::Request, payjoin::send::Context)> {
128+
fn create_pj_request<'a>(&self, bip21: &'a str, fee_rate: &f32) -> Result<RequestContext<'a>> {
133129
let uri = payjoin::Uri::try_from(bip21)
134130
.map_err(|e| anyhow!("Failed to create URI from BIP21: {}", e))?;
135131

@@ -170,22 +166,16 @@ impl App {
170166
.psbt;
171167
let psbt = Psbt::from_str(&psbt).with_context(|| "Failed to load PSBT from base64")?;
172168
log::debug!("Original psbt: {:#?}", psbt);
173-
174-
let (req, ctx) = payjoin::send::RequestBuilder::from_psbt_and_uri(psbt, uri)
169+
let req_ctx = payjoin::send::RequestBuilder::from_psbt_and_uri(psbt, uri)
175170
.with_context(|| "Failed to build payjoin request")?
176171
.build_recommended(fee_rate)
177172
.with_context(|| "Failed to build payjoin request")?;
178173

179-
Ok((req, ctx))
174+
Ok(req_ctx)
180175
}
181176

182-
fn process_pj_response(
183-
&self,
184-
ctx: payjoin::send::Context,
185-
response: &mut impl std::io::Read,
186-
) -> Result<bitcoin::Txid> {
177+
fn process_pj_response(&self, psbt: Psbt) -> Result<bitcoin::Txid> {
187178
// TODO display well-known errors and log::debug the rest
188-
let psbt = ctx.process_response(response).with_context(|| "Failed to process response")?;
189179
log::debug!("Proposed psbt: {:#?}", psbt);
190180
let psbt = self
191181
.bitcoind
@@ -221,7 +211,11 @@ impl App {
221211

222212
#[cfg(feature = "v2")]
223213
pub fn receive_payjoin(self, amount_arg: &str) -> Result<()> {
224-
let context = payjoin::receive::ProposalContext::new();
214+
let mut context = EnrollContext::from_relay_config(
215+
&self.config.pj_endpoint,
216+
&self.config.ohttp_config,
217+
&self.config.ohttp_proxy,
218+
);
225219
let pj_uri_string =
226220
self.construct_payjoin_uri(amount_arg, Some(&context.subdirectory()))?;
227221
println!(
@@ -235,25 +229,25 @@ impl App {
235229
.build()
236230
.with_context(|| "Failed to build reqwest http client")?;
237231
log::debug!("Awaiting request");
238-
let _enroll = client.post(&self.config.pj_endpoint).body(pubkey_base64.clone()).send()?;
239-
240-
let receive_endpoint = format!("{}/{}", self.config.pj_endpoint, context.receive_subdir());
241-
let res = Self::long_poll_get(&client, &receive_endpoint)?;
232+
let _enroll = client.post(&self.config.pj_endpoint).body(context.enroll_body()).send()?;
242233

234+
log::debug!("Awaiting proposal");
235+
let res = self.long_poll_get(&client, &mut context)?;
243236
log::debug!("Received request");
244-
let proposal = context
245-
.parse_relay_response(res)
246-
.map_err(|e| anyhow!("Failed to parse into UncheckedProposal {}", e))?;
247237
let payjoin_proposal = self
248238
.process_proposal(proposal)
249239
.map_err(|e| anyhow!("Failed to process UncheckedProposal {}", e))?;
250240
let payjoin_endpoint = format!("{}/{}/receive", self.config.pj_endpoint, pubkey_base64);
251-
let body = payjoin_proposal.serialize_body();
252-
let _ = client
253-
.post(payjoin_endpoint)
241+
let (body, ohttp_ctx) =
242+
payjoin_proposal.extract_v2_req(&self.config.ohttp_config, &payjoin_endpoint);
243+
let res = client
244+
.post(&self.config.ohttp_proxy)
254245
.body(body)
255246
.send()
256247
.with_context(|| "HTTP request failed")?;
248+
let res = res.bytes()?;
249+
let res = payjoin_proposal.deserialize_res(res.to_vec(), ohttp_ctx);
250+
log::debug!("Received response {:?}", res);
257251
Ok(())
258252
}
259253

@@ -289,14 +283,15 @@ impl App {
289283
let pj_receiver_address = self.bitcoind.get_new_address(None, None)?.assume_checked();
290284
let amount = Amount::from_sat(amount_arg.parse()?);
291285
let pj_uri_string = format!(
292-
"{}?amount={}&pj={}",
286+
"{}?amount={}&pj={}&ohttp={}",
293287
pj_receiver_address.to_qr_uri(),
294288
amount.to_btc(),
295289
format!(
296290
"{}{}",
297291
self.config.pj_endpoint,
298292
pubkey.map_or(String::from(""), |s| format!("/{}", s))
299-
)
293+
),
294+
self.config.ohttp_config,
300295
);
301296

302297
// to check uri validity
@@ -475,6 +470,19 @@ impl App {
475470
}
476471
}
477472

473+
fn serialize_request_to_bytes(req: reqwest::Request) -> Vec<u8> {
474+
let mut serialized_request =
475+
format!("{} {} HTTP/1.1\r\n", req.method(), req.url()).into_bytes();
476+
477+
for (name, value) in req.headers().iter() {
478+
let header_line = format!("{}: {}\r\n", name.as_str(), value.to_str().unwrap());
479+
serialized_request.extend(header_line.as_bytes());
480+
}
481+
482+
serialized_request.extend(b"\r\n");
483+
serialized_request
484+
}
485+
478486
struct SeenInputs {
479487
set: OutPointSet,
480488
file: std::fs::File,
@@ -514,6 +522,8 @@ pub(crate) struct AppConfig {
514522
pub bitcoind_cookie: Option<String>,
515523
pub bitcoind_rpcuser: String,
516524
pub bitcoind_rpcpass: String,
525+
pub ohttp_config: String,
526+
pub ohttp_proxy: String,
517527

518528
// send-only
519529
pub danger_accept_invalid_certs: bool,
@@ -547,6 +557,16 @@ impl AppConfig {
547557
"bitcoind_rpcpass",
548558
matches.get_one::<String>("rpcpass").map(|s| s.as_str()),
549559
)?
560+
.set_default("ohttp_config", "")?
561+
.set_override_option(
562+
"ohttp_config",
563+
matches.get_one::<String>("ohttp_config").map(|s| s.as_str()),
564+
)?
565+
.set_default("ohttp_proxy", "")?
566+
.set_override_option(
567+
"ohttp_proxy",
568+
matches.get_one::<String>("ohttp_proxy").map(|s| s.as_str()),
569+
)?
550570
// Subcommand defaults without which file serialization fails.
551571
.set_default("danger_accept_invalid_certs", false)?
552572
.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
@@ -47,6 +47,12 @@ fn cli() -> ArgMatches {
4747
.long("rpcpass")
4848
.help("The password for the bitcoin node"))
4949
.subcommand_required(true)
50+
.arg(Arg::new("ohttp_config")
51+
.long("ohttp-config")
52+
.help("The ohttp config file"))
53+
.arg(Arg::new("ohttp_proxy")
54+
.long("ohttp-proxy")
55+
.help("The ohttp proxy url"))
5056
.subcommand(
5157
Command::new("send")
5258
.arg_required_else_help(true)

payjoin-relay/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ edition = "2021"
99
hyper = { version = "0.14", features = ["full"] }
1010
anyhow = "1.0.71"
1111
payjoin = { path = "../payjoin", features = ["base64"] }
12+
# ohttp = "0.4.0"
13+
ohttp = { path = "../../ohttp/ohttp" }
14+
bhttp = { version = "0.4.0", features = ["http"] }
1215
sqlx = { version = "0.7.1", features = ["postgres", "runtime-tokio"] }
1316
tokio = { version = "1.12.0", features = ["full"] }
1417
tracing = "0.1.37"

0 commit comments

Comments
 (0)