Skip to content
Draft
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
10 changes: 5 additions & 5 deletions payjoin-ffi/src/ohttp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub mod error {
#[derive(Debug, thiserror::Error, uniffi::Object)]
#[uniffi::export(Debug, Display)]
#[error(transparent)]
pub struct OhttpError(#[from] ohttp::Error);
pub struct OhttpError(#[from] payjoin::OhttpKeysError);
}

impl From<payjoin::OhttpKeys> for OhttpKeys {
Expand All @@ -28,15 +28,15 @@ impl OhttpKeys {
use std::sync::Mutex;

#[derive(uniffi::Object)]
pub struct ClientResponse(Mutex<Option<ohttp::ClientResponse>>);
pub struct ClientResponse(Mutex<Option<payjoin::OhttpResponse>>);

impl From<&ClientResponse> for ohttp::ClientResponse {
impl From<&ClientResponse> for payjoin::OhttpResponse {
fn from(value: &ClientResponse) -> Self {
let mut data_guard = value.0.lock().unwrap();
Option::take(&mut *data_guard).expect("ClientResponse moved out of memory")
}
}

impl From<ohttp::ClientResponse> for ClientResponse {
fn from(value: ohttp::ClientResponse) -> Self { Self(Mutex::new(Some(value))) }
impl From<payjoin::OhttpResponse> for ClientResponse {
fn from(value: payjoin::OhttpResponse) -> Self { Self(Mutex::new(Some(value))) }
}
7 changes: 3 additions & 4 deletions payjoin-ffi/src/receive/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,16 +300,15 @@ mod tests {
use payjoin::persist::InMemoryPersister;
use payjoin::receive::v2::{ReceiverBuilder, SessionEvent};
use payjoin::OhttpKeys;
use payjoin_test_utils::{EXAMPLE_URL, KEM, KEY_ID, SYMMETRIC};
use payjoin_test_utils::EXAMPLE_URL;

// Build a receiver whose session is already expired, then surface the
// expiry error through the dedicated create-request error.
let address = Address::from_str("tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4")
.expect("valid address")
.assume_checked();
let ohttp_keys = OhttpKeys(
ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).expect("valid keys"),
);
let ohttp_keys = OhttpKeys::decode(&payjoin_test_utils::ohttp_key_config_bytes())
.expect("valid ohttp keys");
let persister = InMemoryPersister::<SessionEvent>::default();
let receiver = ReceiverBuilder::new(address, EXAMPLE_URL, ohttp_keys)
.expect("valid builder")
Expand Down
23 changes: 23 additions & 0 deletions payjoin-test-utils/src/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,26 @@ pub const KEY_ID: KeyId = 1;
pub const KEM: Kem = Kem::K256Sha256;
pub const SYMMETRIC: &[SymmetricSuite] =
&[ohttp::SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)];

/// Derive the test OHTTP key config deterministically so that
/// [`ohttp_key_config_bytes`] and [`ohttp_server`] agree on the same key pair.
fn test_key_config() -> ohttp::KeyConfig {
ohttp::KeyConfig::derive(KEY_ID, KEM, SYMMETRIC.to_vec(), &crate::DUMMY32)
.expect("valid test key config")
}

/// The encoded OHTTP key config for tests, decodable via the public
/// `OhttpKeys::decode`.
///
/// Returns raw bytes rather than `OhttpKeys` so it is usable from payjoin's own
/// unit tests, where a helper returning a `payjoin` type would resolve to a
/// separate crate instance through the dev-dependency cycle.
pub fn ohttp_key_config_bytes() -> Vec<u8> {
test_key_config().encode().expect("valid key config encoding")
}

/// The OHTTP server matching [`ohttp_key_config_bytes`], for tests that emulate
/// the directory's OHTTP gateway and must decapsulate client requests.
pub fn ohttp_server() -> ohttp::Server {
ohttp::Server::new(test_key_config()).expect("valid ohttp server")
}
19 changes: 3 additions & 16 deletions payjoin/src/core/hpke.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use core::fmt;
use std::error;
use std::ops::Deref;

use bitcoin::key::constants::{ELLSWIFT_ENCODING_SIZE, PUBLIC_KEY_SIZE};
use bitcoin::secp256k1;
Expand Down Expand Up @@ -57,7 +56,7 @@ fn pubkey_from_compressed_bytes(pk_bytes: &[u8]) -> Result<HpkePublicKey, HpkeEr
}

fn compressed_bytes_from_pubkey(pk: &HpkePublicKey) -> [u8; PUBLIC_KEY_SIZE] {
let reply_pk_uncompressed = pk.to_bytes();
let reply_pk_uncompressed = pk.0.to_bytes();
secp256k1::PublicKey::from_slice(&reply_pk_uncompressed[..])
.expect("parsing a pubkey immediately after serializing it must not fail")
.serialize()
Expand All @@ -81,13 +80,7 @@ fn ellswift_bytes_from_encapped_key(
}

#[derive(Clone, PartialEq, Eq)]
pub struct HpkeSecretKey(pub SecretKey);

impl Deref for HpkeSecretKey {
type Target = SecretKey;

fn deref(&self) -> &Self::Target { &self.0 }
}
pub struct HpkeSecretKey(SecretKey);

impl fmt::Debug for HpkeSecretKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down Expand Up @@ -118,7 +111,7 @@ impl<'de> serde::Deserialize<'de> for HpkeSecretKey {
}

#[derive(Clone, PartialEq, Eq)]
pub struct HpkePublicKey(pub PublicKey);
pub struct HpkePublicKey(PublicKey);

impl HpkePublicKey {
pub fn to_compressed_bytes(&self) -> [u8; 33] {
Expand All @@ -135,12 +128,6 @@ impl HpkePublicKey {
}
}

impl Deref for HpkePublicKey {
type Target = PublicKey;

fn deref(&self) -> &Self::Target { &self.0 }
}

impl fmt::Debug for HpkePublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SecpHpkePublicKey({:?})", self.0)
Expand Down
8 changes: 1 addition & 7 deletions payjoin/src/core/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,6 @@ impl From<InternalErrorInner> for Error {

#[cfg(test)]
mod tests {
use std::str::FromStr;

use http::StatusCode;
use reqwest::Response;

Expand All @@ -195,11 +193,7 @@ mod tests {

#[tokio::test]
async fn test_parse_success_response() {
let valid_keys =
OhttpKeys::from_str("OH1QYPM5JXYNS754Y4R45QWE336QFX6ZR8DQGVQCULVZTV20TFVEYDMFQC")
.expect("valid keys")
.encode()
.expect("encodevalid keys");
let valid_keys = payjoin_test_utils::ohttp_key_config_bytes();

let response = mock_response(StatusCode::OK, valid_keys);
assert!(parse_ohttp_keys_response(response).await.is_ok(), "expected valid keys response");
Expand Down
2 changes: 1 addition & 1 deletion payjoin/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub use crate::hpke::{HpkeKeyPair, HpkePublicKey};
#[cfg(feature = "v2")]
pub(crate) mod ohttp;
#[cfg(feature = "v2")]
pub use crate::ohttp::OhttpKeys;
pub use crate::ohttp::{OhttpKeys, OhttpKeysError, OhttpResponse};

#[cfg(feature = "io")]
#[cfg_attr(docsrs, doc(cfg(feature = "io")))]
Expand Down
88 changes: 52 additions & 36 deletions payjoin/src/core/ohttp.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::ops::{Deref, DerefMut};
use std::{error, fmt};

use bitcoin::bech32::{self, EncodeError};
Expand All @@ -14,13 +13,13 @@ pub const PADDED_BHTTP_REQ_BYTES: usize =
ENCAPSULATED_MESSAGE_BYTES - (N_ENC + N_T + OHTTP_REQ_HEADER_BYTES);

pub(crate) fn ohttp_encapsulate(
ohttp_keys: &ohttp::KeyConfig,
ohttp_keys: &OhttpKeys,
method: &str,
target_resource: &str,
body: Option<&[u8]>,
) -> Result<([u8; ENCAPSULATED_MESSAGE_BYTES], ohttp::ClientResponse), OhttpEncapsulationError> {
use std::fmt::Write;
let mut ohttp_keys = ohttp_keys.clone();
let mut ohttp_keys = ohttp_keys.0.clone();

let ctx = ohttp::ClientRequest::from_config(&mut ohttp_keys)?;
let url = crate::core::Url::parse(target_resource)?;
Expand Down Expand Up @@ -210,16 +209,21 @@ impl error::Error for OhttpEncapsulationError {
}

#[derive(Debug, Clone)]
pub struct OhttpKeys(pub ohttp::KeyConfig);
pub struct OhttpKeys(ohttp::KeyConfig);

impl OhttpKeys {
/// Decode an OHTTP KeyConfig
pub fn decode(bytes: &[u8]) -> Result<Self, ohttp::Error> {
ohttp::KeyConfig::decode(bytes).map(Self)
pub fn decode(bytes: &[u8]) -> Result<Self, OhttpKeysError> {
ohttp::KeyConfig::decode(bytes).map(Self).map_err(|e| OhttpKeysError::Decode(Box::new(e)))
}

pub fn to_bytes(&self) -> Result<Vec<u8>, ohttp::Error> {
let bytes = self.encode()?;
/// Encode the OHTTP KeyConfig, decodable via [`OhttpKeys::decode`].
pub fn encode(&self) -> Result<Vec<u8>, OhttpKeysError> {
self.0.encode().map_err(|e| OhttpKeysError::Encode(Box::new(e)))
}

pub fn to_bytes(&self) -> Result<Vec<u8>, OhttpKeysError> {
let bytes = self.0.encode().map_err(|e| OhttpKeysError::Encode(Box::new(e)))?;

let key_id = bytes[0];
let uncompressed_pubkey = &bytes[3..68];
Expand All @@ -235,6 +239,20 @@ impl OhttpKeys {
}
}

/// An opaque OHTTP client context.
///
/// Returned alongside the [`Request`](crate::Request) by a `create_*_request`
/// method and consumed by the paired `process_*` method to decapsulate the
/// directory's response. Callers hold it between the two calls without
/// inspecting it.
pub struct OhttpResponse(ohttp::ClientResponse);

impl OhttpResponse {
pub(crate) fn new(inner: ohttp::ClientResponse) -> Self { Self(inner) }

pub(crate) fn into_inner(self) -> ohttp::ClientResponse { self.0 }
}

const KEM_ID: &[u8] = b"\x00\x16"; // DHKEM(secp256k1, HKDF-SHA256)
const SYMMETRIC_LEN: &[u8] = b"\x00\x04"; // 4 bytes
const SYMMETRIC_KDF_AEAD: &[u8] = b"\x00\x01\x00\x03"; // KDF(HKDF-SHA256), AEAD(ChaCha20Poly1305)
Expand All @@ -254,31 +272,31 @@ impl fmt::Display for OhttpKeys {
}

impl TryFrom<&[u8]> for OhttpKeys {
type Error = ParseOhttpKeysError;
type Error = OhttpKeysError;

fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let buf: [u8; 34] =
bytes.try_into().map_err(|_| ParseOhttpKeysError::IncorrectLength(bytes.len()))?;
bytes.try_into().map_err(|_| OhttpKeysError::IncorrectLength(bytes.len()))?;

let key_id = buf[0];
let compressed_pk = &buf[1..];

let pubkey = bitcoin::secp256k1::PublicKey::from_slice(compressed_pk)
.map_err(|_| ParseOhttpKeysError::InvalidPublicKey)?;
.map_err(|_| OhttpKeysError::InvalidPublicKey)?;

let mut buf = vec![key_id];
buf.extend_from_slice(KEM_ID);
buf.extend_from_slice(&pubkey.serialize_uncompressed());
buf.extend_from_slice(SYMMETRIC_LEN);
buf.extend_from_slice(SYMMETRIC_KDF_AEAD);

ohttp::KeyConfig::decode(&buf).map(Self).map_err(ParseOhttpKeysError::DecodeKeyConfig)
ohttp::KeyConfig::decode(&buf).map(Self).map_err(|e| OhttpKeysError::Decode(Box::new(e)))
}
}

#[cfg(test)]
impl std::str::FromStr for OhttpKeys {
type Err = ParseOhttpKeysError;
type Err = OhttpKeysError;

/// Parses a base64URL-encoded string into OhttpKeys.
/// The string format is: key_id || compressed_public_key
Expand All @@ -287,10 +305,10 @@ impl std::str::FromStr for OhttpKeys {
bech32::Hrp::parse("OH").expect("parsing a valid HRP constant should never fail");

let (hrp, bytes) =
crate::bech32::nochecksum::decode(s).map_err(|_| ParseOhttpKeysError::InvalidFormat)?;
crate::bech32::nochecksum::decode(s).map_err(|_| OhttpKeysError::InvalidFormat)?;

if hrp != oh_hrp {
return Err(ParseOhttpKeysError::InvalidFormat);
return Err(OhttpKeysError::InvalidFormat);
}

Self::try_from(&bytes[..])
Expand All @@ -299,26 +317,16 @@ impl std::str::FromStr for OhttpKeys {

impl PartialEq for OhttpKeys {
fn eq(&self, other: &Self) -> bool {
match (self.encode(), other.encode()) {
match (self.0.encode(), other.0.encode()) {
(Ok(self_encoded), Ok(other_encoded)) => self_encoded == other_encoded,
// If OhttpKeys::encode(&self) is Err, return false
// If the key config fails to encode, return false
_ => false,
}
}
}

impl Eq for OhttpKeys {}

impl Deref for OhttpKeys {
type Target = ohttp::KeyConfig;

fn deref(&self) -> &Self::Target { &self.0 }
}

impl DerefMut for OhttpKeys {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}

impl<'de> serde::Deserialize<'de> for OhttpKeys {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
Expand All @@ -334,38 +342,46 @@ impl serde::Serialize for OhttpKeys {
where
S: serde::Serializer,
{
let bytes = self.encode().map_err(serde::ser::Error::custom)?;
let bytes = self.0.encode().map_err(serde::ser::Error::custom)?;
bytes.serialize(serializer)
}
}

/// Error encoding or decoding [`OhttpKeys`].
#[derive(Debug)]
pub enum ParseOhttpKeysError {
#[non_exhaustive]
pub enum OhttpKeysError {
/// The provided bytes were not the expected length.
IncorrectLength(usize),
/// The bytes did not encode a valid public key.
InvalidPublicKey,
DecodeKeyConfig(ohttp::Error),
/// The bytes could not be decoded as an OHTTP key config.
Decode(Box<dyn std::error::Error + Send + Sync>),
/// The OHTTP key config could not be encoded.
Encode(Box<dyn std::error::Error + Send + Sync>),
#[cfg(test)]
InvalidFormat,
}

impl std::fmt::Display for ParseOhttpKeysError {
impl std::fmt::Display for OhttpKeysError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use ParseOhttpKeysError::*;
use OhttpKeysError::*;
match self {
IncorrectLength(l) => write!(f, "Invalid length, got {l} expected 34"),
InvalidPublicKey => write!(f, "Invalid public key"),
DecodeKeyConfig(e) => write!(f, "Failed to decode KeyConfig: {e}"),
Decode(e) => write!(f, "Failed to decode OHTTP keys: {e}"),
Encode(e) => write!(f, "Failed to encode OHTTP keys: {e}"),
#[cfg(test)]
InvalidFormat => write!(f, "Invalid format"),
}
}
}

impl std::error::Error for ParseOhttpKeysError {
impl std::error::Error for OhttpKeysError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use ParseOhttpKeysError::*;
use OhttpKeysError::*;
match self {
DecodeKeyConfig(e) => Some(e),
Decode(e) | Encode(e) => Some(e.as_ref()),
IncorrectLength(_) | InvalidPublicKey => None,
#[cfg(test)]
InvalidFormat => None,
Expand Down
Loading
Loading