fix: domain-bind wallet signature challenges (#118) - #124
Open
Marvelg256 wants to merge 1 commit into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔗 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: ' + noncefallback — 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 viaPOST /auth/nonceand 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:
generateNonce()now issues a deterministic envelope —domain,address(wallet),statement,uri,version,nonce,issuedAt,expirationTime,networkPassphrase— and returns it as a newmessagefield on the nonce response.issued_atandmessage_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_MISMATCHotherwise).signatureType: 'sep0043'verifies per SEP-53 (signature overSHA-256("Stellar Signed Message:\n" + envelope), which is what Freighter'ssignMessage()produces) with strict checks: envelopedomain== our host,urimatches, network passphrase matches, and the envelope is not expired.signatureType: 'envelope'verifies raw Ed25519 over the envelope UTF-8 text.AUTH_ALLOW_LEGACY_RAW_SIGNATURES(defaulttruefor mobile-client compatibility) with a documented sunset of 2026-10-31. When disabled, legacy requests fail withAUTH_LEGACY_SIGNATURE_DISABLED.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— addissued_at,message_hashtononcessrc/modules/auth/auth.service.ts— canonical envelope issuance, hash-bound verification, SEP-53 browser verification, flag-gated legacy pathsrc/modules/auth/dto/verify-request.dto.ts—signatureTypegains'envelope'; optionalmessagefieldsrc/modules/auth/dto/nonce-response.dto.ts— nonce response gainsmessageAUTH_CHALLENGE_DOMAIN,AUTH_ALLOW_LEGACY_RAW_SIGNATURESread viaConfigService; documented indocs/setup/environment-variables.md+.env.examplesep0043/envelopehappy paths, wrong-domain / wrong-network rejection, tampered message, expired envelope, rows without a stored hash, single-use replaydocs/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)no-explicit-anyerrors inaudit.interceptor.ts/transaction-status-checker.processor.tsare unrelated)npm run test:e2e) requires Supabase credentials and is not part of CI🗒️ Additional Notes
"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 againststellar-sdk(hash-then-sign) before implementation. Thesep0043string is kept in the API contract for backward compatibility.ConfigServiceinAuthService(same pattern asSTELLAR_NETWORK_PASSPHRASEelsewhere) rather thansrc/config/env.ts, which is purpose-built for the admin-wallet allowlist.