Skip to content

Commit

Permalink
Write compressed OhttpKeys to Uri
Browse files Browse the repository at this point in the history
Payjoin-cli reads ohttp-keys from a binary file as a consequence.
  • Loading branch information
DanGould committed Sep 10, 2024
1 parent e1070a8 commit db1f485
Show file tree
Hide file tree
Showing 8 changed files with 54 additions and 28 deletions.
2 changes: 1 addition & 1 deletion payjoin-cli/example.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
12 changes: 6 additions & 6 deletions payjoin-cli/src/app/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -102,11 +100,13 @@ impl AppConfig {
.set_override_option(
"ohttp_keys",
matches.get_one::<String>("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()
}),
Expand Down
7 changes: 2 additions & 5 deletions payjoin-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,8 @@ fn cli() -> ArgMatches {
.help("The directory to store payjoin requests")
.value_parser(value_parser!(Url)),
);
receive_cmd = receive_cmd.arg(
Arg::new("ohttp_keys")
.long("ohttp-keys")
.help("The ohttp key config as a base64 encoded string"),
);
receive_cmd = receive_cmd
.arg(Arg::new("ohttp_keys").long("ohttp-keys").help("The ohttp key config file path"));
}

cmd = cmd.subcommand(receive_cmd);
Expand Down
5 changes: 3 additions & 2 deletions payjoin-cli/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)
Expand Down
5 changes: 1 addition & 4 deletions payjoin-directory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -138,9 +138,6 @@ fn init_ohttp() -> Result<ohttp::Server> {

// 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)?)
}

Expand Down
11 changes: 4 additions & 7 deletions payjoin/src/uri/url_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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());
}
Expand All @@ -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());
}
Expand Down
38 changes: 36 additions & 2 deletions payjoin/src/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,19 +362,48 @@ 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)
}
}

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<Self, Self::Err> {
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)
}
}

Expand Down Expand Up @@ -422,13 +451,17 @@ impl serde::Serialize for OhttpKeys {

#[derive(Debug)]
pub enum ParseOhttpKeysError {
InvalidFormat,
InvalidPublicKey,
DecodeBase64(bitcoin::base64::DecodeError),
DecodeKeyConfig(ohttp::Error),
}

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),
Expand All @@ -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,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion payjoin/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit db1f485

Please sign in to comment.