Problem
AuthService.verifySignature() (src/modules/auth/auth.service.ts, lines 112–132) accepts TWO different message formats:
- Raw Ed25519 over
Buffer.from(dto.nonce) — the nonce alone, hex bytes.
- SEP-0043 fallback:
'Stellar Signing Key: ' + dto.nonce — string concatenation, no domain, no URI, no timestamp.
Neither challenge binds the signature to StepFi. A signature over a raw 64-char hex nonce is trivially obtainable in other contexts (any dApp that asks a user to sign arbitrary data, phishing prompts, other services using the identical raw scheme). An attacker who captures one (nonce, signature) pair valid under either format can authenticate as the victim on StepFi as long as they can insert the same nonce into the nonces table — which they control via generateNonce(): generate a nonce, harvest a matching signature elsewhere, then self-issue the challenge here. The server picks which format to try, so the weakest accepted format defines the security floor.
SEP-0043 exists precisely to fix this: its envelope includes domain, URI, version, issued-at, and expiration. The current implementation uses none of that structure.
Ground Rules
- Read context/architecture-context.md, context/code-standards.md, context/progress-tracker.md in full
- Read the full auth flow:
generateNonce, verifySignature, generateTokens (auth.service.ts)
- Preserve mobile-client compatibility OR provide an explicit, documented migration window — state your choice in the PR
What To Build
- Define a canonical StepFi challenge envelope containing: domain (API origin), wallet, nonce, issued-at, expires-at — signed as UTF-8 text.
- Implement strict SEP-0043 verification for browser wallets (validate domain == our host, URI, not-expired).
- For native clients, adopt the canonical envelope and deprecate the raw-hex format behind a config flag with a sunset date.
- Bind nonce rows to their challenge content: store a hash of the exact expected message alongside the nonce and verify the signature against the stored message only — never against client-supplied alternatives.
- Tests: legacy-format signatures fail once flag off; SEP-0043 happy path passes; wrong-domain SEP-0043 rejected; expired envelope rejected; replay across environments impossible.
Files To Touch
src/modules/auth/auth.service.ts
src/modules/auth/dto/verify-request.dto.ts
src/config/env.ts / config plumbing for the flag
- tests
- relevant docs/progress tracker
Acceptance Criteria
Mandatory Checks Before Opening PR
Standard checklist applies. PRs failing any check will be closed without review.
Problem
AuthService.verifySignature()(src/modules/auth/auth.service.ts, lines 112–132) accepts TWO different message formats:Buffer.from(dto.nonce)— the nonce alone, hex bytes.'Stellar Signing Key: ' + dto.nonce— string concatenation, no domain, no URI, no timestamp.Neither challenge binds the signature to StepFi. A signature over a raw 64-char hex nonce is trivially obtainable in other contexts (any dApp that asks a user to sign arbitrary data, phishing prompts, other services using the identical raw scheme). An attacker who captures one
(nonce, signature)pair valid under either format can authenticate as the victim on StepFi as long as they can insert the same nonce into thenoncestable — which they control viagenerateNonce(): generate a nonce, harvest a matching signature elsewhere, then self-issue the challenge here. The server picks which format to try, so the weakest accepted format defines the security floor.SEP-0043 exists precisely to fix this: its envelope includes domain, URI, version, issued-at, and expiration. The current implementation uses none of that structure.
Ground Rules
generateNonce,verifySignature,generateTokens(auth.service.ts)What To Build
Files To Touch
src/modules/auth/auth.service.tssrc/modules/auth/dto/verify-request.dto.tssrc/config/env.ts/ config plumbing for the flagAcceptance Criteria
Mandatory Checks Before Opening PR
Standard checklist applies. PRs failing any check will be closed without review.