diff --git a/payjoin-cli/example.config.toml b/payjoin-cli/example.config.toml index 99de2098..74041ab8 100644 --- a/payjoin-cli/example.config.toml +++ b/payjoin-cli/example.config.toml @@ -54,4 +54,4 @@ ohttp_relay="https://pj.bobspacebkk.com" # (v2 only, optional) The HPKE keys which need to be fetched ahead of time from the pj_endpoint for the payjoin packets to be encrypted. # These can now be fetched and no longer need to be configured. -ohttp_keys="AQAg3c9qovMZvPzLh8XHgD8q86WG7SmPQvPamCTvEoueKBsABAABAAM" +ohttp_keys="./path/to/ohttp_keys" diff --git a/payjoin-cli/src/app/config.rs b/payjoin-cli/src/app/config.rs index d89ddf6c..abe9b433 100644 --- a/payjoin-cli/src/app/config.rs +++ b/payjoin-cli/src/app/config.rs @@ -92,8 +92,6 @@ impl AppConfig { #[cfg(feature = "v2")] let builder = { - use payjoin::base64::prelude::BASE64_URL_SAFE_NO_PAD; - use payjoin::base64::Engine; builder .set_override_option( "pj_directory", @@ -102,11 +100,13 @@ impl AppConfig { .set_override_option( "ohttp_keys", matches.get_one::("ohttp_keys").and_then(|s| { - BASE64_URL_SAFE_NO_PAD - .decode(s) + std::fs::read(s) .map_err(|e| { - log::error!("Failed to decode ohttp_keys: {}", e); - ConfigError::Message(format!("Invalid ohttp_keys: {}", e)) + log::error!("Failed to read ohttp_keys file: {}", e); + ConfigError::Message(format!( + "Failed to read ohttp_keys file: {}", + e + )) }) .ok() }), diff --git a/payjoin-cli/tests/e2e.rs b/payjoin-cli/tests/e2e.rs index 126f4100..7eae8b43 100644 --- a/payjoin-cli/tests/e2e.rs +++ b/payjoin-cli/tests/e2e.rs @@ -243,6 +243,8 @@ mod e2e { let ohttp_keys = payjoin::io::fetch_ohttp_keys(ohttp_relay.clone(), directory.clone(), cert.clone()) .await?; + let ohttp_keys_path = temp_dir.join("ohttp_keys"); + tokio::fs::write(&ohttp_keys_path, ohttp_keys.encode()?).await?; let receiver_rpchost = format!("http://{}/wallet/receiver", bitcoind.params.rpc_socket); let sender_rpchost = format!("http://{}/wallet/sender", bitcoind.params.rpc_socket); @@ -268,13 +270,12 @@ mod e2e { .arg("--pj-directory") .arg(&directory) .arg("--ohttp-keys") - .arg(&ohttp_keys.to_string()) + .arg(&ohttp_keys_path) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn() .expect("Failed to execute payjoin-cli"); let bip21 = get_bip21_from_receiver(cli_receive_initiator).await; - let cli_send_initiator = Command::new(payjoin_cli) .arg("--rpchost") .arg(&sender_rpchost) diff --git a/payjoin-directory/src/lib.rs b/payjoin-directory/src/lib.rs index 075a1082..25bbb1e7 100644 --- a/payjoin-directory/src/lib.rs +++ b/payjoin-directory/src/lib.rs @@ -15,7 +15,7 @@ use hyper::{Method, Request, Response, StatusCode, Uri}; use hyper_util::rt::TokioIo; use tokio::net::TcpListener; use tokio::sync::Mutex; -use tracing::{debug, error, info, trace}; +use tracing::{debug, error, trace}; pub const DEFAULT_DIR_PORT: u16 = 8080; pub const DEFAULT_DB_HOST: &str = "localhost:6379"; @@ -138,9 +138,6 @@ fn init_ohttp() -> Result { // create or read from file let server_config = ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC))?; - let encoded_config = server_config.encode()?; - let b64_config = BASE64_URL_SAFE_NO_PAD.encode(encoded_config); - info!("ohttp-keys server config base64 UrlSafe: {:?}", b64_config); Ok(ohttp::Server::new(server_config)?) } diff --git a/payjoin/src/uri/url_ext.rs b/payjoin/src/uri/url_ext.rs index d7d3a55b..c0743ca9 100644 --- a/payjoin/src/uri/url_ext.rs +++ b/payjoin/src/uri/url_ext.rs @@ -91,12 +91,9 @@ mod tests { let mut url = Url::parse("https://example.com").unwrap(); let ohttp_keys = - OhttpKeys::from_str("AQAWBG3fkg7fQCN-bafc-BEJOSnDfq8k1M9Cy1kgQZX42GVOvI0bWVAciTaJCy2A_wy7R7VxtU88xej692bv0uXgt98ABAABAAM").unwrap(); + OhttpKeys::from_str("AQO6SMScPUqSo60A7MY6Ak2hDO0CGAxz7BLYp60syRu0gw").unwrap(); let _ = url.set_ohttp(Some(ohttp_keys.clone())); - assert_eq!( - url.fragment(), - Some("ohttp=AQAWBG3fkg7fQCN-bafc-BEJOSnDfq8k1M9Cy1kgQZX42GVOvI0bWVAciTaJCy2A_wy7R7VxtU88xej692bv0uXgt98ABAABAAM") - ); + assert_eq!(url.fragment(), Some("ohttp=AQO6SMScPUqSo60A7MY6Ak2hDO0CGAxz7BLYp60syRu0gw")); assert_eq!(url.ohttp(), Some(ohttp_keys)); @@ -124,7 +121,7 @@ mod tests { // fragment is not percent encoded so `&ohttp=` is parsed as a query parameter, not a fragment parameter let uri = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=0.01\ &pj=https://example.com\ - #exp=1720547781&ohttp=AQAWBG3fkg7fQCN-bafc-BEJOSnDfq8k1M9Cy1kgQZX42GVOvI0bWVAciTaJCy2A_wy7R7VxtU88xej692bv0uXgt98ABAABAAM"; + #exp=1720547781&ohttp=AQO6SMScPUqSo60A7MY6Ak2hDO0CGAxz7BLYp60syRu0gw"; let uri = Uri::try_from(uri).unwrap().assume_checked().check_pj_supported().unwrap(); assert!(uri.extras.endpoint().ohttp().is_none()); } @@ -133,7 +130,7 @@ mod tests { fn test_valid_v2_url_fragment_on_bip21() { let uri = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=0.01\ &pj=https://example.com\ - #ohttp%3DAQAWBG3fkg7fQCN-bafc-BEJOSnDfq8k1M9Cy1kgQZX42GVOvI0bWVAciTaJCy2A_wy7R7VxtU88xej692bv0uXgt98ABAABAAM"; + #ohttp%3DAQO6SMScPUqSo60A7MY6Ak2hDO0CGAxz7BLYp60syRu0gw"; let uri = Uri::try_from(uri).unwrap().assume_checked().check_pj_supported().unwrap(); assert!(uri.extras.endpoint().ohttp().is_some()); } diff --git a/payjoin/src/v2.rs b/payjoin/src/v2.rs index ebba5737..c2a002f3 100644 --- a/payjoin/src/v2.rs +++ b/payjoin/src/v2.rs @@ -362,9 +362,23 @@ impl OhttpKeys { } } +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) + impl fmt::Display for OhttpKeys { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let encoded = BASE64_URL_SAFE_NO_PAD.encode(self.encode().map_err(|_| fmt::Error)?); + let bytes = self.encode().map_err(|_| fmt::Error)?; + let key_id = bytes[0]; + let pubkey = &bytes[3..68]; + + let compressed_pubkey = + bitcoin::secp256k1::PublicKey::from_slice(pubkey).map_err(|_| fmt::Error)?.serialize(); + + let mut buf = vec![key_id]; + buf.extend_from_slice(&compressed_pubkey); + + let encoded = BASE64_URL_SAFE_NO_PAD.encode(buf); write!(f, "{}", encoded) } } @@ -372,9 +386,24 @@ impl fmt::Display for OhttpKeys { impl std::str::FromStr for OhttpKeys { type Err = ParseOhttpKeysError; + /// Parses a base64URL-encoded string into OhttpKeys. + /// The string format is: key_id || compressed_public_key fn from_str(s: &str) -> Result { let bytes = BASE64_URL_SAFE_NO_PAD.decode(s).map_err(ParseOhttpKeysError::DecodeBase64)?; - OhttpKeys::decode(&bytes).map_err(ParseOhttpKeysError::DecodeKeyConfig) + + let key_id = *bytes.first().ok_or(ParseOhttpKeysError::InvalidFormat)?; + let compressed_pk = bytes.get(1..34).ok_or(ParseOhttpKeysError::InvalidFormat)?; + + let pubkey = bitcoin::secp256k1::PublicKey::from_slice(compressed_pk) + .map_err(|_| ParseOhttpKeysError::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) } } @@ -422,6 +451,8 @@ impl serde::Serialize for OhttpKeys { #[derive(Debug)] pub enum ParseOhttpKeysError { + InvalidFormat, + InvalidPublicKey, DecodeBase64(bitcoin::base64::DecodeError), DecodeKeyConfig(ohttp::Error), } @@ -429,6 +460,8 @@ pub enum ParseOhttpKeysError { impl std::fmt::Display for ParseOhttpKeysError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + ParseOhttpKeysError::InvalidFormat => write!(f, "Invalid format"), + ParseOhttpKeysError::InvalidPublicKey => write!(f, "Invalid public key"), ParseOhttpKeysError::DecodeBase64(e) => write!(f, "Failed to decode base64: {}", e), ParseOhttpKeysError::DecodeKeyConfig(e) => write!(f, "Failed to decode KeyConfig: {}", e), @@ -441,6 +474,7 @@ impl std::error::Error for ParseOhttpKeysError { match self { ParseOhttpKeysError::DecodeBase64(e) => Some(e), ParseOhttpKeysError::DecodeKeyConfig(e) => Some(e), + ParseOhttpKeysError::InvalidFormat | ParseOhttpKeysError::InvalidPublicKey => None, } } } diff --git a/payjoin/tests/integration.rs b/payjoin/tests/integration.rs index f61f8757..299fb085 100644 --- a/payjoin/tests/integration.rs +++ b/payjoin/tests/integration.rs @@ -105,7 +105,7 @@ mod integration { #[tokio::test] async fn test_bad_ohttp_keys() { let bad_ohttp_keys = - OhttpKeys::from_str("AQAWBG3fkg7fQCN-bafc-BEJOSnDfq8k1M9Cy1kgQZX42GVOvI0bWVAciTaJCy2A_wy7R7VxtU88xej692bv0uXgt98ABAABAAM") + OhttpKeys::from_str("AQO6SMScPUqSo60A7MY6Ak2hDO0CGAxz7BLYp60syRu0gw") .expect("Invalid OhttpKeys"); let (cert, key) = local_cert_key();