Skip to content

Deliver NUT-18 payments over the configured transport - #2530

Merged
thesimplekid merged 2 commits into
cashubtc:mainfrom
crodas:fix/nut18-delivery-transport
Oct 2, 2026
Merged

thesimplekid merged 2 commits into
cashubtc:mainfrom
crodas:fix/nut18-delivery-transport

Conversation

@crodas

@crodas crodas commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Description

Fixes #2525

HTTP delivery built a fresh default client, so a wallet configured with --proxy or Tor still reached the recipient over ordinary network egress. The recipient learned the payer's address even though every mint request went through the configured transport.

Delivery now rides the wallet's mint connector, alongside the other non-mint calls it already makes. A connector without delivery support fails explicitly rather than falling back to a direct request.

The receiver's response is now parsed as JSON, so a success answered with an empty body is reported as a failure. NUT-18 does not define a response body. Delivery also stops following redirects and is paced by the client rate limiter, both inherited from the connector transport.


Notes to the reviewers


Suggested CHANGELOG Updates

CHANGED

ADDED

REMOVED

FIXED


Checklist

  • I followed the code style guidelines
  • I ran just quick-check before committing
  • If the Wallet API was modified (added/removed/changed), I have reflected those changes in the FFI bindings (crates/cdk-ffi)

@crodas crodas self-assigned this Sep 10, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in CDK Sep 10, 2026
@crodas

crodas commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

@cdk-bot review

@cdk-bot cdk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified findings approved for disclosure:

  • NUT-18 HTTP delivery misreports success when receiver replies with an empty or non-JSON body (medium) - see inline comment
    Unanchored locations included in summary:
    • crates/cdk/src/wallet/payment_request.rs:455
  • NUT-18 HTTP delivery no longer follows HTTP redirects (medium) - see inline comment
  • NUT-18 delivery now rides a transport that may have TLS certificate verification disabled, exposing ecash proofs to MITM on attacker-supplied receiver URLs (low) - see inline comment
    Unanchored locations included in summary:
    • crates/cdk/src/wallet/payment_request.rs:455

Comment thread crates/cdk/src/wallet/mint_connector/http_client.rs
Comment thread crates/cdk/src/wallet/mint_connector/http_client.rs Outdated
Comment thread crates/cdk/src/wallet/mint_connector/http_client.rs
@crodas
crodas force-pushed the fix/nut18-delivery-transport branch from 8badcb3 to 09aff6e Compare September 10, 2026 23:05
@crodas

crodas commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

@cdk-bot review

@cdk-bot cdk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified findings approved for disclosure:

  • NUT-18 TLS-verification refusal is bypassable when the connector is built via with_transport/with_shared_transport with a pre-configured transport (medium) - Wallets whose mint connector is built via the public HttpClient::with_transport/with_shared_transport (or new) constructors around a transport pre-configured through the public Transport::with_proxy(proxy, _, accept_invalid_certs=true) (supported by the reqwest backend only; bitreq/wasm reject the flag at build time) end up with tls_verification_disabled == false even though the transport accepts any certificate. post_payment_request_payload then delivers spendable NUT-18 proofs over HTTPS to a receiver URL chosen by the payment request on an unverified connection, defeating the refusal the PR adds; a network MITM can capture the bearer proofs and redeem them before the receiver. Pre-PR delivery used a fresh, certificate-verifying client, so this exposure is new. All in-repo construction paths keep the flag accurate; the gap is external embedders using the pre-built-transport constructors.
    Unanchored locations included in summary:
    • crates/cdk/src/wallet/mint_connector/http_client.rs:200
    • crates/cdk/src/wallet/mint_connector/http_client.rs:257
    • crates/cdk-http-client/src/transport/mod.rs:104
  • NUT-18 HTTP POST delivery now hard-fails for wallets built with a custom mint connector (low) - Wallets built with a custom MintConnector implementation (public extension points: WalletBuilder::client/shared_client, WalletConfig.mint_connector, Wallet::set_client) inherit the new trait default Err(PaymentRequestDeliveryUnsupported) for post_payment_request_payload, with no direct-request fallback. For that embedder class, NUT-18 payment requests that only advertise an HttpPost transport — which worked on base for any connector — now fail 100% of the time at runtime (no compile-time signal). The failure surfaces in PreparedPaymentRequest::confirm() after confirm_send has already swapped the proofs into a pending send, so the payer must call revoke_send with the returned operation id to reclaim funds, even though the delivery capability was deterministically absent before any funds moved. Behavior is documented and intentional per the PR's own docs and test, so this is disclosed as a sign-off/migration/fail-fast note rather than an unintended bug.
    Unanchored locations included in summary:
    • crates/cdk/src/wallet/payment_request.rs:455
  • Rewritten custom-transport example follows redirects, silently reintroducing the "redirected POST reported as delivered" failure the PR fixes (low) - The canonical custom-transport example (crates/cdk/examples/mint-token-bolt12-with-custom-http.rs, rewritten by this PR to the new Transport::http_post shape and wired into the wallet via .client(...)) configures its ureq 3.4.0 agent without a redirect policy, so it follows up to 10 redirects by default and converts a POST answered 301/302/303 into a bodyless GET. Since this PR routes NUT-18 HttpPost delivery through the connector transport, a wallet built by copying the example reports PaymentRequestPayload delivery as successful whenever a receiver answers 301/302 (e.g., trailing-slash normalization or http→https upgrade) and the redirect target returns 2xx to the GET: the proofs never reach the receiver, no error prompts the payer, and the swapped proofs sit in the pending/reserved state until someone notices and calls revoke_send. A hostile receiver can also use this to fake successful delivery. The failure is confined to wallets using this example's transport pattern (or any other redirect-following custom transport); the library's built-in Async/TorAsync transports are unaffected because the PR pins them with no_redirects().
    Unanchored locations included in summary:
    • crates/cdk/examples/mint-token-bolt12-with-custom-http.rs:32

Comment thread crates/cdk/src/wallet/mint_connector/http_client.rs
Comment thread crates/cdk/src/wallet/mint_connector/mod.rs
Comment thread crates/cdk/examples/mint-token-bolt12-with-custom-http.rs
@codecov

codecov Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.65385% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.01%. Comparing base (e294f84) to head (1bc2ce0).

Files with missing lines Patch % Lines
...rates/cdk/src/wallet/mint_connector/http_client.rs 93.61% 23 Missing ⚠️
crates/cdk-http-client/src/transport/mod.rs 72.72% 6 Missing ⚠️
crates/cdk/src/wallet/mint_connector/mod.rs 40.00% 3 Missing ⚠️
crates/cdk-http-client/src/error.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2530      +/-   ##
==========================================
+ Coverage   79.88%   80.01%   +0.12%     
==========================================
  Files         387      387              
  Lines      106600   107093     +493     
==========================================
+ Hits        85158    85689     +531     
+ Misses      21442    21404      -38     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@crodas
crodas force-pushed the fix/nut18-delivery-transport branch from 09aff6e to 56043c4 Compare September 11, 2026 14:38
@crodas

crodas commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

@cdk-bot review

@cdk-bot cdk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified findings approved for disclosure:

  • RateLimitedTransport doesn't override tls_verification_disabled, so its default false bypasses the new unverified-TLS refusal for NUT-18 proof delivery and LNURL fetches (medium) - A wallet client built as HttpClient::with_transport(mint, RateLimitedTransport::with_manager(unverified_transport, limiter), ...) — the composition the exported RateLimitedHttpClient/RateLimitedTorHttpClient aliases exist for — reports TLS verification enabled while all HTTPS requests run unverified. NUT-18 proof delivery (bearer e-cash) to https receivers and LNURL invoice fetches then pass ensure_verified_tls, exposing proofs/invoices to a MITM who terminates TLS with any certificate.
    Additional locations included in summary:
    • crates/cdk/src/wallet/mint_connector/http_client.rs:179
    • crates/cdk/src/wallet/mint_connector/http_client.rs:528
      Unanchored locations included in summary:
    • crates/cdk-common/src/rate_limit.rs:772

Comment thread crates/cdk-http-client/src/transport/mod.rs
@crodas

crodas commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

@cdk-bot review

@cdk-bot cdk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified findings approved for disclosure:

  • NUT-18 unverified-TLS refusal fires only at confirm time, after proofs are reserved and swap fees paid (medium) - For a wallet whose transport has TLS certificate verification disabled (WalletRepository::danger_accept_invalid_certs + proxy, HttpClient::with_proxy(..., true), or an embedder-supplied transport reporting tls_verification_disabled() — all paths this PR propagates), any HTTP-POST-only NUT-18 request with an https target deterministically fails inside confirm() after confirm_send has already run the mint swap: swap input fees are consumed and the token is left as a pending send that must be discovered and reclaimed with revoke_send (which rides another swap). The unambiguous refusal is wrapped as PaymentRequestDeliveryFailed, whose doc tells the payer to treat it as an ambiguous outcome, and retrying can never succeed. No fund theft — proofs are reclaimable — but this is exactly the fee-loss-plus-stuck-pending-send outcome the PR's prepare-time capability check (commit 528f9ce) was added to prevent, and a regression vs. base where delivery used a fresh verifying client and succeeded.
    Additional locations included in summary:
    • crates/cdk/src/wallet/mint_connector/http_client.rs:519
    • crates/cdk/src/wallet/mint_connector/mod.rs:102

Comment thread crates/cdk/src/wallet/payment_request.rs Outdated
@crodas
crodas force-pushed the fix/nut18-delivery-transport branch 2 times, most recently from bf95c28 to f42271c Compare September 12, 2026 16:08
@crodas

crodas commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

@cdk-bot review

@crodas
crodas force-pushed the fix/nut18-delivery-transport branch from f42271c to c943dfc Compare September 13, 2026 05:01
@crodas

crodas commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

@cdk-bot review

@cdk-bot cdk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified findings approved for disclosure:

  • NUT-18 pre-check lets parsable non-http(s) receiver targets through, so the swap still pays the input fee first (low) - For an HTTP-only NUT-18 request whose target parses as a URL but uses a scheme the HTTP transport cannot speak (e.g. ftp://, ws://, mailto:), the new pre-check passes, the swap at confirm() pays the mint input fee, and only then does delivery fail — leaving a pending send that must be revoked. This is exactly the outcome the PR's commits and the new trait docs promise to prevent, and the non-http scheme is knowable up front for a transport whose only verbs are http_get/http_post/http_post_form.
    Additional locations included in summary:
    • crates/cdk/src/wallet/payment_request.rs:328
  • On wasm32 a redirecting NUT-18 receiver is misreported as a fetch failure, so the new Transport redirect contract (and PaymentRequestDeliveryRedirected) is unreachable (low) - On wasm32, when a NUT-18 receiver answers a delivery with a 3xx, the payer gets Error::HttpError(None, "Fetch failed: …") wrapped in PaymentRequestDeliveryFailed instead of Error::PaymentRequestDeliveryRedirected and its guidance that the receiver's final URL must be obtained from the receiver. Funds safety is identical (both classifications are non-definitive; the pending-send/revoke guidance applies either way), so this is a diagnostics/contract gap limited to wasm builds, not a funds risk.
    Additional locations included in summary:
    • crates/cdk/src/wallet/mint_connector/http_client.rs:529
      Unanchored locations included in summary:
    • crates/cdk-http-client/src/backends/wasm_backend.rs:159
    • crates/cdk-http-client/src/backends/wasm_backend.rs:187

Comment thread crates/cdk/src/wallet/mint_connector/http_client.rs
Comment thread crates/cdk-http-client/src/transport/mod.rs
@thesimplekid thesimplekid added this to the 0.19.0 milestone Sep 16, 2026
@crodas
crodas force-pushed the fix/nut18-delivery-transport branch 2 times, most recently from 1fc6eb0 to 8b93bed Compare September 17, 2026 23:25
@crodas

crodas commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

@cdk-bot review

@Codepocketdev

Copy link
Copy Markdown

Code review ACK 8b93bed

On ambiguous delivery failure, revoke_send reclaims but there's no path to retry delivery of the same token — is that intended, or is retry planned as follow-up?

@crodas

crodas commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

@cdk-bot review

@crodas

crodas commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

On ambiguous delivery failure, revoke_send reclaims but there's no path to retry delivery of the same token — is that intended, or is retry planned as follow-up?

It is unrelated, if it is an issue it should be addresed in a follow-up

Comment thread crates/cdk/src/wallet/payment_request.rs
@github-project-automation github-project-automation Bot moved this from Backlog to In progress in CDK Sep 22, 2026
@crodas
crodas force-pushed the fix/nut18-delivery-transport branch from 8b93bed to bb69aed Compare September 29, 2026 03:07
@crodas

crodas commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator Author

@cdk-bot review

@thesimplekid
thesimplekid force-pushed the fix/nut18-delivery-transport branch from bb69aed to ed3beec Compare October 1, 2026 09:20
crodas and others added 2 commits October 1, 2026 09:21
BREAKING CHANGE: HttpTransport GET and POST methods now return raw
responses. Update custom implementations to the new signatures and
use json_or_status_error when typed responses are needed.
Check payment-method eligibility and balance before retaining a delivery
refusal during wallet selection. Underfunded wallets no longer mask
InsufficientFunds with an unrelated connector error.

Cover empty and underfunded unsupported wallets alongside an underfunded
deliverable wallet in a regression test.
@thesimplekid
thesimplekid force-pushed the fix/nut18-delivery-transport branch from ed3beec to 73ef05d Compare October 1, 2026 09:21
@thesimplekid

Copy link
Copy Markdown
Collaborator

Squashed, rebased and added on commit to keep the low balance error.

@thesimplekid
thesimplekid merged commit 73ef05d into cashubtc:main Oct 2, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

NUT-18 HTTP payment delivery ignores wallet proxy/Tor settings

4 participants