Skip to content
Merged
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
344 changes: 179 additions & 165 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions payjoin-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ impl App {
let req_ctx = if is_retry {
log::debug!("Resuming session");
// Get a reference to RequestContext
session.req_ctx.as_ref().expect("RequestContext is missing")
session.req_ctx.as_mut().expect("RequestContext is missing")
} else {
let req_ctx = self.create_pj_request(bip21, fee_rate)?;
session.write(req_ctx)?;
log::debug!("Writing req_ctx");
session.req_ctx.as_ref().expect("RequestContext is missing")
session.req_ctx.as_mut().expect("RequestContext is missing")
};
log::debug!("Awaiting response");
let res = self.long_poll_post(req_ctx).await?;
Expand Down Expand Up @@ -163,7 +163,7 @@ impl App {
log::debug!("Awaiting proposal");
let res = self.long_poll_fallback(&mut enrolled).await?;
log::debug!("Received request");
let payjoin_proposal = self
let mut payjoin_proposal = self
.process_v2_proposal(res)
.map_err(|e| anyhow!("Failed to process proposal {}", e))?;
log::debug!("Posting payjoin back");
Expand Down Expand Up @@ -194,7 +194,7 @@ impl App {
}

#[cfg(feature = "v2")]
async fn long_poll_post(&self, req_ctx: &payjoin::send::RequestContext) -> Result<Psbt> {
async fn long_poll_post(&self, req_ctx: &mut payjoin::send::RequestContext) -> Result<Psbt> {
loop {
let (req, ctx) = req_ctx.extract_v2(&self.config.ohttp_proxy)?;
println!("Sending fallback request to {}", &req.url);
Expand Down
34 changes: 15 additions & 19 deletions payjoin-relay/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::sync::Arc;

use anyhow::Result;
use bitcoin::{self, base64};
use hyper::header::HeaderValue;
use hyper::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE};
use hyper::server::conn::AddrIncoming;
use hyper::server::Builder;
use hyper::service::{make_service_fn, service_fn};
Expand Down Expand Up @@ -111,7 +111,7 @@ fn init_ohttp() -> Result<ohttp::Server> {
encoded_config,
base64::Config::new(base64::CharacterSet::UrlSafe, false),
);
info!("ohttp server config base64 UrlSafe: {:?}", b64_config);
info!("ohttp-keys server config base64 UrlSafe: {:?}", b64_config);
Ok(ohttp::Server::new(server_config)?)
}

Expand All @@ -128,17 +128,14 @@ async fn handle_ohttp_gateway(
debug!("handle_ohttp_gateway: {:?}", &path_segments);
let mut response = match (parts.method, path_segments.as_slice()) {
(Method::POST, ["", ""]) => handle_ohttp(body, pool, ohttp).await,
(Method::GET, ["", "ohttp-config"]) =>
Ok(get_ohttp_config(ohttp_config(&ohttp).await?).await),
(Method::GET, ["", "ohttp-keys"]) => get_ohttp_keys(&ohttp).await,
(Method::POST, ["", id]) => post_fallback_v1(id, query, body, pool).await,
_ => Ok(not_found()),
}
.unwrap_or_else(|e| e.to_response());

// Allow CORS for third-party access
response
.headers_mut()
.insert("Access-Control-Allow-Origin", hyper::header::HeaderValue::from_static("*"));
response.headers_mut().insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));

Ok(response)
}
Expand Down Expand Up @@ -227,7 +224,7 @@ impl HandlerError {
error!("Bad request: Key configuration rejected: {}", e);
*res.status_mut() = StatusCode::BAD_REQUEST;
res.headers_mut()
.insert("Content-Type", HeaderValue::from_static("application/problem+json"));
.insert(CONTENT_TYPE, HeaderValue::from_static("application/problem+json"));
*res.body_mut() = Body::from(OHTTP_KEY_REJECTION_RES_JSON);
}
HandlerError::BadRequest(e) => {
Expand Down Expand Up @@ -355,18 +352,17 @@ fn not_found() -> Response<Body> {
res
}

async fn get_ohttp_config(config: String) -> Response<Body> {
trace!("GET ohttp config: {:?}", config);
async fn get_ohttp_keys(ohttp: &Arc<Mutex<ohttp::Server>>) -> Result<Response<Body>, HandlerError> {
let mut res = Response::default();
*res.body_mut() = Body::from(config);
res
res.headers_mut().insert(CONTENT_TYPE, HeaderValue::from_static("application/ohttp-keys"));
let ohttp_keys = ohttp
.lock()
.await
.config()
.encode()
.map_err(|e| HandlerError::InternalServerError(e.into()))?;
*res.body_mut() = Body::from(ohttp_keys);
Ok(res)
}

fn shorten_string(input: &str) -> String { input.chars().take(8).collect() }

async fn ohttp_config(server: &Arc<Mutex<ohttp::Server>>) -> Result<String> {
let b64_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
let server = server.lock().await;
let encoded_config = server.config().encode()?;
Ok(base64::encode_config(encoded_config, b64_config))
}
2 changes: 2 additions & 0 deletions payjoin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub mod send;

#[cfg(feature = "v2")]
pub(crate) mod v2;
#[cfg(feature = "v2")]
pub use ohttp::KeyConfig as OhttpKeys;

#[cfg(any(feature = "send", feature = "receive"))]
pub(crate) mod input_type;
Expand Down
85 changes: 57 additions & 28 deletions payjoin/src/receive/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct Request {
#[derive(Debug, Clone)]
pub struct V2Context {
relay_url: url::Url,
ohttp_config: Vec<u8>,
ohttp_keys: ohttp::KeyConfig,
ohttp_proxy: url::Url,
s: bitcoin::secp256k1::KeyPair,
e: Option<bitcoin::secp256k1::PublicKey>,
Expand All @@ -36,7 +36,7 @@ pub struct V2Context {
#[derive(Debug, Clone)]
pub struct Enroller {
relay_url: url::Url,
ohttp_config: Vec<u8>,
ohttp_keys: ohttp::KeyConfig,
ohttp_proxy: url::Url,
s: bitcoin::secp256k1::KeyPair,
}
Expand All @@ -49,14 +49,15 @@ impl Enroller {
ohttp_proxy_url: &str,
) -> Self {
let ohttp_config = base64::decode_config(ohttp_config_base64, base64::URL_SAFE).unwrap();
let ohttp_keys = ohttp::KeyConfig::decode(&ohttp_config).unwrap();
let ohttp_proxy = url::Url::parse(ohttp_proxy_url).unwrap();
let relay_url = url::Url::parse(relay_url).unwrap();
let secp = bitcoin::secp256k1::Secp256k1::new();
let (sk, _) = secp.generate_keypair(&mut rand::rngs::OsRng);
Enroller {
ohttp_config,
ohttp_proxy,
relay_url,
ohttp_keys,
ohttp_proxy,
s: bitcoin::secp256k1::KeyPair::from_secret_key(&secp, &sk),
}
}
Expand All @@ -71,11 +72,12 @@ impl Enroller {

pub fn extract_req(&mut self) -> Result<(Request, ohttp::ClientResponse), crate::v2::Error> {
let url = self.ohttp_proxy.clone();
let subdirectory = self.subdirectory();
let (body, ctx) = crate::v2::ohttp_encapsulate(
&self.ohttp_config,
&mut self.ohttp_keys,
"POST",
self.relay_url.as_str(),
Some(self.subdirectory().as_bytes()),
Some(subdirectory.as_bytes()),
)?;
let req = Request { url, body };
Ok((req, ctx))
Expand All @@ -93,7 +95,7 @@ impl Enroller {

let ctx = Enrolled {
relay_url: self.relay_url,
ohttp_config: self.ohttp_config,
ohttp_keys: self.ohttp_keys,
ohttp_proxy: self.ohttp_proxy,
s: self.s,
};
Expand All @@ -107,22 +109,37 @@ fn subdirectory(pubkey: &bitcoin::secp256k1::PublicKey) -> String {
base64::encode_config(pubkey, b64_config)
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone)]
pub struct Enrolled {
relay_url: url::Url,
ohttp_config: Vec<u8>,
ohttp_keys: ohttp::KeyConfig,
ohttp_proxy: url::Url,
s: bitcoin::secp256k1::KeyPair,
}

impl PartialEq for Enrolled {
fn eq(&self, other: &Self) -> bool {
self.relay_url == other.relay_url
&& self.ohttp_keys.encode().unwrap() == other.ohttp_keys.encode().unwrap()
&& self.ohttp_proxy == other.ohttp_proxy
&& self.s == other.s
}
}

impl Eq for Enrolled {}

impl Serialize for Enrolled {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::Error;

let mut state = serializer.serialize_struct("Enrolled", 4)?;
state.serialize_field("relay_url", &self.relay_url.to_string())?;
state.serialize_field("ohttp_config", &self.ohttp_config)?;
let ohttp_keys =
self.ohttp_keys.encode().map_err(|_| S::Error::custom("ohttp_key encoding failed"))?;
state.serialize_field("ohttp_keys", &ohttp_keys)?;
state.serialize_field("ohttp_proxy", &self.ohttp_proxy.to_string())?;
state.serialize_field("s", &self.s.secret_key().secret_bytes())?;

Expand Down Expand Up @@ -158,7 +175,7 @@ impl<'de> Deserialize<'de> for Enrolled {
type Value = Field;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("`relay_url`, `ohttp_config`, `ohttp_proxy`, or `s`")
formatter.write_str("`relay_url`, `ohttp_keys`, `ohttp_proxy`, or `s`")
}

fn visit_str<E>(self, value: &str) -> Result<Field, E>
Expand All @@ -167,7 +184,7 @@ impl<'de> Deserialize<'de> for Enrolled {
{
match value {
"relay_url" => Ok(Field::RelayUrl),
"ohttp_config" => Ok(Field::OhttpConfig),
"ohttp_keys" => Ok(Field::OhttpConfig),
"ohttp_proxy" => Ok(Field::OhttpProxy),
"s" => Ok(Field::S),
_ => Err(de::Error::unknown_field(value, FIELDS)),
Expand All @@ -193,7 +210,7 @@ impl<'de> Deserialize<'de> for Enrolled {
V: MapAccess<'de>,
{
let mut relay_url = None;
let mut ohttp_config = None;
let mut ohttp_keys = None;
let mut ohttp_proxy = None;
let mut s = None;
while let Some(key) = map.next_key()? {
Expand All @@ -206,10 +223,14 @@ impl<'de> Deserialize<'de> for Enrolled {
relay_url = Some(url::Url::parse(&url_str).map_err(de::Error::custom)?);
}
Field::OhttpConfig => {
if ohttp_config.is_some() {
return Err(de::Error::duplicate_field("ohttp_config"));
if ohttp_keys.is_some() {
return Err(de::Error::duplicate_field("ohttp_keys"));
}
ohttp_config = Some(map.next_value()?);
let ohttp_keys_bytes: Vec<u8> = map.next_value()?;
ohttp_keys = Some(
ohttp::KeyConfig::decode(&ohttp_keys_bytes)
.map_err(|_| de::Error::custom("ohttp_key decoding failed"))?,
);
}
Field::OhttpProxy => {
if ohttp_proxy.is_some() {
Expand All @@ -233,12 +254,12 @@ impl<'de> Deserialize<'de> for Enrolled {
}
}
let relay_url = relay_url.ok_or_else(|| de::Error::missing_field("relay_url"))?;
let ohttp_config =
ohttp_config.ok_or_else(|| de::Error::missing_field("ohttp_config"))?;
let ohttp_keys =
ohttp_keys.ok_or_else(|| de::Error::missing_field("ohttp_keys"))?;
let ohttp_proxy =
ohttp_proxy.ok_or_else(|| de::Error::missing_field("ohttp_proxy"))?;
let s = s.ok_or_else(|| de::Error::missing_field("s"))?;
Ok(Enrolled { relay_url, ohttp_config, ohttp_proxy, s })
Ok(Enrolled { relay_url, ohttp_keys, ohttp_proxy, s })
}
}

Expand All @@ -248,7 +269,7 @@ impl<'de> Deserialize<'de> for Enrolled {
}

impl Enrolled {
pub fn extract_req(&self) -> Result<(Request, ohttp::ClientResponse), Error> {
pub fn extract_req(&mut self) -> Result<(Request, ohttp::ClientResponse), Error> {
let (body, ohttp_ctx) = self.fallback_req_body()?;
let url = self.ohttp_proxy.clone();
let req = Request { url, body };
Expand All @@ -275,7 +296,7 @@ impl Enrolled {
Ok(proposal) => {
let context = V2Context {
relay_url: self.relay_url.clone(),
ohttp_config: self.ohttp_config.clone(),
ohttp_keys: self.ohttp_keys.clone(),
ohttp_proxy: self.ohttp_proxy.clone(),
s: self.s,
e: None,
Expand All @@ -288,7 +309,7 @@ impl Enrolled {
log::debug!("Some e: {}", e);
let context = V2Context {
relay_url: self.relay_url.clone(),
ohttp_config: self.ohttp_config.clone(),
ohttp_keys: self.ohttp_keys.clone(),
ohttp_proxy: self.ohttp_proxy.clone(),
s: self.s,
e: Some(e),
Expand All @@ -300,10 +321,11 @@ impl Enrolled {
}
}

fn fallback_req_body(&self) -> Result<(Vec<u8>, ohttp::ClientResponse), crate::v2::Error> {
fn fallback_req_body(&mut self) -> Result<(Vec<u8>, ohttp::ClientResponse), crate::v2::Error> {
let fallback_target = format!("{}{}", &self.relay_url, self.fallback_target());
log::trace!("Fallback request target: {}", fallback_target.as_str());
crate::v2::ohttp_encapsulate(&self.ohttp_config, "GET", &self.fallback_target(), None)
let fallback_target = self.fallback_target();
crate::v2::ohttp_encapsulate(&mut self.ohttp_keys, "GET", &fallback_target, None)
}

pub fn pubkey(&self) -> [u8; 33] { self.s.public_key().serialize() }
Expand Down Expand Up @@ -544,7 +566,7 @@ impl PayjoinProposal {
pub fn extract_v1_req(&self) -> String { base64::encode(self.inner.payjoin_psbt.serialize()) }

#[cfg(feature = "v2")]
pub fn extract_v2_req(&self) -> Result<(Request, ohttp::ClientResponse), Error> {
pub fn extract_v2_req(&mut self) -> Result<(Request, ohttp::ClientResponse), Error> {
let body = match self.context.e {
Some(e) => {
let mut payjoin_bytes = self.inner.payjoin_psbt.serialize();
Expand All @@ -560,7 +582,7 @@ impl PayjoinProposal {
);
log::debug!("Payjoin post target: {}", post_payjoin_target.as_str());
let (body, ctx) = crate::v2::ohttp_encapsulate(
&self.context.ohttp_config,
&mut self.context.ohttp_keys,
"POST",
&post_payjoin_target,
Some(&body),
Expand Down Expand Up @@ -590,17 +612,24 @@ mod test {
#[test]
#[cfg(feature = "v2")]
fn enrolled_ser_de_roundtrip() {
use ohttp::hpke::{Aead, Kdf, Kem};
use ohttp::{KeyId, SymmetricSuite};
const KEY_ID: KeyId = 1;
const KEM: Kem = Kem::X25519Sha256;
const SYMMETRIC: &[SymmetricSuite] =
&[ohttp::SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)];

let enrolled = Enrolled {
relay_url: url::Url::parse("https://relay.com").unwrap(),
ohttp_config: vec![1, 2, 3],
ohttp_keys: ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(),
ohttp_proxy: url::Url::parse("https://proxy.com").unwrap(),
s: bitcoin::secp256k1::KeyPair::from_secret_key(
&bitcoin::secp256k1::Secp256k1::new(),
&bitcoin::secp256k1::SecretKey::from_slice(&[1; 32]).unwrap(),
),
};
let serialized = serde_json::to_string(&enrolled).unwrap();
let deserialized = serde_json::from_str(&serialized).unwrap();
let deserialized: Enrolled = serde_json::from_str(&serialized).unwrap();
assert!(enrolled == deserialized);
}
}
9 changes: 2 additions & 7 deletions payjoin/src/send/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ impl RequestContext {
/// The `ohttp_proxy` merely passes the encrypted payload to the ohttp gateway of the receiver
#[cfg(feature = "v2")]
pub fn extract_v2(
&self,
&mut self,
ohttp_proxy_url: &str,
) -> Result<(Request, ContextV2), CreateRequestError> {
let rs_base64 = crate::v2::subdir(self.endpoint.as_str()).to_string();
Expand All @@ -454,12 +454,7 @@ impl RequestContext {
let body = crate::v2::encrypt_message_a(body, self.e, rs)
.map_err(InternalCreateRequestError::V2)?;
let (body, ohttp_res) = crate::v2::ohttp_encapsulate(
&self
.ohttp_config
.as_ref()
.ok_or(InternalCreateRequestError::MissingOhttpConfig)?
.encode()
.map_err(|e| InternalCreateRequestError::V2(e.into()))?,
self.ohttp_config.as_mut().ok_or(InternalCreateRequestError::MissingOhttpConfig)?,
"POST",
url.as_str(),
Some(&body),
Expand Down
Loading