Skip to content

feat(api-server): enforce request signing on Soroban write endpoints - #955

Merged
fejilaup-cloud merged 2 commits into
AtomicIP:mainfrom
Sundayabel222:feat/enforce-request-signing
Aug 29, 2026
Merged

feat(api-server): enforce request signing on Soroban write endpoints#955
fejilaup-cloud merged 2 commits into
AtomicIP:mainfrom
Sundayabel222:feat/enforce-request-signing

Conversation

@Sundayabel222

@Sundayabel222 Sundayabel222 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Closes #858


Summary

The request-signing middleware from #535 (request_signing.rs) was defined and unit-tested but never attached to a single route — the server accepted unsigned requests on every endpoint. This PR wires verify_request_signature onto the six write endpoints that will submit signed transactions to Soroban, in both the production router and the test router:

  • POST /ip/commit (commit_ip)
  • POST /ip/transfer (transfer_ip)
  • POST /swap/initiate (initiate_swap)
  • POST /swap/{swap_id}/accept (accept_swap)
  • POST /swap/{swap_id}/reveal (reveal_key)
  • POST /swap/{swap_id}/cancel (cancel_swap)

All six now require valid X-Signature, X-Timestamp, and X-Public-Key headers (timestamp within 5 minutes, Stellar-format public key). Read endpoints, verify_commitment, and the batch/bulk variants are intentionally not signed.

Motivation

Request signing was verified as enforced on zero of the six endpoints rather than a subset — the middleware existed but had no call site. grep for verify_request_signature found only its definition. Since these are the endpoints that submit signed transactions to Soroban, unsigned requests should not reach them.

Changes

  • api-server/src/main.rs:
    • Added verify_request_signature as a per-route layer on the six write endpoints in main() (production) and build_app() (test router).
    • Added test_signed_write_endpoints_reject_unsigned_requests: each of the six endpoints returns 401 for an unsigned POST.
    • Added test_signed_write_endpoint_accepts_valid_signature: a correctly signed request passes the middleware and reaches the handler.
    • Repaired pre-existing breaks in main.rs that prevented the crate from compiling (found while wiring; see below).

Pre-existing repairs included (required for the crate to build)

  • AppState lost its rpc_client field — the FromRef impl used by get_swap's State<Arc<dyn SorobanRpcClient>> extractor referenced state.rpc_client which didn't exist. Restored the field and populated it in main(), build_app(), and the test helper.
  • build_app() had a duplicate/broken let state = AppState { ... } (one referenced an undefined rpc_client and moved schema/health_checker twice). Rebuilt around a single valid state.
  • app_with_rpc_client was called by the swap read-path tests but never defined. Added it to the test module.
  • Duplicate /ip/owner/{owner}/cursor routes (in both routers) would panic axum at router construction ("overlapping method route"). Removed the duplicates.

Known limitation (follow-up)

The current scheme is sha256(method || path || timestamp || body_hash) — the doc comment claims Stellar keypair signing, but no secret material is used, so a signature is forgeable by anyone who observes a request (the public key travels in the request itself). Enforcing it still rejects unsigned traffic, but genuine authentication requires real ed25519 verification against the Stellar keypair. Suggested follow-up: replace generate_signature/verify_signature with ed25519-dalek and verify against the decoded G... public key. The header contract (X-Signature/X-Timestamp/X-Public-Key) is unchanged, so clients won't break twice.

Testing

  • Added the two middleware-enforcement tests above (six endpoints × unsigned → 401; signed → handler reached).
  • request_signing.rs unit tests are untouched and continue to pass.
  • Note: cargo is not available in the environment where this change was prepared, so it was reviewed by inspection; CI will compile and run the suite.

Related

The request_signing middleware (AtomicIP#535) was defined but never attached to a
route, so every endpoint accepted unsigned requests. Wire it onto the six
write endpoints that submit signed transactions to Soroban (commit_ip,
transfer_ip, initiate_swap, accept_swap, reveal_key, cancel_swap) in both
the production and test routers, and add tests proving all six reject
unsigned requests and accept a valid signature.

Also repair pre-existing breaks in main.rs that prevented the crate from
compiling: restore the rpc_client field on AppState (referenced by the
FromRef impl used by get_swap), rebuild build_app() around a single valid
AppState, define the app_with_rpc_client test helper the swap read-path
tests already call, and remove the duplicated /ip/owner/{owner}/cursor
routes that would panic axum at router construction.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@drips-wave

drips-wave Bot commented Aug 29, 2026

Copy link
Copy Markdown

@Sundayabel222 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@fejilaup-cloud
fejilaup-cloud merged commit 7fa2866 into AtomicIP:main Aug 29, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add request_signing.rs coverage for write endpoints

2 participants