Skip to content

Commit c999b55

Browse files
committed
Protect metadata with Oblivious HTTP
1 parent 27b24ae commit c999b55

File tree

9 files changed

+658
-95
lines changed

9 files changed

+658
-95
lines changed

Cargo.lock

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

payjoin-cli/src/app.rs

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,30 +52,50 @@ impl App {
5252
.danger_accept_invalid_certs(self.config.danger_accept_invalid_certs)
5353
.build()
5454
.with_context(|| "Failed to build reqwest http client")?;
55-
let _ = client
56-
.post(req.url.as_str())
57-
.body(req.body)
58-
.header("Content-Type", "text/plain")
59-
.send()
60-
.await
61-
.with_context(|| "HTTP request failed")?;
55+
let ohttp_config =
56+
payjoin::bitcoin::base64::decode_config(&self.config.ohttp_config, base64::URL_SAFE)
57+
.unwrap();
58+
let (ohttp_req, _) =
59+
payjoin::v2::ohttp_encapsulate(ohttp_config, "POST", req.url.as_str(), Some(&req.body));
60+
dbg!(&ohttp_req);
61+
let _ = client.post(&self.config.ohttp_proxy).body(ohttp_req).send().await?;
6262

6363
log::debug!("Awaiting response");
64-
let res = Self::long_poll(&client, req.url.as_str()).await?;
64+
let res = self.long_poll(&client, req.url.as_str()).await?;
6565
let mut res = std::io::Cursor::new(&res);
6666
self.process_pj_response(ctx, &mut res)?;
6767
Ok(())
6868
}
6969

7070
#[cfg(feature = "v2")]
71-
async fn long_poll(client: &reqwest::Client, url: &str) -> Result<Vec<u8>, reqwest::Error> {
71+
async fn long_poll(
72+
&self,
73+
client: &reqwest::Client,
74+
url: &str,
75+
) -> Result<Vec<u8>, reqwest::Error> {
7276
loop {
73-
let response = client.get(url).send().await?;
74-
75-
if response.status().is_success() {
76-
let body = response.bytes().await?;
77+
let req = client.get(url).build()?;
78+
let ohttp_config = payjoin::bitcoin::base64::decode_config(
79+
&self.config.ohttp_config,
80+
payjoin::bitcoin::base64::URL_SAFE,
81+
)
82+
.unwrap();
83+
let body = req.body().and_then(|b| b.as_bytes());
84+
let (ohttp_req, ctx) = payjoin::v2::ohttp_encapsulate(
85+
ohttp_config,
86+
req.method().as_str(),
87+
req.url().as_str(),
88+
body,
89+
);
90+
91+
let ohttp_response =
92+
client.post(&self.config.ohttp_proxy).body(ohttp_req).send().await?;
93+
log::debug!("Response: {:?}", ohttp_response);
94+
if ohttp_response.status().is_success() {
95+
let body = ohttp_response.bytes().await?;
7796
if !body.is_empty() {
78-
return Ok(body.to_vec());
97+
let res_body = payjoin::v2::ohttp_decapsulate(ctx, &body);
98+
return Ok(res_body);
7999
} else {
80100
log::info!("No response yet for payjoin request, retrying in 5 seconds");
81101
}
@@ -215,7 +235,7 @@ impl App {
215235
.with_context(|| "Failed to build reqwest http client")?;
216236
log::debug!("Awaiting request");
217237
let receive_endpoint = format!("{}/{}", self.config.pj_endpoint, context.receive_subdir());
218-
let mut buffer = Self::long_poll(&client, &receive_endpoint).await?;
238+
let mut buffer = self.long_poll(&client, &receive_endpoint).await?;
219239

220240
log::debug!("Received request");
221241
let proposal = context
@@ -226,9 +246,14 @@ impl App {
226246
.map_err(|e| anyhow!("Failed to process UncheckedProposal {}", e))?;
227247

228248
let body = payjoin_proposal.serialize_body();
249+
let ohttp_config =
250+
payjoin::bitcoin::base64::decode_config(&self.config.ohttp_config, base64::URL_SAFE)
251+
.unwrap();
252+
let (req, _) =
253+
payjoin::v2::ohttp_encapsulate(ohttp_config, "POST", &receive_endpoint, Some(&body));
229254
let _ = client
230-
.post(receive_endpoint)
231-
.body(body)
255+
.post(&self.config.ohttp_proxy)
256+
.body(req)
232257
.send()
233258
.await
234259
.with_context(|| "HTTP request failed")?;
@@ -268,14 +293,15 @@ impl App {
268293
let amount = Amount::from_sat(amount_arg.parse()?);
269294
//let subdir = self.config.pj_endpoint + pubkey.map_or(&String::from(""), |s| &format!("/{}", s));
270295
let pj_uri_string = format!(
271-
"{}?amount={}&pj={}",
296+
"{}?amount={}&pj={}&ohttp={}",
272297
pj_receiver_address.to_qr_uri(),
273298
amount.to_btc(),
274299
format!(
275300
"{}{}",
276301
self.config.pj_endpoint,
277302
pubkey.map_or(String::from(""), |s| format!("/{}", s))
278-
)
303+
),
304+
self.config.ohttp_config,
279305
);
280306

281307
// check validity
@@ -459,6 +485,19 @@ impl App {
459485
}
460486
}
461487

488+
fn serialize_request_to_bytes(req: reqwest::Request) -> Vec<u8> {
489+
let mut serialized_request =
490+
format!("{} {} HTTP/1.1\r\n", req.method(), req.url()).into_bytes();
491+
492+
for (name, value) in req.headers().iter() {
493+
let header_line = format!("{}: {}\r\n", name.as_str(), value.to_str().unwrap());
494+
serialized_request.extend(header_line.as_bytes());
495+
}
496+
497+
serialized_request.extend(b"\r\n");
498+
serialized_request
499+
}
500+
462501
struct SeenInputs {
463502
set: OutPointSet,
464503
file: std::fs::File,
@@ -498,6 +537,8 @@ pub(crate) struct AppConfig {
498537
pub bitcoind_cookie: Option<String>,
499538
pub bitcoind_rpcuser: String,
500539
pub bitcoind_rpcpass: String,
540+
pub ohttp_config: String,
541+
pub ohttp_proxy: String,
501542

502543
// send-only
503544
pub danger_accept_invalid_certs: bool,
@@ -531,6 +572,16 @@ impl AppConfig {
531572
"bitcoind_rpcpass",
532573
matches.get_one::<String>("rpcpass").map(|s| s.as_str()),
533574
)?
575+
.set_default("ohttp_config", "")?
576+
.set_override_option(
577+
"ohttp_config",
578+
matches.get_one::<String>("ohttp_config").map(|s| s.as_str()),
579+
)?
580+
.set_default("ohttp_proxy", "")?
581+
.set_override_option(
582+
"ohttp_proxy",
583+
matches.get_one::<String>("ohttp_proxy").map(|s| s.as_str()),
584+
)?
534585
// Subcommand defaults without which file serialization fails.
535586
.set_default("danger_accept_invalid_certs", false)?
536587
.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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@ edition = "2021"
88
[dependencies]
99
axum = "0.6.2"
1010
anyhow = "1.0.71"
11+
hyper = "0.14.27"
12+
http = "0.2.4"
13+
# ohttp = "0.4.0"
14+
httparse = "1.8.0"
15+
ohttp = { path = "../../ohttp/ohttp" }
16+
bhttp = { version = "0.4.0", features = ["http"] }
1117
payjoin = { path = "../payjoin", features = ["v2"] }
1218
sqlx = { version = "0.7.1", features = ["postgres", "runtime-tokio"] }
1319
tokio = { version = "1.12.0", features = ["full"] }
20+
tower-service = "0.3.2"
1421
tracing = "0.1.37"
1522
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }

payjoin-relay/src/main.rs

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use std::sync::Arc;
22
use std::time::Duration;
33

4-
use anyhow::Result;
4+
use anyhow::{Context, Result};
55
use axum::body::Bytes;
66
use axum::extract::Path;
7-
use axum::http::StatusCode;
7+
use axum::http::{Request, StatusCode};
88
use axum::routing::{get, post};
99
use axum::Router;
1010
use payjoin::v2::{MAX_BUFFER_SIZE, RECEIVE};
@@ -21,8 +21,9 @@ use crate::db::DbPool;
2121
async fn main() -> Result<(), Box<dyn std::error::Error>> {
2222
init_logging();
2323
let pool = DbPool::new(std::time::Duration::from_secs(30)).await?;
24-
25-
let app = Router::new()
24+
let ohttp = Arc::new(init_ohttp()?);
25+
let ohttp_config = ohttp_config(&*ohttp)?;
26+
let target_resource = Router::new()
2627
.route(
2728
"/:id",
2829
post({
@@ -46,8 +47,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
4647
}),
4748
);
4849

50+
let ohttp_gateway = Router::new()
51+
.route("/", post(move |body| handle_ohttp(body, target_resource, ohttp)))
52+
.route("/ohttp-keys", get({ move || get_ohttp_config(ohttp_config) }));
53+
4954
println!("Serverless payjoin relay awaiting HTTP connection on port 8080");
50-
axum::Server::bind(&"0.0.0.0:8080".parse()?).serve(app.into_make_service()).await?;
55+
axum::Server::bind(&"0.0.0.0:8080".parse()?).serve(ohttp_gateway.into_make_service()).await?;
56+
//hyper::Server::bind(&"0.0.0.0:8080").serve()
5157
Ok(())
5258
}
5359

@@ -60,6 +66,79 @@ fn init_logging() {
6066
println!("Logging initialized");
6167
}
6268

69+
fn init_ohttp() -> Result<ohttp::Server> {
70+
use ohttp::hpke::{Aead, Kdf, Kem};
71+
use ohttp::{KeyId, SymmetricSuite};
72+
73+
const KEY_ID: KeyId = 1;
74+
const KEM: Kem = Kem::X25519Sha256;
75+
const SYMMETRIC: &[SymmetricSuite] =
76+
&[SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)];
77+
78+
// create or read from file
79+
let server_config = ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap();
80+
let encoded_config = server_config.encode().unwrap();
81+
let b64_config = payjoin::bitcoin::base64::encode_config(
82+
&encoded_config,
83+
payjoin::bitcoin::base64::Config::new(
84+
payjoin::bitcoin::base64::CharacterSet::UrlSafe,
85+
false,
86+
),
87+
);
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(
93+
enc_request: Bytes,
94+
mut target: Router,
95+
ohttp: Arc<ohttp::Server>,
96+
) -> (StatusCode, Vec<u8>) {
97+
use axum::body::Body;
98+
use http::Uri;
99+
use tower_service::Service;
100+
101+
// decapsulate
102+
let (bhttp_req, res_ctx) = ohttp.decapsulate(&enc_request).unwrap();
103+
let mut cursor = std::io::Cursor::new(bhttp_req);
104+
let req = bhttp::Message::read_bhttp(&mut cursor).unwrap();
105+
// let parsed_request: httparse::Request = httparse::Request::new(&mut vec![]).parse(cursor).unwrap();
106+
// // handle request
107+
// Request::new
108+
let uri = Uri::builder()
109+
.scheme(req.control().scheme().unwrap())
110+
.authority(req.control().authority().unwrap())
111+
.path_and_query(req.control().path().unwrap())
112+
.build()
113+
.unwrap();
114+
let body = req.content().to_vec();
115+
let mut request = Request::builder().uri(uri).method(req.control().method().unwrap());
116+
for header in req.header().fields() {
117+
request = request.header(header.name(), header.value())
118+
}
119+
let request = request.body(Body::from(body)).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 =
138+
server.config().encode().with_context(|| "Failed to encode ohttp config")?;
139+
Ok(base64::encode_config(&encoded_config, b64_config))
140+
}
141+
63142
async fn post_fallback(Path(id): Path<String>, body: Bytes, pool: DbPool) -> (StatusCode, String) {
64143
let id = shorten_string(&id);
65144
let body = body.to_vec();
@@ -73,6 +152,8 @@ async fn post_fallback(Path(id): Path<String>, body: Bytes, pool: DbPool) -> (St
73152
}
74153
}
75154

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

payjoin/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ edition = "2018"
1616
send = []
1717
receive = ["rand"]
1818
base64 = ["bitcoin/base64"]
19-
v2 = ["bitcoin/rand-std", "chacha20poly1305", "serde", "serde_json"]
19+
v2 = ["bitcoin/rand-std", "chacha20poly1305", "ohttp", "bhttp", "serde", "serde_json"]
2020

2121
[dependencies]
2222
bitcoin = { version = "0.30.0", features = ["base64"] }
2323
bip21 = "0.3.1"
2424
chacha20poly1305 = { version = "0.10.1", optional = true }
2525
log = { version = "0.4.14"}
26+
#ohttp = { version = "0.4.0", optional = true }
27+
bhttp = { path = "../../ohttp/bhttp", optional = true }
28+
ohttp = { path = "../../ohttp/ohttp", optional = true }
2629
rand = { version = "0.8.4", optional = true }
2730
serde = { version = "1.0", optional = true }
2831
serde_json = { version = "1.0", optional = true }

0 commit comments

Comments
 (0)