Skip to content

Commit

Permalink
Fix unresumable Sender (payjoin#443)
Browse files Browse the repository at this point in the history
The Sender was generating new hpke keys on resume since keygen was being
done in `extract_v2`. This caused a problem where when the Sender was
persisted, stopped, and resumed, the receiver would have already pushed
a response to a different ShortId than the one the Sender would look for
it in the resumed state.

By generating a key on Sender creation and persisting that the Sender
can produce a consistent HpkeContext every single run.

The e2e test did not catch this because it was not propagating errors.
The e2e test has been fixed.
  • Loading branch information
DanGould authored Dec 23, 2024
2 parents b9cb530 + 86a9a8a commit f202098
Show file tree
Hide file tree
Showing 8 changed files with 28 additions and 11 deletions.
2 changes: 1 addition & 1 deletion Cargo-minimal.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,7 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"

[[package]]
name = "payjoin"
version = "0.21.0"
version = "0.22.0"
dependencies = [
"bhttp",
"bitcoin",
Expand Down
2 changes: 1 addition & 1 deletion Cargo-recent.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,7 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"

[[package]]
name = "payjoin"
version = "0.21.0"
version = "0.22.0"
dependencies = [
"bhttp",
"bitcoin",
Expand Down
2 changes: 1 addition & 1 deletion payjoin-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ hyper = { version = "1", features = ["http1", "server"], optional = true }
hyper-rustls = { version = "0.26", optional = true }
hyper-util = { version = "0.1", optional = true }
log = "0.4.7"
payjoin = { version = "0.21.0", features = ["send", "receive", "base64"] }
payjoin = { version = "0.22.0", features = ["send", "receive", "base64"] }
rcgen = { version = "0.11.1", optional = true }
reqwest = { version = "0.12", default-features = false }
rustls = { version = "0.22.4", optional = true }
Expand Down
6 changes: 3 additions & 3 deletions payjoin-cli/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ mod e2e {
.stderr(Stdio::inherit())
.spawn()
.expect("Failed to execute payjoin-cli");
let _ = send_until_request_timeout(cli_send_initiator).await;
send_until_request_timeout(cli_send_initiator).await?;

let cli_receive_resumer = Command::new(payjoin_cli)
.arg("--rpchost")
Expand All @@ -309,7 +309,7 @@ mod e2e {
.stderr(Stdio::inherit())
.spawn()
.expect("Failed to execute payjoin-cli");
let _ = respond_with_payjoin(cli_receive_resumer).await;
respond_with_payjoin(cli_receive_resumer).await?;

let cli_send_resumer = Command::new(payjoin_cli)
.arg("--rpchost")
Expand All @@ -328,7 +328,7 @@ mod e2e {
.stderr(Stdio::inherit())
.spawn()
.expect("Failed to execute payjoin-cli");
let _ = check_payjoin_sent(cli_send_resumer).await;
check_payjoin_sent(cli_send_resumer).await?;
Ok(())
}

Expand Down
5 changes: 5 additions & 0 deletions payjoin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Payjoin Changelog

## 0.22.0

- Propagate Uri Fragment parameter errors to the caller
- Have `Sender` to persist reply key so resumption listens where a previous sender left off

## 0.21.0

- Upgrade rustls v0.22.4
Expand Down
2 changes: 1 addition & 1 deletion payjoin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "payjoin"
version = "0.21.0"
version = "0.22.0"
authors = ["Dan Gould <d@ngould.dev>"]
description = "Payjoin Library for the BIP78 Pay to Endpoint protocol."
repository = "https://github.com/payjoin/rust-payjoin"
Expand Down
5 changes: 5 additions & 0 deletions payjoin/src/hpke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ impl From<HpkeKeyPair> for (HpkeSecretKey, HpkePublicKey) {
}

impl HpkeKeyPair {
pub fn from_secret_key(secret_key: &HpkeSecretKey) -> Self {
let public_key = <SecpK256HkdfSha256 as hpke::Kem>::sk_to_pk(&secret_key.0);
Self(secret_key.clone(), HpkePublicKey(public_key))
}

pub fn gen_keypair() -> Self {
let (sk, pk) = <SecpK256HkdfSha256 as hpke::Kem>::gen_keypair(&mut OsRng);
Self(HpkeSecretKey(sk), HpkePublicKey(pk))
Expand Down
15 changes: 11 additions & 4 deletions payjoin/src/send/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ use serde::{Deserialize, Serialize};
use url::Url;

#[cfg(feature = "v2")]
use crate::hpke::{decrypt_message_b, encrypt_message_a, HpkeKeyPair, HpkePublicKey};
use crate::hpke::{
decrypt_message_b, encrypt_message_a, HpkeKeyPair, HpkePublicKey, HpkeSecretKey,
};
#[cfg(feature = "v2")]
use crate::ohttp::{ohttp_decapsulate, ohttp_encapsulate};
use crate::psbt::PsbtExt;
Expand Down Expand Up @@ -228,6 +230,8 @@ impl<'a> SenderBuilder<'a> {
fee_contribution,
payee,
min_fee_rate: self.min_fee_rate,
#[cfg(feature = "v2")]
reply_key: HpkeKeyPair::gen_keypair().0,
})
}
}
Expand All @@ -246,6 +250,8 @@ pub struct Sender {
min_fee_rate: FeeRate,
/// Script of the person being paid
payee: ScriptBuf,
#[cfg(feature = "v2")]
reply_key: HpkeSecretKey,
}

impl Sender {
Expand Down Expand Up @@ -298,7 +304,7 @@ impl Sender {
self.fee_contribution,
self.min_fee_rate,
)?;
let hpke_ctx = HpkeContext::new(rs);
let hpke_ctx = HpkeContext::new(rs, &self.reply_key);
let body = encrypt_message_a(
body,
&hpke_ctx.reply_pair.public_key().clone(),
Expand Down Expand Up @@ -475,8 +481,8 @@ struct HpkeContext {

#[cfg(feature = "v2")]
impl HpkeContext {
pub fn new(receiver: HpkePublicKey) -> Self {
Self { receiver, reply_pair: HpkeKeyPair::gen_keypair() }
pub fn new(receiver: HpkePublicKey, reply_key: &HpkeSecretKey) -> Self {
Self { receiver, reply_pair: HpkeKeyPair::from_secret_key(reply_key) }
}
}

Expand Down Expand Up @@ -975,6 +981,7 @@ mod test {
fee_contribution: None,
min_fee_rate: FeeRate::ZERO,
payee: ScriptBuf::from(vec![0x00]),
reply_key: HpkeKeyPair::gen_keypair().0,
};
let serialized = serde_json::to_string(&req_ctx).unwrap();
let deserialized = serde_json::from_str(&serialized).unwrap();
Expand Down

0 comments on commit f202098

Please sign in to comment.