Skip to content

Commit

Permalink
Use bitcoin-hpke for client aead
Browse files Browse the repository at this point in the history
  • Loading branch information
DanGould committed Sep 8, 2024
1 parent 3201ed6 commit 546805c
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 144 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion payjoin-cli/src/db/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use super::*;
impl Database {
pub(crate) fn insert_recv_session(&self, session: ActiveSession) -> Result<()> {
let recv_tree = self.0.open_tree("recv_sessions")?;
let key = &session.public_key().serialize();
let key = &session.id();
let value = serde_json::to_string(&session).map_err(Error::Serialize)?;
recv_tree.insert(key.as_slice(), IVec::from(value.as_str()))?;
recv_tree.flush()?;
Expand Down
4 changes: 2 additions & 2 deletions payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ exclude = ["tests"]
send = []
receive = ["bitcoin/rand"]
base64 = ["bitcoin/base64"]
v2 = ["bitcoin/rand", "bitcoin/serde", "chacha20poly1305", "dep:http", "bhttp", "ohttp", "serde", "url/serde"]
v2 = ["bitcoin/rand", "bitcoin/serde", "hpke", "dep:http", "bhttp", "ohttp", "serde", "url/serde"]
io = ["reqwest/rustls-tls"]
danger-local-https = ["io", "reqwest/rustls-tls", "rustls"]

[dependencies]
bitcoin = { version = "0.32.2", features = ["base64"] }
bip21 = "0.5.0"
chacha20poly1305 = { version = "0.10.1", optional = true }
hpke = { package = "bitcoin-hpke", version = "0.13.0", optional = true }
log = { version = "0.4.14"}
http = { version = "1", optional = true }
bhttp = { version = "=0.5.1", optional = true }
Expand Down
41 changes: 19 additions & 22 deletions payjoin/src/receive/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use bitcoin::address::NetworkUnchecked;
use bitcoin::base64::prelude::BASE64_URL_SAFE_NO_PAD;
use bitcoin::base64::Engine;
use bitcoin::psbt::Psbt;
use bitcoin::secp256k1::{rand, PublicKey};
use bitcoin::{Address, Amount, FeeRate, OutPoint, Script, TxOut};
use hpke::Serializable;
use serde::de::{self, Deserializer, MapAccess, Visitor};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize, Serializer};
Expand All @@ -18,7 +18,7 @@ use super::v2::error::{InternalSessionError, SessionError};
use super::{Error, InternalRequestError, RequestError, SelectionError};
use crate::psbt::PsbtExt;
use crate::receive::optional_parameters::Params;
use crate::v2::OhttpEncapsulationError;
use crate::v2::{HpkePublicKey, HpkeSecretKey, OhttpEncapsulationError};
use crate::{OhttpKeys, PjUriBuilder, Request};

pub(crate) mod error;
Expand All @@ -33,8 +33,8 @@ struct SessionContext {
ohttp_keys: OhttpKeys,
expiry: SystemTime,
ohttp_relay: url::Url,
s: bitcoin::secp256k1::Keypair,
e: Option<bitcoin::secp256k1::PublicKey>,
s: (HpkeSecretKey, HpkePublicKey),
e: Option<HpkePublicKey>,
}

/// Initializes a new payjoin session, including necessary context
Expand Down Expand Up @@ -67,8 +67,6 @@ impl SessionInitializer {
ohttp_relay: Url,
expire_after: Option<Duration>,
) -> Self {
let secp = bitcoin::secp256k1::Secp256k1::new();
let (sk, _) = secp.generate_keypair(&mut rand::rngs::OsRng);
Self {
context: SessionContext {
address,
Expand All @@ -78,15 +76,15 @@ impl SessionInitializer {
ohttp_relay,
expiry: SystemTime::now()
+ expire_after.unwrap_or(TWENTY_FOUR_HOURS_DEFAULT_EXPIRY),
s: bitcoin::secp256k1::Keypair::from_secret_key(&secp, &sk),
s: crate::v2::gen_keypair(),
e: None,
},
}
}

pub fn extract_req(&mut self) -> Result<(Request, ohttp::ClientResponse), Error> {
let url = self.context.ohttp_relay.clone();
let subdirectory = subdir_path_from_pubkey(&self.context.s.public_key());
let subdirectory = subdir_path_from_pubkey(&self.context.s.1); // TODO ensure it's the compressed key
let (body, ctx) = crate::v2::ohttp_encapsulate(
&mut self.context.ohttp_keys,
"POST",
Expand Down Expand Up @@ -122,8 +120,10 @@ impl SessionInitializer {
}
}

fn subdir_path_from_pubkey(pubkey: &bitcoin::secp256k1::PublicKey) -> String {
BASE64_URL_SAFE_NO_PAD.encode(pubkey.serialize())
fn subdir_path_from_pubkey(
pubkey: &<hpke::kem::SecpK256HkdfSha256 as hpke::Kem>::PublicKey,
) -> String {
BASE64_URL_SAFE_NO_PAD.encode(pubkey.to_bytes())
}

/// An active payjoin V2 session, allowing for polled requests to the
Expand Down Expand Up @@ -186,7 +186,7 @@ impl ActiveSession {

fn extract_proposal_from_v2(&mut self, response: Vec<u8>) -> Result<UncheckedProposal, Error> {
let (payload_bytes, e) =
crate::v2::decrypt_message_a(&response, self.context.s.secret_key())?;
crate::v2::decrypt_message_a_hpke(&response, self.context.s.0.clone())?;
self.context.e = Some(e);
let payload = String::from_utf8(payload_bytes).map_err(InternalRequestError::Utf8)?;
Ok(self.unchecked_from_payload(payload)?)
Expand Down Expand Up @@ -234,7 +234,7 @@ impl ActiveSession {
// The contents of the `&pj=` query parameter including the base64url-encoded public key receiver subdirectory.
// This identifies a session at the payjoin directory server.
pub fn pj_url(&self) -> Url {
let pubkey = &self.context.s.public_key().serialize();
let pubkey = &self.context.s.1.to_bytes();
let pubkey_base64 = BASE64_URL_SAFE_NO_PAD.encode(pubkey);
let mut url = self.context.directory.clone();
{
Expand All @@ -246,7 +246,7 @@ impl ActiveSession {
}

/// The per-session public key to use as an identifier
pub fn public_key(&self) -> PublicKey { self.context.s.public_key() }
pub fn id(&self) -> Vec<u8> { self.context.s.1.to_bytes().to_vec() }
}

/// The sender's original PSBT and optional parameters
Expand Down Expand Up @@ -465,15 +465,15 @@ impl PayjoinProposal {

#[cfg(feature = "v2")]
pub fn extract_v2_req(&mut self) -> Result<(Request, ohttp::ClientResponse), Error> {
let body = match self.context.e {
let body = match &self.context.e {
Some(e) => {
let mut payjoin_bytes = self.inner.payjoin_psbt.serialize();
log::debug!("THERE IS AN e: {}", e);
crate::v2::encrypt_message_b(&mut payjoin_bytes, e)
let payjoin_bytes = self.inner.payjoin_psbt.serialize();
log::debug!("THERE IS AN e: {:?}", e);
crate::v2::encrypt_message_b_hpke(payjoin_bytes, self.context.s.clone(), e)
}
None => Ok(self.extract_v1_req().as_bytes().to_vec()),
}?;
let subdir_path = subdir_path_from_pubkey(&self.context.s.public_key());
let subdir_path = subdir_path_from_pubkey(&self.context.s.1);
let post_payjoin_target =
self.context.directory.join(&subdir_path).map_err(|e| Error::Server(e.into()))?;
log::debug!("Payjoin post target: {}", post_payjoin_target.as_str());
Expand Down Expand Up @@ -679,10 +679,7 @@ mod test {
),
ohttp_relay: url::Url::parse("https://relay.com").unwrap(),
expiry: SystemTime::now() + Duration::from_secs(60),
s: bitcoin::secp256k1::Keypair::from_secret_key(
&bitcoin::secp256k1::Secp256k1::new(),
&bitcoin::secp256k1::SecretKey::from_slice(&[1; 32]).unwrap(),
),
s: crate::v2::gen_keypair(),
e: None,
},
};
Expand Down
8 changes: 4 additions & 4 deletions payjoin/src/send/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pub(crate) enum InternalValidationError {
FeeContributionPaysOutputSizeIncrease,
FeeRateBelowMinimum,
#[cfg(feature = "v2")]
HpkeError(crate::v2::HpkeError),
Hpke(crate::v2::HpkeError),
#[cfg(feature = "v2")]
OhttpEncapsulation(crate::v2::OhttpEncapsulationError),
#[cfg(feature = "v2")]
Expand Down Expand Up @@ -108,7 +108,7 @@ impl fmt::Display for ValidationError {
FeeContributionPaysOutputSizeIncrease => write!(f, "fee contribution pays for additional outputs"),
FeeRateBelowMinimum => write!(f, "the fee rate of proposed transaction is below minimum"),
#[cfg(feature = "v2")]
HpkeError(e) => write!(f, "v2 error: {}", e),
Hpke(e) => write!(f, "v2 error: {}", e),
#[cfg(feature = "v2")]
OhttpEncapsulation(e) => write!(f, "Ohttp encapsulation error: {}", e),
#[cfg(feature = "v2")]
Expand Down Expand Up @@ -153,7 +153,7 @@ impl std::error::Error for ValidationError {
FeeContributionPaysOutputSizeIncrease => None,
FeeRateBelowMinimum => None,
#[cfg(feature = "v2")]
HpkeError(error) => Some(error),
Hpke(error) => Some(error),
#[cfg(feature = "v2")]
OhttpEncapsulation(error) => Some(error),
#[cfg(feature = "v2")]
Expand Down Expand Up @@ -282,7 +282,7 @@ impl From<ParseSubdirectoryError> for CreateRequestError {
pub(crate) enum ParseSubdirectoryError {
MissingSubdirectory,
SubdirectoryNotBase64(bitcoin::base64::DecodeError),
SubdirectoryInvalidPubkey(bitcoin::secp256k1::Error),
SubdirectoryInvalidPubkey(hpke::HpkeError),
}

#[cfg(feature = "v2")]
Expand Down
53 changes: 21 additions & 32 deletions payjoin/src/send/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@
use std::str::FromStr;

use bitcoin::psbt::Psbt;
#[cfg(feature = "v2")]
use bitcoin::secp256k1::rand;
#[cfg(feature = "v2")]
use bitcoin::secp256k1::PublicKey;
use bitcoin::{FeeRate, Script, ScriptBuf, Sequence, TxOut, Weight};
pub use error::{CreateRequestError, ResponseError, ValidationError};
pub(crate) use error::{InternalCreateRequestError, InternalValidationError};
Expand All @@ -45,6 +41,8 @@ use url::Url;
use crate::input_type::InputType;
use crate::psbt::PsbtExt;
use crate::request::Request;
#[cfg(feature = "v2")]
use crate::v2::{HpkePublicKey, HpkeSecretKey};
use crate::weight::{varint_size, ComputeWeight};
use crate::PjUri;

Expand Down Expand Up @@ -238,13 +236,6 @@ impl<'a> RequestBuilder<'a> {
let input_type = InputType::from_spent_input(txout, zeroth_input.psbtin)
.map_err(InternalCreateRequestError::InputType)?;

#[cfg(feature = "v2")]
let e = {
let secp = bitcoin::secp256k1::Secp256k1::new();
let (e_sec, _) = secp.generate_keypair(&mut rand::rngs::OsRng);
e_sec
};

Ok(RequestContext {
psbt,
endpoint,
Expand All @@ -255,7 +246,7 @@ impl<'a> RequestBuilder<'a> {
sequence,
min_fee_rate: self.min_fee_rate,
#[cfg(feature = "v2")]
e,
e: crate::v2::gen_keypair().0,
})
}
}
Expand All @@ -271,7 +262,7 @@ pub struct RequestContext {
sequence: Sequence,
payee: ScriptBuf,
#[cfg(feature = "v2")]
e: bitcoin::secp256k1::SecretKey,
e: crate::v2::HpkeSecretKey,
}

#[cfg(feature = "v2")]
Expand Down Expand Up @@ -340,7 +331,7 @@ impl RequestContext {
fn extract_v2_strict(
&mut self,
ohttp_relay: Url,
rs: PublicKey,
rs: HpkePublicKey,
) -> Result<(Request, ContextV2), CreateRequestError> {
use crate::uri::UrlExt;
let url = self.endpoint.clone();
Expand All @@ -350,7 +341,7 @@ impl RequestContext {
self.fee_contribution,
self.min_fee_rate,
)?;
let body = crate::v2::encrypt_message_a(body, self.e, rs)
let body = crate::v2::encrypt_message_a_hpke(body, &self.e.clone(), &rs)
.map_err(InternalCreateRequestError::Hpke)?;
let mut ohttp =
self.endpoint.ohttp().ok_or(InternalCreateRequestError::MissingOhttpConfig)?;
Expand All @@ -370,14 +361,14 @@ impl RequestContext {
sequence: self.sequence,
min_fee_rate: self.min_fee_rate,
},
e: Some(self.e),
e: Some(self.e.clone()),
ohttp_res: Some(ohttp_res),
},
))
}

#[cfg(feature = "v2")]
fn extract_rs_pubkey(&self) -> Result<PublicKey, error::ParseSubdirectoryError> {
fn extract_rs_pubkey(&self) -> Result<HpkePublicKey, error::ParseSubdirectoryError> {
use bitcoin::base64::prelude::BASE64_URL_SAFE_NO_PAD;
use bitcoin::base64::Engine;
use error::ParseSubdirectoryError;
Expand All @@ -392,7 +383,7 @@ impl RequestContext {
.decode(subdirectory)
.map_err(ParseSubdirectoryError::SubdirectoryNotBase64)?;

bitcoin::secp256k1::PublicKey::from_slice(&pubkey_bytes)
HpkePublicKey::from_bytes(&pubkey_bytes)
.map_err(ParseSubdirectoryError::SubdirectoryInvalidPubkey)
}

Expand All @@ -417,7 +408,7 @@ impl Serialize for RequestContext {
state.serialize_field("input_type", &self.input_type)?;
state.serialize_field("sequence", &self.sequence)?;
state.serialize_field("payee", &self.payee)?;
state.serialize_field("e", &self.e.secret_bytes())?;
state.serialize_field("e", &self.e)?;
state.end()
}
}
Expand Down Expand Up @@ -486,13 +477,7 @@ impl<'de> Deserialize<'de> for RequestContext {
"input_type" => input_type = Some(map.next_value()?),
"sequence" => sequence = Some(map.next_value()?),
"payee" => payee = Some(map.next_value()?),
"e" => {
let secret_bytes: Vec<u8> = map.next_value()?;
e = Some(
bitcoin::secp256k1::SecretKey::from_slice(&secret_bytes)
.map_err(de::Error::custom)?,
);
}
"e" => e = Some(map.next_value()?),
_ => return Err(de::Error::unknown_field(key.as_str(), FIELDS)),
}
}
Expand Down Expand Up @@ -535,7 +520,7 @@ pub struct ContextV1 {
#[cfg(feature = "v2")]
pub struct ContextV2 {
context_v1: ContextV1,
e: Option<bitcoin::secp256k1::SecretKey>,
e: Option<HpkeSecretKey>,
ohttp_res: Option<ohttp::ClientResponse>,
}

Expand Down Expand Up @@ -576,13 +561,13 @@ impl ContextV2 {
response.read_to_end(&mut res_buf).map_err(InternalValidationError::Io)?;
let response = crate::v2::ohttp_decapsulate(ohttp_res, &res_buf)
.map_err(InternalValidationError::OhttpEncapsulation)?;
let mut body = match response.status() {
let body = match response.status() {
http::StatusCode::OK => response.body().to_vec(),
http::StatusCode::ACCEPTED => return Ok(None),
_ => return Err(InternalValidationError::UnexpectedStatusCode)?,
};
let psbt = crate::v2::decrypt_message_b(&mut body, e)
.map_err(InternalValidationError::HpkeError)?;
let psbt = crate::v2::decrypt_message_b_hpke(&body, e)
.map_err(InternalValidationError::Hpke)?;

let proposal = Psbt::deserialize(&psbt).map_err(InternalValidationError::Psbt)?;
let processed_proposal = self.context_v1.process_proposal(proposal)?;
Expand Down Expand Up @@ -1109,8 +1094,9 @@ mod test {
#[test]
#[cfg(feature = "v2")]
fn req_ctx_ser_de_roundtrip() {
use super::*;
use hpke::Deserializable;

use super::*;
let req_ctx = RequestContext {
psbt: Psbt::from_str(ORIGINAL_PSBT).unwrap(),
endpoint: Url::parse("http://localhost:1234").unwrap(),
Expand All @@ -1123,7 +1109,10 @@ mod test {
},
sequence: Sequence::MAX,
payee: ScriptBuf::from(vec![0x00]),
e: bitcoin::secp256k1::SecretKey::from_slice(&[0x01; 32]).unwrap(),
e: HpkeSecretKey(
<hpke::kem::SecpK256HkdfSha256 as hpke::Kem>::PrivateKey::from_bytes(&[0x01; 32])
.unwrap(),
),
};
let serialized = serde_json::to_string(&req_ctx).unwrap();
let deserialized = serde_json::from_str(&serialized).unwrap();
Expand Down
Loading

0 comments on commit 546805c

Please sign in to comment.