From 7707b31601c482e9039a03a6dff473546f05cbd1 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 11 Sep 2025 00:10:01 +0100 Subject: [PATCH] feat: add OHTTP (NUT-26) support Add Oblivious HTTP support for enhanced privacy between wallets and mints. - Add ohttp-gateway crate: OHTTP gateway server that can be embedded in cdk-mintd or run standalone, handling BHTTP encapsulation/decapsulation and key management - Add ohttp-client crate: client library for OHTTP key discovery and request encapsulation - Add OHTTP transport layer in cdk wallet for routing requests through an OHTTP relay - Add OhttpSettings (NUT-26) to MintInfo for advertising OHTTP support - Add MintBuilder::with_ohttp() for configuring OHTTP on the mint side - Add ohttp_gateway config section to cdk-mintd - Add --ohttp-relay CLI flag to cdk-cli - Wire OHTTP gateway router into cdk-mintd when enabled --- Cargo.lock | 354 ++++++++++- Cargo.lock.msrv | 573 +++++++++++++++--- crates/cashu/src/nuts/mod.rs | 2 +- crates/cashu/src/nuts/nut06.rs | 48 ++ crates/cdk-cli/Cargo.toml | 1 + crates/cdk-cli/README.md | 97 ++- crates/cdk-cli/src/main.rs | 4 + crates/cdk-ffi/src/types/mint.rs | 32 + .../src/bin/start_regtest_mints.rs | 1 + crates/cdk-integration-tests/src/shared.rs | 3 + crates/cdk-mintd/Cargo.toml | 2 + crates/cdk-mintd/example.config.toml | 4 + crates/cdk-mintd/src/config.rs | 10 + crates/cdk-mintd/src/env_vars/mod.rs | 5 + .../cdk-mintd/src/env_vars/ohttp_gateway.rs | 23 + crates/cdk-mintd/src/lib.rs | 43 ++ crates/cdk-mintd/src/main.rs | 20 +- crates/cdk/Cargo.toml | 2 + crates/cdk/src/mint/builder.rs | 19 +- .../cdk/src/wallet/mint_connector/README.md | 140 +++++ crates/cdk/src/wallet/mint_connector/mod.rs | 6 + .../wallet/mint_connector/ohttp_transport.rs | 163 +++++ crates/cdk/src/wallet/mod.rs | 4 + crates/ohttp-client/Cargo.toml | 29 + crates/ohttp-client/README.md | 9 + crates/ohttp-client/src/client.rs | 541 +++++++++++++++++ crates/ohttp-client/src/lib.rs | 3 + crates/ohttp-gateway/Cargo.toml | 39 ++ crates/ohttp-gateway/README.md | 64 ++ crates/ohttp-gateway/src/bin/main.rs | 63 ++ crates/ohttp-gateway/src/cli.rs | 69 +++ crates/ohttp-gateway/src/gateway.rs | 365 +++++++++++ crates/ohttp-gateway/src/key_config.rs | 83 +++ crates/ohttp-gateway/src/lib.rs | 12 + crates/ohttp-gateway/src/router.rs | 38 ++ 35 files changed, 2747 insertions(+), 124 deletions(-) create mode 100644 crates/cdk-mintd/src/env_vars/ohttp_gateway.rs create mode 100644 crates/cdk/src/wallet/mint_connector/README.md create mode 100644 crates/cdk/src/wallet/mint_connector/ohttp_transport.rs create mode 100644 crates/ohttp-client/Cargo.toml create mode 100644 crates/ohttp-client/README.md create mode 100644 crates/ohttp-client/src/client.rs create mode 100644 crates/ohttp-client/src/lib.rs create mode 100644 crates/ohttp-gateway/Cargo.toml create mode 100644 crates/ohttp-gateway/README.md create mode 100644 crates/ohttp-gateway/src/bin/main.rs create mode 100644 crates/ohttp-gateway/src/cli.rs create mode 100644 crates/ohttp-gateway/src/gateway.rs create mode 100644 crates/ohttp-gateway/src/key_config.rs create mode 100644 crates/ohttp-gateway/src/lib.rs create mode 100644 crates/ohttp-gateway/src/router.rs diff --git a/Cargo.lock b/Cargo.lock index cb6cc21eec..aaef7d47e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", + "rand_core 0.6.4", +] + [[package]] name = "aead" version = "0.5.2" @@ -27,6 +37,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures 0.2.17", + "opaque-debug", +] + [[package]] name = "aes" version = "0.8.4" @@ -34,22 +56,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", "zeroize", ] +[[package]] +name = "aes-gcm" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" +dependencies = [ + "aead 0.4.3", + "aes 0.7.5", + "cipher 0.3.0", + "ctr 0.7.0", + "ghash 0.4.4", + "subtle", +] + [[package]] name = "aes-gcm" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", + "aead 0.5.2", + "aes 0.8.4", + "cipher 0.4.4", + "ctr 0.9.2", + "ghash 0.5.1", "subtle", ] @@ -818,6 +854,16 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" +[[package]] +name = "bhttp" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16fc24bc615b9fd63148f59b218ea58a444b55762f8845da910e23aca686398b" +dependencies = [ + "thiserror 1.0.69", + "url", +] + [[package]] name = "bincode" version = "2.0.1" @@ -884,6 +930,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin-hpke" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37a54c486727c1d1ae9cc28dcf78b6e6ba20dcb88e8c892f1437d9ce215dc8c" +dependencies = [ + "aead 0.5.2", + "chacha20poly1305 0.10.1", + "digest 0.10.7", + "generic-array", + "hkdf 0.12.4", + "hmac 0.12.1", + "rand_core 0.6.4", + "secp256k1 0.29.1", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "bitcoin-internals" version = "0.2.1" @@ -899,6 +964,29 @@ version = "0.1.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11301df0b06f22dea7bb1916403fdd88a371031e495c49b8f96931b28189e175" +[[package]] +name = "bitcoin-ohttp" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a803a4b54e44635206b53329c78c0029d0c70926288ac2f07f4bb1267546cb" +dependencies = [ + "aead 0.4.3", + "aes-gcm 0.9.2", + "bitcoin-hpke", + "byteorder", + "chacha20poly1305 0.8.0", + "hex", + "hkdf 0.11.0", + "lazy_static", + "log", + "rand 0.8.6", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror 1.0.69", + "toml 0.5.11", +] + [[package]] name = "bitcoin-payment-instructions" version = "0.7.0" @@ -1002,6 +1090,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1185,7 +1282,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -1250,6 +1347,7 @@ dependencies = [ "lightning", "lightning-invoice", "nostr-sdk", + "ohttp-client", "rand 0.9.4", "regex", "ring 0.17.14", @@ -1669,6 +1767,7 @@ dependencies = [ "futures", "home", "lightning-invoice", + "ohttp-gateway", "serde", "serde_json", "tokio", @@ -1677,6 +1776,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "url", ] [[package]] @@ -1865,7 +1965,7 @@ dependencies = [ name = "cdk-supabase" version = "0.17.0-rc.1" dependencies = [ - "aes-gcm", + "aes-gcm 0.10.3", "async-trait", "bitcoin 0.32.100", "cdk-common", @@ -1894,6 +1994,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures 0.1.5", + "zeroize", +] + [[package]] name = "chacha20" version = "0.9.1" @@ -1901,7 +2013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", ] @@ -1922,16 +2034,29 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b4b0fc281743d80256607bd65e8beedc42cb0787ea119c85b81b4c0eab85e5f" +[[package]] +name = "chacha20poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1580317203210c517b6d44794abfbe600698276db18127e37ad3e69bf5e848e5" +dependencies = [ + "aead 0.4.3", + "chacha20 0.7.1", + "cipher 0.3.0", + "poly1305 0.7.2", + "zeroize", +] + [[package]] name = "chacha20poly1305" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "aead", + "aead 0.5.2", "chacha20 0.9.1", - "cipher", - "poly1305", + "cipher 0.4.4", + "poly1305 0.8.0", "zeroize", ] @@ -1982,6 +2107,15 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "cipher" version = "0.4.4" @@ -2249,6 +2383,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2402,13 +2545,32 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "crypto-mac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481" +dependencies = [ + "cipher 0.3.0", +] + [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -2642,6 +2804,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" @@ -3354,6 +3525,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug", + "polyval 0.5.3", +] + [[package]] name = "ghash" version = "0.5.1" @@ -3361,7 +3542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", - "polyval", + "polyval 0.6.2", ] [[package]] @@ -3626,6 +3807,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac 0.11.0", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -3635,6 +3826,16 @@ dependencies = [ "hmac 0.12.1", ] +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.12.1" @@ -4812,14 +5013,14 @@ version = "0.44.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d8f0fe13526800300a36bf3b7c5f752e62e32ab81c74a8e5caa2865708625a" dependencies = [ - "aes", + "aes 0.8.4", "base64 0.22.1", "bech32 0.11.1", "bip39", "bitcoin_hashes 0.14.100", "cbc", "chacha20 0.9.1", - "chacha20poly1305", + "chacha20poly1305 0.10.1", "getrandom 0.2.17", "hex", "instant", @@ -5024,6 +5225,54 @@ dependencies = [ "memchr", ] +[[package]] +name = "ohttp-client" +version = "0.17.0-rc.1" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bhttp", + "bitcoin 0.32.100", + "bitcoin-ohttp", + "bytes", + "clap", + "http 1.4.1", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "ohttp-gateway" +version = "0.17.0-rc.1" +dependencies = [ + "anyhow", + "axum 0.8.9", + "base64 0.22.1", + "bhttp", + "bitcoin 0.32.100", + "bitcoin-ohttp", + "bytes", + "clap", + "futures", + "home", + "http-body-util", + "hyper 1.10.1", + "hyper-util", + "reqwest", + "serde", + "serde_json", + "tokio", + "tower 0.5.3", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -5480,6 +5729,17 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.4.0", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -5488,7 +5748,19 @@ checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.5.1", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.4.0", ] [[package]] @@ -5500,7 +5772,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.5.1", ] [[package]] @@ -6553,7 +6825,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -6922,6 +7194,19 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.9" @@ -7109,7 +7394,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" dependencies = [ - "cipher", + "cipher 0.4.4", "ssh-encoding", ] @@ -7722,6 +8007,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.23" @@ -8396,9 +8690,9 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a050d7145f3d1209d579f2d62d0da3eeb25e269d5a98ec1ef3c11c72d8eb174" dependencies = [ - "aes", + "aes 0.8.4", "base64ct", - "ctr", + "ctr 0.9.2", "curve25519-dalek", "derive_more", "digest 0.10.7", @@ -8474,7 +8768,7 @@ dependencies = [ "amplify", "base64ct", "bitflags 2.11.1", - "cipher", + "cipher 0.4.4", "derive_builder_fork_arti", "derive_more", "digest 0.10.7", @@ -8535,14 +8829,14 @@ dependencies = [ "asynchronous-codec", "bitvec", "bytes", - "cipher", + "cipher 0.4.4", "coarsetime", "derive_builder_fork_arti", "derive_more", "digest 0.10.7", "educe", "futures", - "hkdf", + "hkdf 0.12.4", "hmac 0.12.1", "pin-project", "rand 0.8.6", @@ -9140,6 +9434,16 @@ dependencies = [ "weedle2", ] +[[package]] +name = "universal-hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "universal-hash" version = "0.5.1" diff --git a/Cargo.lock.msrv b/Cargo.lock.msrv index edb2287da0..c276711bf7 100644 --- a/Cargo.lock.msrv +++ b/Cargo.lock.msrv @@ -17,6 +17,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", + "rand_core 0.6.4", +] + [[package]] name = "aead" version = "0.5.2" @@ -27,6 +37,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures 0.2.17", + "opaque-debug", +] + [[package]] name = "aes" version = "0.8.4" @@ -34,22 +56,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", "zeroize", ] +[[package]] +name = "aes-gcm" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" +dependencies = [ + "aead 0.4.3", + "aes 0.7.5", + "cipher 0.3.0", + "ctr 0.7.0", + "ghash 0.4.4", + "subtle", +] + [[package]] name = "aes-gcm" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", + "aead 0.5.2", + "aes 0.8.4", + "cipher 0.4.4", + "ctr 0.9.2", + "ghash 0.5.1", "subtle", ] @@ -203,7 +239,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -214,7 +250,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -818,6 +854,16 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" +[[package]] +name = "bhttp" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16fc24bc615b9fd63148f59b218ea58a444b55762f8845da910e23aca686398b" +dependencies = [ + "thiserror 1.0.69", + "url", +] + [[package]] name = "bincode" version = "2.0.1" @@ -884,6 +930,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin-hpke" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37a54c486727c1d1ae9cc28dcf78b6e6ba20dcb88e8c892f1437d9ce215dc8c" +dependencies = [ + "aead 0.5.2", + "chacha20poly1305 0.10.1", + "digest 0.10.7", + "generic-array", + "hkdf 0.12.4", + "hmac 0.12.1", + "rand_core 0.6.4", + "secp256k1 0.29.1", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "bitcoin-internals" version = "0.2.1" @@ -899,6 +964,29 @@ version = "0.1.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11301df0b06f22dea7bb1916403fdd88a371031e495c49b8f96931b28189e175" +[[package]] +name = "bitcoin-ohttp" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a803a4b54e44635206b53329c78c0029d0c70926288ac2f07f4bb1267546cb" +dependencies = [ + "aead 0.4.3", + "aes-gcm 0.9.2", + "bitcoin-hpke", + "byteorder", + "chacha20poly1305 0.8.0", + "hex", + "hkdf 0.11.0", + "lazy_static", + "log", + "rand 0.8.6", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror 1.0.69", + "toml 0.5.11", +] + [[package]] name = "bitcoin-payment-instructions" version = "0.7.0" @@ -1002,6 +1090,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1185,7 +1282,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -1250,6 +1347,7 @@ dependencies = [ "lightning", "lightning-invoice", "nostr-sdk", + "ohttp-client", "rand 0.9.4", "regex", "ring 0.17.14", @@ -1386,7 +1484,7 @@ dependencies = [ "serde_with", "thiserror 2.0.18", "tokio", - "tonic 0.14.5", + "tonic 0.14.6", "tracing", "url", "uuid", @@ -1612,7 +1710,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-util", - "tonic 0.14.5", + "tonic 0.14.6", "tonic-prost", "tonic-prost-build", "tracing", @@ -1633,7 +1731,7 @@ dependencies = [ "serde_json", "thiserror 2.0.18", "tokio", - "tonic 0.14.5", + "tonic 0.14.6", "tonic-prost", "tonic-prost-build", "tracing", @@ -1669,6 +1767,7 @@ dependencies = [ "futures", "home", "lightning-invoice", + "ohttp-gateway", "serde", "serde_json", "tokio", @@ -1677,6 +1776,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "url", ] [[package]] @@ -1731,7 +1831,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "tonic 0.14.5", + "tonic 0.14.6", "tonic-prost", "tonic-prost-build", "tracing", @@ -1816,7 +1916,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-stream", - "tonic 0.14.5", + "tonic 0.14.6", "tonic-prost", "tonic-prost-build", "tracing", @@ -1865,7 +1965,7 @@ dependencies = [ name = "cdk-supabase" version = "0.17.0-rc.1" dependencies = [ - "aes-gcm", + "aes-gcm 0.10.3", "async-trait", "bitcoin 0.32.100", "cdk-common", @@ -1894,6 +1994,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures 0.1.5", + "zeroize", +] + [[package]] name = "chacha20" version = "0.9.1" @@ -1901,7 +2013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", ] @@ -1922,16 +2034,29 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b4b0fc281743d80256607bd65e8beedc42cb0787ea119c85b81b4c0eab85e5f" +[[package]] +name = "chacha20poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1580317203210c517b6d44794abfbe600698276db18127e37ad3e69bf5e848e5" +dependencies = [ + "aead 0.4.3", + "chacha20 0.7.1", + "cipher 0.3.0", + "poly1305 0.7.2", + "zeroize", +] + [[package]] name = "chacha20poly1305" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "aead", + "aead 0.5.2", "chacha20 0.9.1", - "cipher", - "poly1305", + "cipher 0.4.4", + "poly1305 0.8.0", "zeroize", ] @@ -1982,6 +2107,15 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "cipher" version = "0.4.4" @@ -2249,6 +2383,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2402,13 +2545,32 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "crypto-mac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481" +dependencies = [ + "cipher 0.3.0", +] + [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -2578,7 +2740,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c173dfcd5b92893ab05a8efb18b9522db4db6e0b93db5740f397573c027ce1e" dependencies = [ "derive-deftly-macros", - "heck 0.4.1", + "heck 0.5.0", ] [[package]] @@ -2587,8 +2749,8 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216fa20211bcd18cc359b75413bfb6cf89f62568fa27bc5fed3778a7a16e17af" dependencies = [ - "heck 0.4.1", - "indexmap 1.9.3", + "heck 0.5.0", + "indexmap 2.14.0", "itertools 0.12.1", "proc-macro-crate 3.5.0", "proc-macro2", @@ -2643,6 +2805,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" @@ -2943,7 +3114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3355,6 +3526,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug", + "polyval 0.5.3", +] + [[package]] name = "ghash" version = "0.5.1" @@ -3362,7 +3543,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", - "polyval", + "polyval 0.6.2", ] [[package]] @@ -3627,6 +3808,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac 0.11.0", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -3636,6 +3827,16 @@ dependencies = [ "hmac 0.12.1", ] +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3927,12 +4128,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -3940,13 +4142,12 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", - "serde", "tinystr 0.8.3", "writeable", "zerovec", @@ -3954,11 +4155,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b24a59706036ba941c9476a55cd57b82b77f38a3c667d637ee7cabbc85eaedc" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -3969,42 +4169,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.0.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a97b8ac6235e69506e8dacfb2adf38461d2ce6d3e9bd9c94c4cbc3cd4400a4" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", - "serde", - "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -4037,9 +4233,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -4160,6 +4356,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -4764,6 +4969,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "native-tls" version = "0.2.14" @@ -4803,14 +5014,14 @@ version = "0.44.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d8f0fe13526800300a36bf3b7c5f752e62e32ab81c74a8e5caa2865708625a" dependencies = [ - "aes", + "aes 0.8.4", "base64 0.22.1", "bech32 0.11.1", "bip39", "bitcoin_hashes 0.14.100", "cbc", "chacha20 0.9.1", - "chacha20poly1305", + "chacha20poly1305 0.10.1", "getrandom 0.2.17", "hex", "instant", @@ -4921,9 +5132,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -5015,6 +5226,54 @@ dependencies = [ "memchr", ] +[[package]] +name = "ohttp-client" +version = "0.17.0-rc.1" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bhttp", + "bitcoin 0.32.100", + "bitcoin-ohttp", + "bytes", + "clap", + "http 1.4.1", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "ohttp-gateway" +version = "0.17.0-rc.1" +dependencies = [ + "anyhow", + "axum 0.8.9", + "base64 0.22.1", + "bhttp", + "bitcoin 0.32.100", + "bitcoin-ohttp", + "bytes", + "clap", + "futures", + "home", + "http-body-util", + "hyper 1.10.1", + "hyper-util", + "reqwest", + "serde", + "serde_json", + "tokio", + "tower 0.5.3", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -5471,6 +5730,17 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.4.0", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -5479,7 +5749,19 @@ checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.5.1", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.4.0", ] [[package]] @@ -5491,7 +5773,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.5.1", ] [[package]] @@ -5725,7 +6007,7 @@ dependencies = [ "itertools 0.10.5", "lazy_static", "log", - "multimap", + "multimap 0.8.3", "petgraph 0.6.5", "prettyplease 0.1.25", "prost 0.11.9", @@ -5743,10 +6025,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.4.1", + "heck 0.5.0", "itertools 0.12.1", "log", - "multimap", + "multimap 0.10.1", "once_cell", "petgraph 0.6.5", "prettyplease 0.2.37", @@ -5763,10 +6045,10 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ - "heck 0.4.1", - "itertools 0.11.0", + "heck 0.5.0", + "itertools 0.14.0", "log", - "multimap", + "multimap 0.10.1", "petgraph 0.8.3", "prettyplease 0.2.37", "prost 0.14.3", @@ -5811,7 +6093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -5940,7 +6222,7 @@ dependencies = [ "once_cell", "socket2 0.6.4", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6422,7 +6704,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6544,7 +6826,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -6912,6 +7194,19 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.9" @@ -7002,9 +7297,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "simple_asn1" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", @@ -7068,7 +7363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7099,7 +7394,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" dependencies = [ - "cipher", + "cipher 0.4.4", "ssh-encoding", ] @@ -7339,7 +7634,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7422,30 +7717,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -7476,7 +7771,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", - "serde_core", "zerovec", ] @@ -7713,6 +8007,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.23" @@ -7870,9 +8173,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum 0.8.9", @@ -7913,9 +8216,9 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1882ac3bf5ef12877d7ed57aad87e75154c11931c2ba7e6cde5e22d63522c734" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" dependencies = [ "prettyplease 0.2.37", "proc-macro2", @@ -7925,20 +8228,20 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost 0.14.3", - "tonic 0.14.5", + "tonic 0.14.6", ] [[package]] name = "tonic-prost-build" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3144df636917574672e93d0f56d7edec49f90305749c668df5101751bb8f95a" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" dependencies = [ "prettyplease 0.2.37", "proc-macro2", @@ -7947,7 +8250,7 @@ dependencies = [ "quote", "syn 2.0.117", "tempfile", - "tonic-build 0.14.5", + "tonic-build 0.14.6", ] [[package]] @@ -8387,9 +8690,9 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a050d7145f3d1209d579f2d62d0da3eeb25e269d5a98ec1ef3c11c72d8eb174" dependencies = [ - "aes", + "aes 0.8.4", "base64ct", - "ctr", + "ctr 0.9.2", "curve25519-dalek", "derive_more", "digest 0.10.7", @@ -8465,7 +8768,7 @@ dependencies = [ "amplify", "base64ct", "bitflags 2.11.1", - "cipher", + "cipher 0.4.4", "derive_builder_fork_arti", "derive_more", "digest 0.10.7", @@ -8526,14 +8829,14 @@ dependencies = [ "asynchronous-codec", "bitvec", "bytes", - "cipher", + "cipher 0.4.4", "coarsetime", "derive_builder_fork_arti", "derive_more", "digest 0.10.7", "educe", "futures", - "hkdf", + "hkdf 0.12.4", "hmac 0.12.1", "pin-project", "rand 0.8.6", @@ -9131,6 +9434,16 @@ dependencies = [ "weedle2", ] +[[package]] +name = "universal-hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "universal-hash" version = "0.5.1" @@ -9742,6 +10055,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -9775,13 +10097,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -9794,6 +10133,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -9806,6 +10151,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -9818,12 +10169,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -9836,6 +10199,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -9848,6 +10217,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -9860,6 +10235,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -9872,6 +10253,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" @@ -10134,7 +10521,6 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", - "zerovec", ] [[package]] @@ -10143,7 +10529,6 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ - "serde", "yoke", "zerofrom", "zerovec-derive", diff --git a/crates/cashu/src/nuts/mod.rs b/crates/cashu/src/nuts/mod.rs index 54a185ac03..7cf9ff2d20 100644 --- a/crates/cashu/src/nuts/mod.rs +++ b/crates/cashu/src/nuts/mod.rs @@ -60,7 +60,7 @@ pub use nut05::{ MeltMethodSettings, MeltQuoteCustomRequest, MeltQuoteCustomResponse, MeltRequest, QuoteState as MeltQuoteState, Settings as NUT05Settings, }; -pub use nut06::{ContactInfo, MintInfo, MintVersion, Nuts}; +pub use nut06::{ContactInfo, MintInfo, MintVersion, Nuts, OhttpSettings}; pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State}; pub use nut09::{RestoreRequest, RestoreResponse}; pub use nut10::{ diff --git a/crates/cashu/src/nuts/nut06.rs b/crates/cashu/src/nuts/nut06.rs index 66507a7446..078bcbb775 100644 --- a/crates/cashu/src/nuts/nut06.rs +++ b/crates/cashu/src/nuts/nut06.rs @@ -332,6 +332,10 @@ pub struct Nuts { #[serde(rename = "22")] #[serde(skip_serializing_if = "Option::is_none")] pub nut22: Option, + /// NUT26 Settings + #[serde(rename = "26")] + #[serde(skip_serializing_if = "Option::is_none")] + pub nut26: Option, /// NUT29 Settings #[serde(default)] #[serde(rename = "29")] @@ -454,6 +458,14 @@ impl Nuts { } } + /// Nut26 OHTTP settings + pub fn nut26(self, ohttp_settings: OhttpSettings) -> Self { + Self { + nut26: Some(ohttp_settings), + ..self + } + } + /// Nut29 settings pub fn nut29(self, settings: nut29::Settings) -> Self { Self { @@ -485,6 +497,42 @@ impl Nuts { } } +/// NUT-26 OHTTP Settings +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct OhttpSettings { + /// Ohttp is supported + pub supported: bool, + /// OHTTP gateway URL + #[serde(skip_serializing_if = "Option::is_none")] + pub gateway_url: Option, +} + +impl OhttpSettings { + /// Create new [`OhttpSettings`] + pub fn new(supported: bool, gateway_url: Option) -> Self { + Self { + supported, + gateway_url, + } + } +} + +impl MintInfo { + /// Check if mint supports OHTTP + pub fn supports_ohttp(&self) -> bool { + self.nuts + .nut26 + .as_ref() + .map(|s| s.supported) + .unwrap_or_default() + } + + /// Get OHTTP configuration if supported + pub fn ohttp_config(&self) -> Option<&OhttpSettings> { + self.nuts.nut26.as_ref() + } +} + /// Check state Settings #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)] pub struct SupportedSettings { diff --git a/crates/cdk-cli/Cargo.toml b/crates/cdk-cli/Cargo.toml index 7d70ed13c2..7b0609b64c 100644 --- a/crates/cdk-cli/Cargo.toml +++ b/crates/cdk-cli/Cargo.toml @@ -17,6 +17,7 @@ sqlcipher = ["cdk-sqlite/sqlcipher"] redb = ["dep:cdk-redb"] tor = ["cdk/tor"] npubcash = ["cdk/npubcash"] +ohttp = ["cdk/ohttp"] [dependencies] anyhow.workspace = true diff --git a/crates/cdk-cli/README.md b/crates/cdk-cli/README.md index 8fe4faef3f..9b36c7880e 100644 --- a/crates/cdk-cli/README.md +++ b/crates/cdk-cli/README.md @@ -571,12 +571,105 @@ cdk-cli burn cdk-cli restore ``` + ## License Code is under the [MIT License](../../LICENSE) -## Contribution +## Contributing All contributions are welcome. -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, shall be licensed as above, without any additional terms or conditions. +### Basic Commands + +Check wallet balance: +```bash +cdk-cli balance +``` + +Send tokens: +```bash +cdk-cli send --amount 100 +``` + +Receive tokens: +```bash +cdk-cli receive +``` + +### OHTTP Support + +The CLI supports OHTTP (Oblivious HTTP) for enhanced privacy when communicating with mints. OHTTP is enabled by default and automatically used when: + +1. The mint supports OHTTP (advertised in mint info) +2. You provide an OHTTP relay URL using `--ohttp-relay` + +#### OHTTP Usage + +To use OHTTP, simply provide a relay URL. The CLI will automatically detect if the mint supports OHTTP and configure the connection appropriately: + +```bash +# Use OHTTP relay - CLI auto-detects OHTTP support and gateway URL from mint +cdk-cli --ohttp-relay https://relay.example.com balance +cdk-cli --ohttp-relay https://relay.example.com send --amount 100 +cdk-cli --ohttp-relay https://relay.example.com receive +``` + +#### How OHTTP Works in CDK-CLI + +1. **Automatic Detection**: When `--ohttp-relay` is provided, the CLI checks if the mint supports OHTTP +2. **Gateway Discovery**: The gateway URL is automatically discovered from the mint's OHTTP configuration, or falls back to using the mint URL directly +3. **Transport Setup**: An OHTTP transport layer is created with the mint URL, relay, and gateway +4. **Privacy Protection**: Requests are routed through the relay, providing privacy from both the relay and the gateway + +#### OHTTP Arguments + +- `--ohttp-relay `: OHTTP relay URL for routing requests through a privacy relay + +#### Example OHTTP Usage + +```bash +# Standard OHTTP usage with relay +cdk-cli --ohttp-relay https://ohttp-relay.example.com balance + +# All commands work with OHTTP +cdk-cli --ohttp-relay https://relay.example.com send --amount 100 +cdk-cli --ohttp-relay https://relay.example.com receive +cdk-cli --ohttp-relay https://relay.example.com mint --amount 1000 +``` + +#### OHTTP vs Regular Proxy + +OHTTP provides significantly better privacy compared to regular HTTP proxies: + +- **Regular proxy:** `cdk-cli --proxy https://proxy.example.com balance` + - Proxy can see all request content and your IP +- **OHTTP relay:** `cdk-cli --ohttp-relay https://relay.example.com balance` + - Relay cannot see request content (cryptographically protected) + - Gateway cannot see your real IP address + - Provides true metadata protection + +#### Important Notes + +- OHTTP requires the mint to explicitly support it +- If you specify `--ohttp-relay` but the mint doesn't support OHTTP, you'll see a warning and fall back to regular HTTP +- Gateway URL is automatically determined from the mint's OHTTP configuration +- When OHTTP is used, WebSocket subscriptions are automatically disabled in favor of HTTP polling + +## Building + +### Features + +- `ohttp`: Enables OHTTP support for enhanced privacy +- `sqlcipher`: Enables SQLCipher support for encrypted databases +- `redb`: Enables redb as an alternative database backend + +### Examples + +```bash +# Build with all features +cargo build --features "ohttp,sqlcipher,redb" + +# Build with just OHTTP +cargo build --features ohttp +``` diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 849b455b1f..b82a6b2a91 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -70,6 +70,10 @@ struct Cli { #[cfg(all(feature = "tor", not(target_arch = "wasm32")))] #[arg(long = "tor", value_enum, default_value_t = TorToggle::On)] transport: TorToggle, + /// OHTTP Relay URL for proxying requests (advanced usage) + #[cfg(feature = "ohttp")] + #[arg(long)] + ohttp_relay: Option, /// Subcommand to run #[command(subcommand)] command: Commands, diff --git a/crates/cdk-ffi/src/types/mint.rs b/crates/cdk-ffi/src/types/mint.rs index b297e86a5f..2ec3391356 100644 --- a/crates/cdk-ffi/src/types/mint.rs +++ b/crates/cdk-ffi/src/types/mint.rs @@ -405,6 +405,30 @@ impl TryFrom for cdk::nuts::BlindAuthSettings { } } +/// FFI-compatible OhttpSettings (NUT-26) +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +pub struct OhttpSettings { + /// OHTTP is supported + pub supported: bool, + /// OHTTP gateway URL + pub gateway_url: Option, +} + +impl From for OhttpSettings { + fn from(settings: cdk::nuts::OhttpSettings) -> Self { + Self { + supported: settings.supported, + gateway_url: settings.gateway_url, + } + } +} + +impl From for cdk::nuts::OhttpSettings { + fn from(settings: OhttpSettings) -> Self { + Self::new(settings.supported, settings.gateway_url) + } +} + /// FFI-compatible Nut29Settings (NUT-29) #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record, Default)] pub struct Nut29Settings { @@ -495,6 +519,8 @@ pub struct Nuts { pub nut21: Option, /// NUT22 Settings - Blind authentication pub nut22: Option, + /// NUT26 Settings - OHTTP + pub nut26: Option, /// NUT29 Settings - Batch minting pub nut29: Nut29Settings, /// Supported currency units for minting @@ -529,6 +555,7 @@ impl From for Nuts { nut20_supported: nuts.nut20.supported, nut21: nuts.nut21.map(Into::into), nut22: nuts.nut22.map(Into::into), + nut26: nuts.nut26.map(Into::into), nut29: nuts.nut29.into(), mint_units, melt_units, @@ -572,6 +599,7 @@ impl TryFrom for cdk::nuts::Nuts { }, nut21: n.nut21.map(|s| s.try_into()).transpose()?, nut22: n.nut22.map(|s| s.try_into()).transpose()?, + nut26: n.nut26.map(Into::into), nut29: n.nut29.into(), }) } @@ -747,6 +775,7 @@ mod tests { ), )], }), + nut26: None, nut29: Default::default(), } } @@ -887,6 +916,7 @@ mod tests { nut20: cdk::nuts::nut06::SupportedSettings { supported: false }, nut21: None, nut22: None, + nut26: None, nut29: Default::default(), }; @@ -919,6 +949,7 @@ mod tests { nut20_supported: false, nut21: None, nut22: None, + nut26: None, nut29: Default::default(), mint_units: vec![], melt_units: vec![], @@ -1080,6 +1111,7 @@ mod tests { path: "/v1/unknown-custom-endpoint".to_string(), }], }), + nut26: None, nut29: Nut29Settings::default(), mint_units: vec![], melt_units: vec![], diff --git a/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs index 8e69f24c3b..9ffd82f457 100644 --- a/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs +++ b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs @@ -333,6 +333,7 @@ fn create_ldk_settings( ldk_node: Some(ldk_config), fake_wallet: None, onchain: None, + ohttp_gateway: None, ..Default::default() } } diff --git a/crates/cdk-integration-tests/src/shared.rs b/crates/cdk-integration-tests/src/shared.rs index 5dbb0b61a9..be0c11f2ad 100644 --- a/crates/cdk-integration-tests/src/shared.rs +++ b/crates/cdk-integration-tests/src/shared.rs @@ -227,6 +227,7 @@ pub fn create_fake_wallet_settings( mint_management_rpc: None, auth: None, prometheus: Some(Default::default()), + ohttp_gateway: None, ..Default::default() } } @@ -285,6 +286,7 @@ pub fn create_cln_settings( mint_management_rpc: None, auth: None, prometheus: Some(Default::default()), + ohttp_gateway: None, ..Default::default() } } @@ -338,6 +340,7 @@ pub fn create_lnd_settings( mint_management_rpc: None, auth: None, prometheus: Some(Default::default()), + ohttp_gateway: None, ..Default::default() } } diff --git a/crates/cdk-mintd/Cargo.toml b/crates/cdk-mintd/Cargo.toml index ab10df795c..5d7a92856b 100644 --- a/crates/cdk-mintd/Cargo.toml +++ b/crates/cdk-mintd/Cargo.toml @@ -51,6 +51,7 @@ cdk-axum.workspace = true cdk-signatory.workspace = true cdk-mint-rpc = { workspace = true, optional = true } cdk-payment-processor = { workspace = true, optional = true } +ohttp-gateway = { path = "../ohttp-gateway" } config.workspace = true cdk-prometheus = { workspace = true, optional = true , features = ["system-metrics"]} clap.workspace = true @@ -67,6 +68,7 @@ tower-http = { workspace = true, features = ["compression-full", "decompression- tower.workspace = true lightning-invoice.workspace = true home.workspace = true +url.workspace = true [lints] workspace = true diff --git a/crates/cdk-mintd/example.config.toml b/crates/cdk-mintd/example.config.toml index 538534d029..7e30accba9 100644 --- a/crates/cdk-mintd/example.config.toml +++ b/crates/cdk-mintd/example.config.toml @@ -31,6 +31,10 @@ melt_ttl = 120 enabled = false # address = "127.0.0.1" # port = 8086 +# + +# [ohttp_gateway] +# enabled = false #[prometheus] #enabled = true diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index 290436eaa0..2b4af6b9fb 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -1029,6 +1029,7 @@ pub struct Settings { #[cfg(feature = "prometheus")] #[serde(default, skip_serializing_if = "Option::is_none")] pub prometheus: Option, + pub ohttp_gateway: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -1067,6 +1068,15 @@ fn default_max_outputs() -> usize { 1000 } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct OhttpGateway { + /// Whether OHTTP Gateway is enabled + #[serde(default)] + pub enabled: bool, + /// OHTTP gateway URL (if different from mint URL) + pub gateway_url: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct MintInfo { /// name of the mint and should be recognizable diff --git a/crates/cdk-mintd/src/env_vars/mod.rs b/crates/cdk-mintd/src/env_vars/mod.rs index f3e51c3ac6..0fc8fe5511 100644 --- a/crates/cdk-mintd/src/env_vars/mod.rs +++ b/crates/cdk-mintd/src/env_vars/mod.rs @@ -10,6 +10,7 @@ mod info; mod limits; mod ln; mod mint_info; +mod ohttp_gateway; mod onchain; mod auth; @@ -58,6 +59,7 @@ pub use lnd::*; #[cfg(feature = "management-rpc")] pub use management_rpc::*; pub use mint_info::*; +pub use ohttp_gateway::*; pub use onchain::*; #[cfg(feature = "prometheus")] pub use prometheus::*; @@ -145,6 +147,9 @@ impl Settings { self.prometheus = Some(self.prometheus.clone().unwrap_or_default().from_env()); } + // Process OHTTP gateway configuration from environment variables + self.ohttp_gateway = Some(self.ohttp_gateway.clone().unwrap_or_default().from_env()); + #[cfg(feature = "cln")] { let cln = self.cln.clone().unwrap_or_default().from_env(); diff --git a/crates/cdk-mintd/src/env_vars/ohttp_gateway.rs b/crates/cdk-mintd/src/env_vars/ohttp_gateway.rs new file mode 100644 index 0000000000..1f7170b53d --- /dev/null +++ b/crates/cdk-mintd/src/env_vars/ohttp_gateway.rs @@ -0,0 +1,23 @@ +//! Environment variables for OHTTP Gateway configuration + +use std::env; + +use crate::config::OhttpGateway; + +// Environment variable names +pub const OHTTP_GATEWAY_ENABLED_ENV_VAR: &str = "CDK_MINTD_OHTTP_GATEWAY_ENABLED"; +pub const OHTTP_GATEWAY_URL_ENV_VAR: &str = "CDK_MINTD_OHTTP_GATEWAY_URL"; + +impl OhttpGateway { + pub fn from_env(mut self) -> Self { + if let Ok(enabled_str) = env::var(OHTTP_GATEWAY_ENABLED_ENV_VAR) { + self.enabled = enabled_str.to_lowercase() == "true" || enabled_str == "1"; + } + + if let Ok(gateway_url) = env::var(OHTTP_GATEWAY_URL_ENV_VAR) { + self.gateway_url = Some(gateway_url); + } + + self + } +} diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index 8f8fd400d1..b45b98d7bb 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -434,6 +434,9 @@ async fn configure_mint_builder( bail!("At least one payment backend (Lightning or On-chain) must be configured"); } + // Configure OHTTP + let mint_builder = configure_ohttp(settings, mint_builder); + Ok(mint_builder) } @@ -984,6 +987,28 @@ async fn configure_cache( Ok(mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints)) } +/// Configures OHTTP settings +fn configure_ohttp(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder { + if let Some(ohttp_config) = &settings.ohttp_gateway { + if ohttp_config.enabled { + let mint_url = format!( + "http://{}:{}", + settings.info.listen_host, settings.info.listen_port + ); + + tracing::info!("Configuring OHTTP support"); + + return mint_builder.with_ohttp( + ohttp_config.enabled, + ohttp_config.gateway_url.clone(), + Some(mint_url), + ); + } + } + + mint_builder +} + async fn setup_authentication( settings: &config::Settings, _work_dir: &Path, @@ -1531,6 +1556,9 @@ async fn start_services_with_shutdown( } }; + #[cfg(not(feature = "prometheus"))] + let _prometheus_handle: Option> = None; + mint.start().await?; let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?; @@ -1608,6 +1636,21 @@ fn work_dir() -> Result { Ok(dir) } +/// Creates an OHTTP gateway router that forwards encapsulated requests to the mint +pub fn create_ohttp_gateway_router(settings: &config::Settings, work_dir: &Path) -> Result { + // Use the mint's own URL as the backend URL + let backend_url = format!( + "http://{}:{}", + settings.info.listen_host, settings.info.listen_port + ); + + // OHTTP keys are always stored in the work directory + let ohttp_keys_path = work_dir.join("ohttp_keys.json"); + + // Use the ohttp-gateway crate's router creation function + ohttp_gateway::create_ohttp_gateway_router(&backend_url, ohttp_keys_path) +} + /// The main entry point for the application when used as a library pub async fn run_mintd( work_dir: &Path, diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 07987588c5..40aa5f58a7 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -24,13 +24,31 @@ fn main() -> Result<()> { #[cfg(not(feature = "sqlcipher"))] let password = None; + // Create OHTTP gateway router if enabled + let mut routers = vec![]; + + if let Some(ohttp_config) = &settings.ohttp_gateway { + if ohttp_config.enabled { + match cdk_mintd::create_ohttp_gateway_router(&settings, &work_dir) { + Ok(router) => { + tracing::info!("OHTTP gateway enabled and router created"); + routers.push(router); + } + Err(e) => { + tracing::error!("Failed to create OHTTP gateway router: {}", e); + return Err(e); + } + } + } + } + cdk_mintd::run_mintd( &work_dir, &settings, password, args.enable_logging, Some(rt_clone), - vec![], + routers, ) .await }) diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index 1113df0238..2c98b4ce61 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -17,6 +17,7 @@ nostr = ["wallet", "dep:nostr-sdk", "cdk-common/nostr"] npubcash = ["wallet", "nostr", "dep:cdk-npubcash"] mint = ["dep:futures", "cdk-common/mint", "cdk-common/http", "cdk-signatory"] bip353 = ["dep:hickory-resolver", "cdk-common/bip353"] +ohttp = ["wallet", "dep:ohttp-client"] bench = [] http_subscription = [] tor = [ @@ -58,6 +59,7 @@ cdk-npubcash = { workspace = true, optional = true } cdk-prometheus = {workspace = true, optional = true} bitcoin-payment-instructions = { workspace = true } web-time.workspace = true +ohttp-client = { path = "../ohttp-client", optional = true } zeroize = "1" tokio-util.workspace = true diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index daca526a2b..b93d147fce 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -21,7 +21,7 @@ use crate::cdk_database; use crate::mint::Mint; use crate::nuts::{ ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion, - MppMethodSettings, PaymentMethod, ProtectedEndpoint, + MppMethodSettings, OhttpSettings, PaymentMethod, ProtectedEndpoint, }; use crate::types::PaymentProcessorKey; @@ -371,6 +371,23 @@ impl MintBuilder { Ok(()) } + /// Add support for NUT26 OHTTP + pub fn with_ohttp( + mut self, + enabled: bool, + gateway_url: Option, + mint_url: Option, + ) -> Self { + let final_gateway_url = match (gateway_url, mint_url) { + (Some(gateway), Some(mint)) if gateway == mint => None, + (gateway, _) => gateway, + }; + + let ohttp_settings = OhttpSettings::new(enabled, final_gateway_url); + self.mint_info.nuts.nut26 = Some(ohttp_settings); + self + } + /// Add a payment processor for the given unit and payment method /// /// If the unit has not been configured via [`configure_unit`](Self::configure_unit), diff --git a/crates/cdk/src/wallet/mint_connector/README.md b/crates/cdk/src/wallet/mint_connector/README.md new file mode 100644 index 0000000000..e424a6f8d6 --- /dev/null +++ b/crates/cdk/src/wallet/mint_connector/README.md @@ -0,0 +1,140 @@ +# Mint Connectors + +This module provides different ways to connect wallets to Cashu mints. + +## HTTP Connector + +The standard `HttpClient` provides direct HTTP communication with mints: + +```rust +use cdk::wallet::mint_connector::HttpClient; +use cdk::mint_url::MintUrl; + +let mint_url = MintUrl::from("https://mint.example.com")?; +let client = HttpClient::new(mint_url); +``` + +## OHTTP Connector + +The OHTTP connector provides privacy-enhanced communication through OHTTP gateways or relays using the same `HttpClient` with an OHTTP transport: + +### Using an OHTTP Gateway + +```rust +use cdk::wallet::mint_connector::{http_client::HttpClient, ohttp_transport::OhttpTransport}; +use cdk::mint_url::MintUrl; +use url::Url; + +let mint_url = MintUrl::from("https://mint.example.com")?; +let gateway_url = Url::parse("https://gateway.example.com")?; +let keys_source_url = gateway_url.clone(); // Keys fetched from same gateway + +// Create OHTTP transport +let transport = OhttpTransport::new_with_gateway(gateway_url, keys_source_url); + +// Create HTTP client with OHTTP transport +let client = HttpClient::with_transport(mint_url, transport); +``` + +### Using an OHTTP Relay + +```rust +use cdk::wallet::mint_connector::{http_client::HttpClient, ohttp_transport::OhttpTransport}; +use cdk::mint_url::MintUrl; +use url::Url; + +let mint_url = MintUrl::from("https://mint.example.com")?; +let gateway_url = Url::parse("https://gateway.example.com")?; +let relay_url = Url::parse("https://relay.example.com")?; +let keys_source_url = gateway_url.clone(); + +// Create OHTTP transport with relay +let transport = OhttpTransport::new(mint_url.as_url().clone(), gateway_url, relay_url, keys_source_url); + +// Create HTTP client with OHTTP transport +let client = HttpClient::with_transport(mint_url, transport); +``` + +### Using Pre-loaded OHTTP Keys + +```rust +use cdk::wallet::mint_connector::{http_client::HttpClient, ohttp_transport::OhttpTransport}; +use cdk::mint_url::MintUrl; +use url::Url; + +let mint_url = MintUrl::from("https://mint.example.com")?; +let gateway_url = Url::parse("https://gateway.example.com")?; +let ohttp_keys = std::fs::read("ohttp_keys.bin")?; + +// Create OHTTP transport with pre-loaded keys +let transport = OhttpTransport::new_with_keys(gateway_url, ohttp_keys); + +// Create HTTP client with OHTTP transport +let client = HttpClient::with_transport(mint_url, transport); +``` + +### Convenient Type Alias + +For easier usage, you can also use the type alias: + +```rust +use cdk::wallet::mint_connector::OhttpHttpClient; +use cdk::mint_url::MintUrl; +use url::Url; + +let mint_url = MintUrl::from("https://mint.example.com")?; +let gateway_url = Url::parse("https://gateway.example.com")?; +let keys_source_url = gateway_url.clone(); + +// Create OHTTP transport +let transport = ohttp_transport::OhttpTransport::new_with_gateway(gateway_url, keys_source_url); + +// Use the convenient type alias +let client: OhttpHttpClient = HttpClient::with_transport(mint_url, transport); +``` + +## Usage + +All connectors implement the `MintConnector` trait, so they can be used interchangeably: + +```rust +use cdk::wallet::mint_connector::MintConnector; + +async fn mint_info(client: &dyn MintConnector) -> Result<(), Error> { + let info = client.get_mint_info().await?; + println!("Mint: {}", info.name.unwrap_or_default()); + Ok(()) +} +``` + +## Features + +- **HTTP Connector**: Direct, fast communication +- **OHTTP Connector**: Privacy-enhanced communication through OHTTP protocol + - Gateway mode: Direct connection to OHTTP gateway + - Relay mode: Connection through OHTTP relay to gateway + - Pre-loaded keys: Use cached OHTTP keys for faster initialization + +## Privacy Benefits of OHTTP + +The OHTTP (Oblivious HTTP) protocol provides: + +1. **Request Privacy**: The gateway cannot see request contents +2. **Response Privacy**: The gateway cannot see response contents +3. **Metadata Protection**: Connection metadata is separated from request data +4. **Forward Secrecy**: Each request uses fresh encryption keys + +This makes it much harder for network observers to correlate wallet activities with specific users. + +## Performance Considerations + +- OHTTP adds some latency due to encryption/decryption overhead +- Network topology (gateway/relay locations) affects performance +- Pre-loading OHTTP keys can reduce initialization time +- Consider caching strategies for frequently accessed data + +## Examples + +See the `examples/` directory for complete usage examples: + +- `ohttp_mint_connector.rs`: Basic OHTTP connector usage diff --git a/crates/cdk/src/wallet/mint_connector/mod.rs b/crates/cdk/src/wallet/mint_connector/mod.rs index c3225cea1b..4158d5272e 100644 --- a/crates/cdk/src/wallet/mint_connector/mod.rs +++ b/crates/cdk/src/wallet/mint_connector/mod.rs @@ -19,6 +19,8 @@ use crate::nuts::{ use crate::wallet::AuthWallet; pub mod http_client; +#[cfg(feature = "ohttp")] +pub mod ohttp_transport; pub mod transport; /// Auth HTTP Client with async transport @@ -29,6 +31,10 @@ pub type HttpClient = http_client::HttpClient; #[cfg(all(feature = "tor", not(target_arch = "wasm32")))] pub type TorHttpClient = http_client::HttpClient; +/// OHTTP Client using HttpClient with OHTTP transport +#[cfg(feature = "ohttp")] +pub type OhttpHttpClient = http_client::HttpClient; + /// Interface that connects a wallet to a mint. Typically represents an [HttpClient]. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] diff --git a/crates/cdk/src/wallet/mint_connector/ohttp_transport.rs b/crates/cdk/src/wallet/mint_connector/ohttp_transport.rs new file mode 100644 index 0000000000..47b4abdd2b --- /dev/null +++ b/crates/cdk/src/wallet/mint_connector/ohttp_transport.rs @@ -0,0 +1,163 @@ +//! OHTTP Transport implementation +use std::sync::Arc; + +use async_trait::async_trait; +use cdk_common::AuthToken; +use serde::de::DeserializeOwned; +use serde::Serialize; +use url::Url; + +use super::transport::Transport; +use super::Error; +use crate::error::ErrorResponse; + +/// OHTTP Transport for communicating through OHTTP gateways/relays +#[derive(Clone)] +pub struct OhttpTransport { + client: Arc, +} + +impl std::fmt::Debug for OhttpTransport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OhttpTransport") + .field("client", &"Arc") + .finish() + } +} + +impl OhttpTransport { + /// Create new OHTTP transport with gateway and relay URLs + /// + /// The request flow: + /// 1. Send to relay_url + /// 2. Relay forwards to gateway_url + /// 3. Gateway forwards to target_url (mint) + /// 4. Keys are fetched from keys_source_url (same as target) + pub fn new(target_url: Url, relay_url: Url, gateway_url: Url) -> Self { + let client = ohttp_client::OhttpClient::new(relay_url, None, gateway_url, target_url); + + Self { + client: Arc::new(client), + } + } +} + +impl Default for OhttpTransport { + fn default() -> Self { + // Provide a minimal default that won't panic, but won't work until properly configured + // This is needed for the Transport trait, but users should use ::new() instead + let dummy_url = Url::parse("http://localhost").expect("Invalid default URL"); + Self::new(dummy_url.clone(), dummy_url.clone(), dummy_url) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Transport for OhttpTransport { + fn with_proxy( + &mut self, + _proxy: Url, + _host_matcher: Option<&str>, + _accept_invalid_certs: bool, + ) -> Result<(), Error> { + // OHTTP transport doesn't support traditional proxies since it already + // provides privacy through the OHTTP protocol + Err(Error::Custom( + "OHTTP transport does not support traditional proxies".to_string(), + )) + } + + async fn http_get(&self, url: Url, auth: Option) -> Result + where + R: DeserializeOwned, + { + // Extract path from URL + let path = url.path(); + + // Prepare headers + let mut headers = Vec::new(); + if let Some(auth_token) = auth { + headers.push((auth_token.header_key().to_string(), auth_token.to_string())); + } + + // Send GET request through OHTTP + let response = self + .client + .send_ohttp_request("GET", &[], &headers, path) + .await + .map_err(|e| Error::Custom(format!("OHTTP request failed: {}", e)))?; + + // Check for HTTP errors + if response.status >= 400 { + return Err(Error::HttpError( + Some(response.status), + format!("HTTP {} error", response.status), + )); + } + + // Parse response body + let response_text = response + .text() + .map_err(|e| Error::Custom(format!("Failed to decode response: {}", e)))?; + + serde_json::from_str::(&response_text).map_err(|err| { + tracing::warn!("OHTTP Response error: {}", err); + match ErrorResponse::from_json(&response_text) { + Ok(error_response) => error_response.into(), + Err(parse_err) => parse_err.into(), + } + }) + } + + async fn http_post( + &self, + url: Url, + auth_token: Option, + payload: &P, + ) -> Result + where + P: Serialize + ?Sized + Send + Sync, + R: DeserializeOwned, + { + // Extract path from URL + let path = url.path(); + + // Serialize payload to JSON + let body = serde_json::to_vec(payload) + .map_err(|e| Error::Custom(format!("Failed to serialize payload: {}", e)))?; + + // Prepare headers + let mut headers = vec![("Content-Type".to_string(), "application/json".to_string())]; + if let Some(auth) = auth_token { + headers.push((auth.header_key().to_string(), auth.to_string())); + } + + // Send POST request through OHTTP + let response = self + .client + .send_ohttp_request("POST", &body, &headers, path) + .await + .map_err(|e| Error::Custom(format!("OHTTP request failed: {}", e)))?; + + // Check for HTTP errors + if response.status >= 400 { + return Err(Error::HttpError( + Some(response.status), + format!("HTTP {} error", response.status), + )); + } + + // Parse response body + let response_text = response + .text() + .map_err(|e| Error::Custom(format!("Failed to decode response: {}", e)))?; + + serde_json::from_str::(&response_text).map_err(|err| { + tracing::warn!("OHTTP Response error: {}", err); + match ErrorResponse::from_json(&response_text) { + Ok(error_response) => error_response.into(), + Err(parse_err) => parse_err.into(), + } + }) + } +} diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 5fa4962adb..ca11e9949c 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -84,7 +84,11 @@ pub use cdk_common::wallet::{ }; pub use keysets::KeysetFilter; pub use melt::{MeltConfirmOptions, MeltOutcome, PendingMelt, PreparedMelt}; +#[cfg(feature = "ohttp")] +pub use mint_connector::ohttp_transport; pub use mint_connector::transport::Transport as HttpTransport; +#[cfg(feature = "ohttp")] +pub use mint_connector::OhttpHttpClient; pub use mint_connector::{ AuthHttpClient, HttpClient, LnurlPayInvoiceResponse, LnurlPayResponse, MintConnector, }; diff --git a/crates/ohttp-client/Cargo.toml b/crates/ohttp-client/Cargo.toml new file mode 100644 index 0000000000..bc25a5087a --- /dev/null +++ b/crates/ohttp-client/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ohttp-client" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +description = "Generic OHTTP client for sending arbitrary data through OHTTP gateways" +keywords = ["bitcoin", "e-cash", "cashu", "ohttp"] +categories = ["cryptography::cryptocurrencies", "command-line-utilities"] + +[dependencies] +anyhow.workspace = true +bhttp = { version = "0.6.1", features = ["http"] } +bitcoin.workspace = true +bytes = "1.9.0" +clap = { workspace = true, features = ["derive", "env"] } +http = "1.2.0" +ohttp = { package = "bitcoin-ohttp", version = "0.6.0" } +reqwest.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio = { workspace = true, features = ["full"] } +tracing.workspace = true +tracing-subscriber.workspace = true +base64 = "0.22.1" +url.workspace = true diff --git a/crates/ohttp-client/README.md b/crates/ohttp-client/README.md new file mode 100644 index 0000000000..667a5c36b0 --- /dev/null +++ b/crates/ohttp-client/README.md @@ -0,0 +1,9 @@ +# OHTTP Client + +## Overview + +The OHTTP Client implements the [Oblivious HTTP specification](https://ietf-wg-ohai.github.io/oblivious-http/draft-ietf-ohai-ohttp.html) to enable private HTTP communications. It can work with: + +- **OHTTP Gateways**: Direct connection to gateways that decrypt and forward requests to backend services +- **OHTTP Relays**: Connection through relays that forward encrypted requests to gateways without seeing content + diff --git a/crates/ohttp-client/src/client.rs b/crates/ohttp-client/src/client.rs new file mode 100644 index 0000000000..c69a4000ec --- /dev/null +++ b/crates/ohttp-client/src/client.rs @@ -0,0 +1,541 @@ +use std::sync::Arc; + +use anyhow::{anyhow, Result}; +use http::HeaderMap; +use reqwest::Client; +use tokio::sync::RwLock; +use url::Url; + +/// OHTTP client for sending requests through gateways or relays +pub struct OhttpClient { + client: Client, + relay_url: Url, + ohttp_keys: Arc>>>, + gateway_url: Url, + target_url: Url, +} + +impl OhttpClient { + /// Create a new OHTTP client + /// + /// # Relay URL Construction + /// + /// When making requests, the gateway URL is normalized (scheme + authority only) + /// and appended as a path component to the relay URL. This provides privacy + /// protection by only revealing the gateway's base URL to the relay. + /// + /// ## Examples + /// + /// | Relay Base | Gateway URL | Final Relay URL | + /// |------------|-------------|-----------------| + /// | `https://relay.com` | `https://dir.com/session123` | `https://relay.com/https://dir.com/` | + /// | `https://relay.com/ohttp` | `https://payjoin.xyz:8080/api` | `https://relay.com/ohttp/https://payjoin.xyz:8080/` | + /// | `https://relay.com/` | `https://dir.com` | `https://relay.com/https://dir.com/` | + /// + /// # Arguments + /// + /// * `relay_url` - The OHTTP relay that will forward requests to the gateway + /// * `ohttp_keys` - Optional pre-fetched OHTTP keys (will fetch from gateway if None) + /// * `gateway_url` - The OHTTP gateway that will decrypt and forward to the target + /// * `target_url` - The final destination for the decrypted request + pub fn new( + relay_url: Url, + ohttp_keys: Option>, + gateway_url: Url, + target_url: Url, + ) -> Self { + let client = Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .expect("Failed to build HTTP client"); + + Self { + client, + relay_url, + ohttp_keys: Arc::new(RwLock::new(ohttp_keys)), + gateway_url, + target_url, + } + } + + /// Fetch OHTTP keys from the keys source (can be different from target URL) + pub async fn fetch_keys(&self) -> Result> { + let keys_url = self.gateway_url.join("/.well-known/ohttp-gateway")?; + + tracing::debug!("Fetching OHTTP keys from: {}", keys_url); + + let response = self.client.get(keys_url).send().await?.error_for_status()?; + + let keys = response.bytes().await?; + tracing::debug!("Fetched OHTTP keys, size: {} bytes", keys.len()); + + let mut ohttp_keys = self.ohttp_keys.write().await; + + *ohttp_keys = Some(keys.to_vec()); + + Ok(keys.to_vec()) + } + + /// Construct the relay URL with normalized gateway URL as path component + /// + /// This implements the privacy protection mechanism where: + /// 1. Gateway URL is normalized to its base form (scheme + authority only) + /// 2. The normalized gateway base is appended as a path component to the relay URL + /// 3. Only scheme and authority are revealed to the relay, full path/query/fragments remain encrypted + fn construct_relay_url(&self) -> Result { + // Step 1: Normalize gateway URL to base form (scheme + authority only) + let gateway_base = self + .gateway_url + .join("/") + .map_err(|e| anyhow!("Failed to normalize gateway URL: {}", e))?; + + tracing::debug!( + "Normalized gateway URL from '{}' to '{}'", + self.gateway_url, + gateway_base + ); + + // Step 2: Manually construct the full relay URL to avoid URL.join() issues with absolute URLs + let mut full_relay_url = self.relay_url.clone(); + + // Ensure the relay path ends with a slash + let relay_path = if full_relay_url.path().ends_with('/') { + full_relay_url.path().to_string() + } else { + format!("{}/", full_relay_url.path()) + }; + + // Append the gateway URL as a path component + let new_path = format!("{}{}", relay_path, gateway_base); + full_relay_url.set_path(&new_path); + + tracing::debug!( + "Constructed relay URL: '{}' + '{}' = '{}'", + self.relay_url, + gateway_base, + full_relay_url + ); + + Ok(full_relay_url) + } + + /// Send a request using proper OHTTP encapsulation + pub async fn send_ohttp_request( + &self, + method: &str, + body: &[u8], + headers: &[(String, String)], + request_path: &str, + ) -> Result { + // Fetch OHTTP keys if not already available + let maybe_keys = { + let guard = self.ohttp_keys.read().await; + guard.clone() + }; + + let keys_data = match maybe_keys { + Some(keys) => keys, + None => self.fetch_keys().await?, + }; + + // Parse the OHTTP keys and create client request + let client_request = ohttp::ClientRequest::from_encoded_config(&keys_data) + .map_err(|e| anyhow!("Failed to decode OHTTP keys: {}", e))?; + + tracing::debug!("Created OHTTP client request"); + + // Create BHTTP request + let bhttp_request = self.create_bhttp_request(method, body, headers, request_path)?; + tracing::debug!("Created BHTTP request, size: {} bytes", bhttp_request.len()); + + // Encapsulate the request using OHTTP + let (ohttp_request, response_context) = client_request + .encapsulate(&bhttp_request) + .map_err(|e| anyhow!("Failed to encapsulate OHTTP request: {}", e))?; + + tracing::debug!( + "Encapsulated OHTTP request, size: {} bytes", + ohttp_request.len() + ); + + // Construct relay URL with normalized gateway URL as path component + let endpoint_url = self.construct_relay_url()?; + + tracing::debug!("Sending OHTTP request to: {}", endpoint_url); + + // Send the OHTTP request + let start_time = std::time::Instant::now(); + let response = self + .client + .post(endpoint_url) + .header("content-type", "message/ohttp-req") + .body(ohttp_request) + .send() + .await?; + + let elapsed = start_time.elapsed(); + + tracing::debug!( + "OHTTP response received in {:.2}ms: {} {}", + elapsed.as_millis(), + response.status(), + response.url() + ); + + // Check if we got the expected content type + let content_type = response + .headers() + .get("content-type") + .and_then(|ct| ct.to_str().ok()) + .unwrap_or(""); + + if content_type != "message/ohttp-res" { + tracing::debug!("Warning: Unexpected content type: {}", content_type); + } + + let _response_status = response.status().as_u16(); + let _response_headers = response.headers().clone(); + let ohttp_response_body = response.bytes().await?; + + tracing::debug!( + "OHTTP response body size: {} bytes", + ohttp_response_body.len() + ); + + // Decapsulate the OHTTP response + let bhttp_response = response_context + .decapsulate(&ohttp_response_body) + .map_err(|e| anyhow!("Failed to decapsulate OHTTP response: {}", e))?; + + tracing::debug!( + "Decapsulated BHTTP response, size: {} bytes", + bhttp_response.len() + ); + + // Parse the BHTTP response + let (status, headers, body) = self.parse_bhttp_response(&bhttp_response)?; + + Ok(OhttpResponse { + status, + headers, + body, + elapsed, + }) + } + + /// Create a BHTTP request from the given parameters + fn create_bhttp_request( + &self, + method: &str, + body: &[u8], + headers: &[(String, String)], + request_path: &str, + ) -> Result> { + use bhttp::Message; + + tracing::debug!("Creating BHTTP request: {} {}", method, request_path); + + // Extract proper authority from target URL (host:port only, no scheme) + let authority = if let Some(port) = self.target_url.port() { + format!( + "{}:{}", + self.target_url.host_str().unwrap_or("localhost"), + port + ) + } else { + self.target_url + .host_str() + .unwrap_or("localhost") + .to_string() + }; + + tracing::debug!( + "Using authority: {} for target: {}", + authority, + self.target_url + ); + + // Create the BHTTP message + let mut bhttp_msg = Message::request( + method.as_bytes().to_vec(), + self.target_url.scheme().as_bytes().to_vec(), // scheme from target URL + authority.as_bytes().to_vec(), // authority (host:port only) + request_path.as_bytes().to_vec(), // path + ); + + // Add headers + for (name, value) in headers { + bhttp_msg.put_header(name.as_bytes(), value.as_bytes()); + tracing::debug!("Added header: {}: {}", name, value); + } + + // Add body + if !body.is_empty() { + bhttp_msg.write_content(body); + tracing::debug!("Added body, size: {} bytes", body.len()); + } + + // Serialize to bytes + let mut bhttp_bytes = Vec::new(); + bhttp_msg + .write_bhttp(bhttp::Mode::KnownLength, &mut bhttp_bytes) + .map_err(|e| anyhow!("Failed to write BHTTP request: {}", e))?; + + Ok(bhttp_bytes) + } + + /// Parse a BHTTP response into status, headers, and body + fn parse_bhttp_response(&self, bhttp_bytes: &[u8]) -> Result<(u16, HeaderMap, Vec)> { + use bhttp::Message; + + tracing::debug!("Parsing BHTTP response, size: {} bytes", bhttp_bytes.len()); + + let mut cursor = std::io::Cursor::new(bhttp_bytes); + let bhttp_msg = Message::read_bhttp(&mut cursor) + .map_err(|e| anyhow!("Failed to read BHTTP response: {}", e))?; + + // Extract status + let status = bhttp_msg + .control() + .status() + .ok_or_else(|| anyhow!("Missing status in BHTTP response"))?; + + tracing::debug!("Parsed status: {}", u16::from(status)); + + // Extract headers + let mut headers = HeaderMap::new(); + for field in bhttp_msg.header().fields() { + let name = String::from_utf8_lossy(field.name()); + let value = String::from_utf8_lossy(field.value()); + + if let (Ok(header_name), Ok(header_value)) = ( + http::HeaderName::from_bytes(field.name()), + http::HeaderValue::from_bytes(field.value()), + ) { + headers.insert(header_name, header_value); + tracing::debug!("Parsed header: {}: {}", name, value); + } + } + + // Extract body + let body = bhttp_msg.content().to_vec(); + tracing::debug!("Parsed body, size: {} bytes", body.len()); + + Ok((status.into(), headers, body)) + } + + /// Get target information + pub async fn get_target_info(&self) -> Result { + let keys = self.fetch_keys().await?; + + Ok(TargetInfo { + target_url: self.relay_url.clone(), + keys_available: true, + keys_size: keys.len(), + }) + } +} + +#[derive(Debug)] +pub struct OhttpResponse { + pub status: u16, + pub headers: HeaderMap, + pub body: Vec, + pub elapsed: std::time::Duration, +} + +impl OhttpResponse { + /// Get response body as text + pub fn text(&self) -> Result { + String::from_utf8(self.body.clone()) + .map_err(|e| anyhow!("Failed to decode response as UTF-8: {}", e)) + } + + /// Get response body as JSON + pub fn json(&self) -> Result { + serde_json::from_slice(&self.body) + .map_err(|e| anyhow!("Failed to parse JSON response: {}", e)) + } + + /// Check if response is JSON + pub fn is_json(&self) -> bool { + self.headers + .get("content-type") + .and_then(|ct| ct.to_str().ok()) + .map(|ct| ct.contains("json")) + .unwrap_or(false) + } +} + +#[derive(Debug)] +pub struct GatewayInfo { + pub gateway_url: Url, + pub keys_available: bool, + pub keys_size: usize, +} + +#[derive(Debug)] +pub struct TargetInfo { + pub target_url: Url, + pub keys_available: bool, + pub keys_size: usize, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_authority_extraction() { + // Test with port + let target_url = Url::parse("http://127.0.0.1:8085").unwrap(); + let _client = OhttpClient::new( + target_url.clone(), + None, + target_url.clone(), + target_url.clone(), + ); + + let authority = if let Some(port) = target_url.port() { + format!("{}:{}", target_url.host_str().unwrap_or("localhost"), port) + } else { + target_url.host_str().unwrap_or("localhost").to_string() + }; + + assert_eq!(authority, "127.0.0.1:8085"); + + // Test without explicit port (default ports) + let target_url_no_port = Url::parse("https://example.com").unwrap(); + let authority_no_port = if let Some(port) = target_url_no_port.port() { + format!( + "{}:{}", + target_url_no_port.host_str().unwrap_or("localhost"), + port + ) + } else { + target_url_no_port + .host_str() + .unwrap_or("localhost") + .to_string() + }; + + assert_eq!(authority_no_port, "example.com"); + } + + #[test] + fn test_authority_does_not_include_scheme() { + let target_url = Url::parse("https://example.com:8443/some/path").unwrap(); + + let authority = if let Some(port) = target_url.port() { + format!("{}:{}", target_url.host_str().unwrap_or("localhost"), port) + } else { + target_url.host_str().unwrap_or("localhost").to_string() + }; + + // Authority should NOT include scheme or path + assert_eq!(authority, "example.com:8443"); + assert!(!authority.contains("https://")); + assert!(!authority.contains("/some/path")); + } + + #[test] + fn test_construct_relay_url() { + // Test case 1: Basic URL construction + let relay_url = Url::parse("https://relay.com").unwrap(); + let gateway_url = + Url::parse("https://payjoin-directory.com/session123?query=value#fragment").unwrap(); + let target_url = Url::parse("https://target.com").unwrap(); + + let client = OhttpClient::new(relay_url, None, gateway_url, target_url); + let result = client.construct_relay_url().unwrap(); + + assert_eq!( + result.to_string(), + "https://relay.com/https://payjoin-directory.com/" + ); + + // Test case 2: Relay with existing path + let relay_url = Url::parse("https://relay.com/ohttp").unwrap(); + let gateway_url = Url::parse("https://payjoin.xyz:8080/api").unwrap(); + let target_url = Url::parse("https://target.com").unwrap(); + + let client = OhttpClient::new(relay_url, None, gateway_url, target_url); + let result = client.construct_relay_url().unwrap(); + + assert_eq!( + result.to_string(), + "https://relay.com/ohttp/https://payjoin.xyz:8080/" + ); + + // Test case 3: Relay URL with trailing slash + let relay_url = Url::parse("https://relay.com/").unwrap(); + let gateway_url = Url::parse("https://dir.com").unwrap(); + let target_url = Url::parse("https://target.com").unwrap(); + + let client = OhttpClient::new(relay_url, None, gateway_url, target_url); + let result = client.construct_relay_url().unwrap(); + + assert_eq!(result.to_string(), "https://relay.com/https://dir.com/"); + } + + #[test] + fn test_gateway_url_normalization() { + // Test that gateway URL normalization strips path, query, and fragment + let relay_url = Url::parse("https://relay.example.com").unwrap(); + let target_url = Url::parse("https://target.com").unwrap(); + + // Test with complex gateway URL + let gateway_url = Url::parse( + "https://gateway.com:8443/some/deep/path?param1=value1¶m2=value2#section", + ) + .unwrap(); + let client = OhttpClient::new(relay_url.clone(), None, gateway_url, target_url.clone()); + let result = client.construct_relay_url().unwrap(); + + // Should normalize to just scheme + authority + assert_eq!( + result.to_string(), + "https://relay.example.com/https://gateway.com:8443/" + ); + + // Test with simple gateway URL + let gateway_url_simple = Url::parse("https://simple.gateway.com").unwrap(); + let client_simple = OhttpClient::new(relay_url, None, gateway_url_simple, target_url); + let result_simple = client_simple.construct_relay_url().unwrap(); + + assert_eq!( + result_simple.to_string(), + "https://relay.example.com/https://simple.gateway.com/" + ); + } + + #[test] + fn test_privacy_protection_verification() { + // Verify that sensitive information from gateway URL is NOT exposed to relay + let relay_url = Url::parse("https://relay.com").unwrap(); + let gateway_url = Url::parse( + "https://payjoin.com/sensitive/session/abc123?secret=token&user=alice#private", + ) + .unwrap(); + let target_url = Url::parse("https://target.com").unwrap(); + + let client = OhttpClient::new(relay_url, None, gateway_url, target_url); + let relay_request_url = client.construct_relay_url().unwrap(); + + let relay_url_str = relay_request_url.to_string(); + + // Verify only scheme and authority are included + assert!(relay_url_str.contains("https://payjoin.com/")); + + // Verify sensitive parts are NOT included + assert!(!relay_url_str.contains("sensitive")); + assert!(!relay_url_str.contains("session")); + assert!(!relay_url_str.contains("abc123")); + assert!(!relay_url_str.contains("secret=token")); + assert!(!relay_url_str.contains("user=alice")); + assert!(!relay_url_str.contains("#private")); + + // Expected format: https://relay.com/https://payjoin.com/ + assert_eq!(relay_url_str, "https://relay.com/https://payjoin.com/"); + } +} diff --git a/crates/ohttp-client/src/lib.rs b/crates/ohttp-client/src/lib.rs new file mode 100644 index 0000000000..c8ab57bdc4 --- /dev/null +++ b/crates/ohttp-client/src/lib.rs @@ -0,0 +1,3 @@ +pub mod client; + +pub use client::*; diff --git a/crates/ohttp-gateway/Cargo.toml b/crates/ohttp-gateway/Cargo.toml new file mode 100644 index 0000000000..7294195bee --- /dev/null +++ b/crates/ohttp-gateway/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "ohttp-gateway" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +description = "A generic OHTTP gateway that forwards requests to a configured backend without storing data" +keywords = ["bitcoin", "e-cash", "cashu", "ohttp"] +categories = ["cryptography::cryptocurrencies", "network-programming"] + +[[bin]] +name = "ohttp-gateway" +path = "src/bin/main.rs" + +[dependencies] +anyhow.workspace = true +axum = { workspace = true, features = ["tokio"] } +base64 = "0.22.1" +bitcoin.workspace = true +bhttp = { version = "0.6.1", features = ["http"] } +bytes = "1.9.0" +clap = { workspace = true, features = ["derive", "env"] } +futures.workspace = true +home.workspace = true +http-body-util = "0.1.3" +hyper = { version = "1.6.0", features = ["http1", "client"] } +hyper-util = { version = "0.1.16", features = ["client", "client-legacy", "tokio"] } +reqwest.workspace = true +ohttp = { package = "bitcoin-ohttp", version = "0.6.0" } +serde.workspace = true +serde_json.workspace = true +tokio = { workspace = true, features = ["full"] } +tower.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +url.workspace = true diff --git a/crates/ohttp-gateway/README.md b/crates/ohttp-gateway/README.md new file mode 100644 index 0000000000..2166c35549 --- /dev/null +++ b/crates/ohttp-gateway/README.md @@ -0,0 +1,64 @@ +# OHTTP Gateway + +A high-performance OHTTP (Oblivious HTTP) gateway that provides privacy-preserving HTTP proxying by decrypting OHTTP-encapsulated requests and forwarding them to backend services. Built with Axum for async performance and designed for production deployments. + +## Overview + +The OHTTP Gateway implements the [Oblivious HTTP specification](https://ietf-wg-ohai.github.io/oblivious-http/draft-ietf-ohai-ohttp.html) as a transparent proxy that: + +- **Decapsulates OHTTP requests**: Decrypts incoming encrypted HTTP requests +- **Forwards to backends**: Proxies decrypted requests to configured backend services +- **Encapsulates responses**: Encrypts backend responses for return to clients +- **Zero data storage**: Operates as a stateless proxy with no request logging or storage +- **High performance**: Built on Axum with async/await for concurrent request handling + +## Architecture + +``` +OHTTP Client → [OHTTP Gateway] → Backend Service + ↑ + (encrypt/decrypt with HPKE) +``` + +**Request Flow:** +1. Client sends encrypted request (`message/ohttp-req`) to `/.well-known/ohttp-gateway` +2. Gateway decrypts using private OHTTP keys +3. Gateway forwards plain HTTP request to configured backend +4. Backend processes request and returns response +5. Gateway encrypts response and returns as `message/ohttp-res` + +## Features + +- ✅ **RFC Compliant**: Full OHTTP specification implementation +- ✅ **Zero Storage**: Stateless operation with no request persistence +- ✅ **High Performance**: Async request handling with Axum +- ✅ **Automatic Key Management**: Generates and manages OHTTP keys +- ✅ **Flexible Backend**: Forward to any HTTP/HTTPS service +- ✅ **Health Monitoring**: Built-in health check endpoints +- ✅ **CORS Support**: Cross-origin resource sharing for web clients +- ✅ **Cashu Gateway Prober**: Support for gateway probing and purpose discovery + +## Endpoints + +- `POST /.well-known/ohttp-gateway` - Main OHTTP endpoint for encapsulated requests +- `GET /.well-known/ohttp-gateway` - Returns OHTTP public keys for clients +- `GET /.well-known/ohttp-gateway?allowed_purposes` - Gateway prober endpoint for Cashu opt-in detection +- `GET /ohttp-keys` - Alternative endpoint for OHTTP public keys + +## Gateway Prober Support + +The gateway implements Cashu gateway prober support. When a GET request is made to `/.well-known/ohttp-gateway?allowed_purposes`, the gateway responds with: + +- **Status**: 200 OK +- **Content-Type**: `application/x-ohttp-allowed-purposes` +- **Body**: TLS ALPN protocol list encoded containing the magic Cashu purpose string + +The encoding follows the same format as BIP77 - a U16BE count of strings followed by U8 length encoded strings: +- 2 bytes: Big-endian count of strings in the list (1 for Cashu) +- 1 byte: Length of the purpose string (42 bytes) +- 42 bytes: The magic Cashu purpose string `CASHU 2253f530-151f-4800-a58e-c852a8dc8cff` + +Example request: +```bash +curl "http://localhost:8080/.well-known/ohttp-gateway?allowed_purposes" +``` diff --git a/crates/ohttp-gateway/src/bin/main.rs b/crates/ohttp-gateway/src/bin/main.rs new file mode 100644 index 0000000000..eb737b633d --- /dev/null +++ b/crates/ohttp-gateway/src/bin/main.rs @@ -0,0 +1,63 @@ +use anyhow::Result; +use axum::routing::{get, post}; +use axum::Router; +use clap::Parser; +use ohttp_gateway::cli::Cli; +use ohttp_gateway::{gateway, key_config}; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::EnvFilter; + +#[tokio::main] +async fn main() -> Result<()> { + init_logging(); + + let cli = Cli::parse(); + + // Get work directory and construct OHTTP keys path + let work_dir = cli.get_work_dir()?; + let ohttp_keys_path = work_dir.join("ohttp_keys.json"); + + // Load or generate OHTTP keys + let ohttp = key_config::load_or_generate_keys(&ohttp_keys_path)?; + + // HTTP client is set up within the gateway handlers + + // Create the Axum app + let app = Router::new() + .route( + "/.well-known/ohttp-gateway", + post(gateway::handle_ohttp_request).get(gateway::handle_gateway_get), + ) + .route("/ohttp-keys", get(gateway::handle_ohttp_keys)) + // Catch-all route to handle any path with OHTTP requests + .fallback(gateway::handle_ohttp_request) + .layer(axum::extract::Extension(ohttp)) + .layer(axum::extract::Extension(cli.backend_url.clone())); + + // Create TCP listener + let addr = format!("0.0.0.0:{}", cli.port); + + tracing::info!("OHTTP Gateway listening on: {}", addr); + tracing::info!("Forwarding requests to: {}", cli.backend_url); + + // Run the server + let listener = tokio::net::TcpListener::bind(&addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +fn init_logging() { + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::DEBUG.into()) + .from_env_lossy() + .add_directive("hyper=info".parse().unwrap()) + .add_directive("tower_http=debug".parse().unwrap()); + + tracing_subscriber::fmt() + .with_target(false) + .with_env_filter(env_filter) + .init(); + + tracing::info!("Logging initialized"); +} diff --git a/crates/ohttp-gateway/src/cli.rs b/crates/ohttp-gateway/src/cli.rs new file mode 100644 index 0000000000..5ea84ce534 --- /dev/null +++ b/crates/ohttp-gateway/src/cli.rs @@ -0,0 +1,69 @@ +use std::path::PathBuf; + +use clap::{value_parser, Parser}; +use url::Url; + +#[derive(Debug, Parser)] +#[command( + version = env!("CARGO_PKG_VERSION"), + about = "OHTTP Gateway", + long_about = "A generic OHTTP gateway that forwards encapsulated requests to a configured backend without storing data", +)] +pub struct Cli { + /// The port to bind the gateway on + #[arg(long, short = 'p', env = "OHTTP_GATEWAY_PORT", default_value = "8080")] + pub port: u16, + + /// The backend URL to forward requests to + #[arg( + long, + env = "OHTTP_GATEWAY_BACKEND_URL", + help = "The backend URL to forward requests to", + default_value = "http://localhost:8080", + value_parser = validate_url + )] + pub backend_url: Url, + + /// The working directory where OHTTP keys will be stored + #[arg( + long = "work-dir", + env = "OHTTP_GATEWAY_WORK_DIR", + help = "The working directory where OHTTP keys will be stored", + value_parser = value_parser!(PathBuf) + )] + pub work_dir: Option, +} + +impl Cli { + /// Get the work directory, using default if not specified + pub fn get_work_dir(&self) -> anyhow::Result { + match &self.work_dir { + Some(dir) => Ok(dir.clone()), + None => { + let home_dir = home::home_dir() + .ok_or_else(|| anyhow::anyhow!("Unable to determine home directory"))?; + let dir = home_dir.join(".ohttp-gateway"); + std::fs::create_dir_all(&dir)?; + Ok(dir) + } + } + } +} + +/// Validate that the backend URL is well-formed +fn validate_url(s: &str) -> Result { + let url = Url::parse(s).map_err(|e| format!("Invalid URL '{}': {}", s, e))?; + + if url.scheme() != "http" && url.scheme() != "https" { + return Err(format!( + "URL must use http or https scheme, got: {}", + url.scheme() + )); + } + + if url.host().is_none() { + return Err("URL must have a host".to_string()); + } + + Ok(url) +} diff --git a/crates/ohttp-gateway/src/gateway.rs b/crates/ohttp-gateway/src/gateway.rs new file mode 100644 index 0000000000..88c90dd27a --- /dev/null +++ b/crates/ohttp-gateway/src/gateway.rs @@ -0,0 +1,365 @@ +use std::str::FromStr; + +use axum::http::{StatusCode, Uri}; +use axum::response::{IntoResponse, Response}; +use bytes::Bytes; +use reqwest; +use serde_json; +use url::Url; + +use crate::key_config::OhttpConfig; + +pub type BoxError = Box; + +/// Magic Cashu purpose string for gateway prober +const MAGIC_CASHU_PURPOSE: &[u8] = b"CASHU 2253f530-151f-4800-a58e-c852a8dc8cff"; + +#[derive(Debug)] +struct BackendResponse { + status: u16, + headers: Vec<(reqwest::header::HeaderName, reqwest::header::HeaderValue)>, + body: Vec, +} + +/// Handle OHTTP gateway requests +pub async fn handle_ohttp_request( + axum::extract::Extension(ohttp): axum::extract::Extension, + axum::extract::Extension(backend_url): axum::extract::Extension, + body: Bytes, +) -> Result { + tracing::trace!("Received OHTTP request, size: {}", body.len()); + + // Decapsulate the OHTTP request + let (bhttp_req, response_context) = match ohttp.server.decapsulate(&body) { + Ok(result) => result, + Err(e) => { + tracing::error!("Failed to decapsulate OHTTP request: {}", e); + return Err(GatewayError::OhttpDecapsulation); + } + }; + + // Parse the inner BHTTP request + let inner_req = match parse_bhttp_request(&bhttp_req) { + Ok(req) => req, + Err(e) => { + tracing::error!("Failed to parse BHTTP request: {}", e); + return Err(GatewayError::InvalidRequest); + } + }; + + // Forward the request to the configured backend + let response = match forward_request(&backend_url, &inner_req).await { + Ok(resp) => resp, + Err(e) => { + tracing::error!("Failed to forward request: {}", e); + return Err(GatewayError::ForwardingFailed); + } + }; + + // Convert the response back to BHTTP format + let bhttp_resp = match convert_to_bhttp_response(&response).await { + Ok(resp) => resp, + Err(e) => { + tracing::error!("Failed to convert response to BHTTP: {}", e); + return Err(GatewayError::ResponseEncodingFailed); + } + }; + + // Re-encapsulate the response + let ohttp_resp = match response_context.encapsulate(&bhttp_resp) { + Ok(resp) => resp, + Err(e) => { + tracing::error!("Failed to re-encapsulate OHTTP response: {}", e); + return Err(GatewayError::OhttpEncapsulation); + } + }; + + tracing::trace!("Sending OHTTP response, size: {}", ohttp_resp.len()); + + Ok(Response::builder() + .status(StatusCode::OK) + .header("content-type", "message/ohttp-res") + .body(axum::body::Body::from(ohttp_resp)) + .unwrap()) +} + +/// Handle requests for OHTTP keys +pub async fn handle_ohttp_keys( + axum::extract::Extension(ohttp): axum::extract::Extension, +) -> Result { + let keys = match ohttp.server.config().encode() { + Ok(keys) => keys, + Err(e) => { + tracing::error!("Failed to encode OHTTP keys: {}", e); + return Err(GatewayError::KeyEncodingFailed); + } + }; + + Ok(Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/ohttp-keys") + .body(axum::body::Body::from(keys)) + .unwrap()) +} + +/// Handle GET requests to /.well-known/ohttp-gateway +/// +/// This endpoint handles two scenarios: +/// 1. Without query params: returns OHTTP keys (standard behavior) +/// 2. With ?allowed_purposes: returns Cashu opt-in information (gateway prober) +pub async fn handle_gateway_get( + axum::extract::Extension(ohttp): axum::extract::Extension, + axum::extract::Query(params): axum::extract::Query>, +) -> Result { + tracing::debug!( + "Received GET request to /.well-known/ohttp-gateway with params: {:?}", + params + ); + + // Check if the allowed_purposes query parameter is present (gateway prober) + if params.contains_key("allowed_purposes") { + tracing::debug!("Received gateway prober request for allowed purposes"); + + // Encode the magic string in the same format as a TLS ALPN protocol list (a + // U16BE count of strings followed by U8 length encoded strings). + // + // The string is just "CASHU" followed by a UUID, that signals to relays + // that this OHTTP gateway will accept any requests associated with this + // purpose. + let mut alpn_encoded = Vec::new(); + + // Add 16-bit big-endian count of strings in the list + // We have 1 string + let num_strings = 1u16; + alpn_encoded.extend_from_slice(&num_strings.to_be_bytes()); + + // Add the Cashu purpose string with its length prefix + let purpose_len = MAGIC_CASHU_PURPOSE.len() as u8; + alpn_encoded.push(purpose_len); + alpn_encoded.extend_from_slice(MAGIC_CASHU_PURPOSE); + + tracing::debug!( + "Responding with Cashu opt-in, purpose string: {}", + String::from_utf8_lossy(MAGIC_CASHU_PURPOSE) + ); + + Ok(Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/x-ohttp-allowed-purposes") + .body(axum::body::Body::from(alpn_encoded)) + .unwrap()) + } else { + // Standard OHTTP keys request + tracing::debug!("Returning OHTTP keys"); + + let keys = match ohttp.server.config().encode() { + Ok(keys) => keys, + Err(e) => { + tracing::error!("Failed to encode OHTTP keys: {}", e); + return Err(GatewayError::KeyEncodingFailed); + } + }; + + Ok(Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/ohttp-keys") + .body(axum::body::Body::from(keys)) + .unwrap()) + } +} + +#[derive(Clone)] +pub struct InnerRequest { + pub method: String, + pub uri: String, + pub headers: Vec<(String, String)>, + pub body: Vec, +} + +#[derive(Debug)] +pub enum GatewayError { + OhttpDecapsulation, + OhttpEncapsulation, + InvalidRequest, + ForwardingFailed, + ResponseEncodingFailed, + KeyEncodingFailed, +} + +impl IntoResponse for GatewayError { + fn into_response(self) -> Response { + let (status, message) = match self { + GatewayError::OhttpDecapsulation => (StatusCode::BAD_REQUEST, "Invalid OHTTP request"), + GatewayError::OhttpEncapsulation => ( + StatusCode::INTERNAL_SERVER_ERROR, + "OHTTP encapsulation failed", + ), + GatewayError::InvalidRequest => (StatusCode::BAD_REQUEST, "Invalid inner request"), + GatewayError::ForwardingFailed => { + (StatusCode::BAD_GATEWAY, "Failed to forward request") + } + GatewayError::ResponseEncodingFailed => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Response encoding failed", + ), + GatewayError::KeyEncodingFailed => { + (StatusCode::INTERNAL_SERVER_ERROR, "Key encoding failed") + } + }; + + Response::builder() + .status(status) + .header("content-type", "application/json") + .body(axum::body::Body::from( + serde_json::to_string(&serde_json::json!({ + "error": message, + "type": "gateway_error" + })) + .unwrap_or_else(|_| "{}".to_string()), + )) + .unwrap() + } +} + +fn parse_bhttp_request(bhttp_bytes: &[u8]) -> Result { + use bhttp::Message; + + tracing::trace!("Parsing BHTTP request, size: {} bytes", bhttp_bytes.len()); + + let mut cursor = std::io::Cursor::new(bhttp_bytes); + let req = Message::read_bhttp(&mut cursor)?; + + let method = + String::from_utf8_lossy(req.control().method().ok_or("Missing method")?).to_string(); + + let scheme = req.control().scheme().unwrap_or(b"https"); + let authority = req.control().authority().unwrap_or(b""); + let path = req.control().path().unwrap_or(b"/"); + + let uri = format!( + "{}://{}{}", + String::from_utf8_lossy(scheme), + String::from_utf8_lossy(authority), + String::from_utf8_lossy(path) + ); + + tracing::info!("Gateway request: {} {}", method, uri); + tracing::trace!( + "URI components - scheme: '{}', authority: '{}', path: '{}'", + String::from_utf8_lossy(scheme), + String::from_utf8_lossy(authority), + String::from_utf8_lossy(path) + ); + + let mut headers = Vec::new(); + for header in req.header().fields() { + headers.push(( + String::from_utf8_lossy(header.name()).to_string(), + String::from_utf8_lossy(header.value()).to_string(), + )); + } + + let body = req.content().to_vec(); + tracing::trace!("Inner request body size: {} bytes", body.len()); + + Ok(InnerRequest { + method, + uri, + headers, + body, + }) +} + +async fn forward_request( + backend_url: &Url, + inner_req: &InnerRequest, +) -> Result { + // Extract path from inner request's URI for forwarding + let inner_uri = Uri::from_str(&inner_req.uri)?; + let path_and_query = inner_uri + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/"); + + // Construct backend URL with the path from the inner request + let mut backend_url_with_path = backend_url.clone(); + backend_url_with_path.set_path(path_and_query); + if let Some(query) = inner_uri.query() { + backend_url_with_path.set_query(Some(query)); + tracing::trace!("Added query parameters: '{}'", query); + } + + tracing::debug!( + "Forwarding {} {} to {}", + inner_req.method, + inner_req.uri, + backend_url_with_path + ); + tracing::trace!("Request headers: {:?}", inner_req.headers); + tracing::trace!("Request body size: {} bytes", inner_req.body.len()); + + // Use reqwest for the actual HTTP request (simpler than hyper's low-level API) + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::limited(10)) + .build()?; + + let mut req_builder = client.request( + reqwest::Method::from_str(&inner_req.method)?, + backend_url_with_path.as_str(), + ); + + // Add headers from the inner request + for (name, value) in &inner_req.headers { + req_builder = req_builder.header(name, value); + } + + // Add body if present + let request = if inner_req.body.is_empty() { + req_builder.build()? + } else { + req_builder.body(inner_req.body.clone()).build()? + }; + + let response = client.execute(request).await?; + let status = response.status(); + let headers = response.headers().clone(); + let body_bytes = response.bytes().await?; + + tracing::debug!("Backend response: {}", status); + tracing::trace!("Response headers: {:?}", headers); + tracing::trace!("Response body size: {} bytes", body_bytes.len()); + + // Create a simple response structure for processing + let backend_response = BackendResponse { + status: status.as_u16(), + headers: headers + .into_iter() + .filter_map(|(k, v)| k.map(|key| (key, v.clone()))) + .collect(), + body: body_bytes.to_vec(), + }; + + Ok(backend_response) +} + +async fn convert_to_bhttp_response(resp: &BackendResponse) -> Result, BoxError> { + use bhttp::{Message, StatusCode as BhttpStatus}; + + let status_code = BhttpStatus::try_from(resp.status).map_err(|_| "Invalid status code")?; + + let mut bhttp_resp = Message::response(status_code); + + // Add response headers + for (name, value) in &resp.headers { + bhttp_resp.put_header(name.as_str(), value.to_str()?); + } + + // Write the response body + bhttp_resp.write_content(&resp.body); + + let mut bhttp_bytes = Vec::new(); + bhttp_resp.write_bhttp(bhttp::Mode::KnownLength, &mut bhttp_bytes)?; + + Ok(bhttp_bytes) +} diff --git a/crates/ohttp-gateway/src/key_config.rs b/crates/ohttp-gateway/src/key_config.rs new file mode 100644 index 0000000000..6418821f23 --- /dev/null +++ b/crates/ohttp-gateway/src/key_config.rs @@ -0,0 +1,83 @@ +use std::fs::File; +use std::io::Write; +use std::path::Path; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone)] +pub struct OhttpConfig { + pub server: ohttp::Server, +} + +#[derive(Serialize, Deserialize)] +struct KeyPair { + ikm: [u8; 32], +} + +impl OhttpConfig { + pub fn generate_new() -> Result { + let _ikm = bitcoin::key::rand::random::<[u8; 32]>(); + let config = ohttp::KeyConfig::new( + 1, + ohttp::hpke::Kem::K256Sha256, + vec![ohttp::SymmetricSuite::new( + ohttp::hpke::Kdf::HkdfSha256, + ohttp::hpke::Aead::ChaCha20Poly1305, + )], + )?; + Ok(OhttpConfig { + server: ohttp::Server::new(config)?, + }) + } + + pub fn load_from_file>(path: P) -> Result { + let data = std::fs::read_to_string(path)?; + let keys: KeyPair = serde_json::from_str(&data)?; + + let config = ohttp::KeyConfig::derive( + 1, + ohttp::hpke::Kem::K256Sha256, + vec![ohttp::SymmetricSuite::new( + ohttp::hpke::Kdf::HkdfSha256, + ohttp::hpke::Aead::ChaCha20Poly1305, + )], + &keys.ikm, + ) + .map_err(|e| anyhow::anyhow!("Failed to derive OHTTP keys from file: {}", e))?; + + Ok(OhttpConfig { + server: ohttp::Server::new(config)?, + }) + } + + pub fn save_to_file>(&self, path: P) -> Result<()> { + // For now, just save an empty IKM - we'll generate each time for simplicity + let _ikm = bitcoin::key::rand::random::<[u8; 32]>(); + let keys = KeyPair { ikm: _ikm }; + + let data = serde_json::to_string_pretty(&keys)?; + let mut file = File::create(path)?; + file.write_all(data.as_bytes())?; + + Ok(()) + } +} + +pub fn generate_and_save_keys>(key_file: P) -> Result { + let config = OhttpConfig::generate_new()?; + config.save_to_file(&key_file)?; + tracing::info!( + "Generated new OHTTP keys and saved to {:?}", + key_file.as_ref() + ); + Ok(config) +} + +pub fn load_or_generate_keys>(key_file: P) -> Result { + if key_file.as_ref().exists() { + OhttpConfig::load_from_file(&key_file) + } else { + generate_and_save_keys(key_file) + } +} diff --git a/crates/ohttp-gateway/src/lib.rs b/crates/ohttp-gateway/src/lib.rs new file mode 100644 index 0000000000..10d20671cf --- /dev/null +++ b/crates/ohttp-gateway/src/lib.rs @@ -0,0 +1,12 @@ +pub mod cli; +pub mod gateway; +pub mod key_config; +pub mod router; + +// Re-exports for easier access +pub use cli::*; +pub use gateway::*; +pub use key_config::*; +pub use router::*; + +pub type BoxError = Box; diff --git a/crates/ohttp-gateway/src/router.rs b/crates/ohttp-gateway/src/router.rs new file mode 100644 index 0000000000..ecfa5c0e35 --- /dev/null +++ b/crates/ohttp-gateway/src/router.rs @@ -0,0 +1,38 @@ +use std::path::Path; + +use anyhow::{anyhow, Result}; +use axum::routing::post; +use axum::Router; +use url::Url; + +use crate::{gateway, key_config}; + +/// Creates an OHTTP gateway router that forwards encapsulated requests to the specified backend +pub fn create_ohttp_gateway_router>( + backend_url: &str, + ohttp_keys_path: P, +) -> Result { + // Parse and validate the backend URL + let backend_url = Url::parse(backend_url) + .map_err(|e| anyhow!("Failed to parse backend URL '{}': {}", backend_url, e))?; + + tracing::info!("Creating OHTTP gateway router"); + tracing::info!("Backend URL: {}", backend_url); + tracing::info!("OHTTP keys file: {:?}", ohttp_keys_path.as_ref()); + + // Load or generate OHTTP keys + let ohttp = key_config::load_or_generate_keys(&ohttp_keys_path)?; + + // Create the router with OHTTP gateway endpoints + let router = Router::new() + .route( + "/.well-known/ohttp-gateway", + post(gateway::handle_ohttp_request).get(gateway::handle_gateway_get), + ) + .layer(axum::extract::Extension(ohttp)) + .layer(axum::extract::Extension(backend_url)); + + tracing::info!("OHTTP gateway router created successfully"); + + Ok(router) +}