Skip to content

fix: domain-bind wallet signature challenges (#118) - #124

Open
Marvelg256 wants to merge 1 commit into
StepFi-app:mainfrom
Marvelg256:security/118-domain-bound-signatures
Open

fix: domain-bind wallet signature challenges (#118)#124
Marvelg256 wants to merge 1 commit into
StepFi-app:mainfrom
Marvelg256:security/118-domain-bound-signatures

Conversation

@Marvelg256

Copy link
Copy Markdown

🔗 Related Issue

Closes #118


🔖 Title

Domain-bind wallet signature challenges — eliminate cross-service signature replay


📝 Description

AuthService.verifySignature() previously accepted two ad-hoc message formats — raw Ed25519 over the bare nonce hex, and the 'Stellar Signing Key: ' + nonce fallback — neither of which binds a signature to StepFi. An attacker who captured a (nonce, signature) pair from any other dApp, phishing prompt, or service using an identical raw scheme could self-issue the same nonce via POST /auth/nonce and authenticate as the victim. Because the server tried formats in order, the weakest accepted format defined the security floor.

This PR makes every accepted signature provably sign a StepFi-bound challenge:

  • Canonical challenge envelope. generateNonce() now issues a deterministic envelope — domain, address (wallet), statement, uri, version, nonce, issuedAt, expirationTime, networkPassphrase — and returns it as a new message field on the nonce response.
  • Hash-bound nonce rows. Each nonce row stores issued_at and message_hash (SHA-256 of the exact challenge text). Verification only ever runs against a message whose digest matches the stored hash — never against client-supplied alternatives (AUTH_CHALLENGE_MISMATCH otherwise).
  • Strict browser verification. signatureType: 'sep0043' verifies per SEP-53 (signature over SHA-256("Stellar Signed Message:\n" + envelope), which is what Freighter's signMessage() produces) with strict checks: envelope domain == our host, uri matches, network passphrase matches, and the envelope is not expired.
  • Native clients. signatureType: 'envelope' verifies raw Ed25519 over the envelope UTF-8 text.
  • Legacy deprecated. The raw-nonce scheme is gated behind AUTH_ALLOW_LEGACY_RAW_SIGNATURES (default true for mobile-client compatibility) with a documented sunset of 2026-10-31. When disabled, legacy requests fail with AUTH_LEGACY_SIGNATURE_DISABLED.
  • One scheme per request. The server selects exactly one format via signatureType; the old multi-format fallback is removed.

Compatibility choice (per issue ground rules): documented migration window. The legacy scheme stays accepted by default so current StepFi-App builds keep working, and the window is documented in docs/setup/environment-variables.md, .env.example, and the progress tracker.


🔄 Changes Made

  • supabase/migrations/20260825000000_add_nonce_message_binding.sql — add issued_at, message_hash to nonces
  • src/modules/auth/auth.service.ts — canonical envelope issuance, hash-bound verification, SEP-53 browser verification, flag-gated legacy path
  • src/modules/auth/dto/verify-request.dto.tssignatureType gains 'envelope'; optional message field
  • src/modules/auth/dto/nonce-response.dto.ts — nonce response gains message
  • Config plumbing — AUTH_CHALLENGE_DOMAIN, AUTH_ALLOW_LEGACY_RAW_SIGNATURES read via ConfigService; documented in docs/setup/environment-variables.md + .env.example
  • Unit tests — legacy rejected once flag off, sep0043/envelope happy paths, wrong-domain / wrong-network rejection, tampered message, expired envelope, rows without a stored hash, single-use replay
  • E2E tests — canonical envelope + SEP-53 flows, tampered / foreign-domain rejection
  • Docs — docs/api/endpoints.md, docs/setup/environment-variables.md, .env.example, context/progress-tracker.md, context/architecture-context.md, SECURITY.md

🧪 Verification

  • npm run build
  • npm test ✅ — 353 tests passing (up from 344; none removed)
  • Lint: no new issues in touched files (7 pre-existing no-explicit-any errors in audit.interceptor.ts / transaction-status-checker.processor.ts are unrelated)
  • E2E suite (npm run test:e2e) requires Supabase credentials and is not part of CI

🗒️ Additional Notes

  • SEP-0043 vs SEP-53 naming. The issue refers to the browser flow as "SEP-0043"; the actual signing scheme Freighter implements is SEP-53 ("Stellar Signed Message:\n" prefix + SHA-256), and the envelope structure follows the EIP-4361-style fields the issue describes (domain, URI, version, issued-at, expiration). The SEP-53 test vector was verified empirically against stellar-sdk (hash-then-sign) before implementation. The sep0043 string is kept in the API contract for backward compatibility.
  • Config placement. The legacy flag and challenge-domain config are read via ConfigService in AuthService (same pattern as STELLAR_NETWORK_PASSPHRASE elsewhere) rather than src/config/env.ts, which is purpose-built for the admin-wallet allowlist.

The nonce challenge was not domain-bound: verifySignature accepted raw
signatures over the bare nonce hex and a "Stellar Signing Key:" fallback,
so a (nonce, signature) pair captured from any other context could be
replayed against StepFi. Every accepted signature now signs a canonical
StepFi challenge envelope (domain, address, uri, version, nonce, issuedAt,
expirationTime, networkPassphrase); the nonce row stores a SHA-256 digest
of the exact message and verification only ever runs against that message.
Browser wallets verify per SEP-53; the legacy raw-nonce scheme is
deprecated behind AUTH_ALLOW_LEGACY_RAW_SIGNATURES with a 2026-10-31
sunset. Migration: 20260825000000_add_nonce_message_binding.sql.

Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
@Marvelg256
Marvelg256 requested a review from EmeditWeb as a code owner August 25, 2026 15:00
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.

critical: signature challenge is not domain-bound — raw-nonce and SEP-0043 fallback accept cross-service signature replay

1 participant