You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(security): wallet-signature replay protection and challenge hardening
π Overview
Nester authenticates and authorizes actions with Stellar wallet signatures β there is a challenge store (apps/api/internal/service/challenge_store.go) implementing the standard challenge-response pattern where the server issues a challenge, the user signs it with their wallet, and the server verifies the signature to prove key ownership. This is the right pattern, but its security depends entirely on details that are easy to get subtly wrong: a challenge that can be replayed lets an attacker who captured one signed challenge reuse it; a challenge without a tight expiry widens the replay window; a challenge not bound to a specific action or domain can be signed in one context and replayed in another; and signature verification that does not check every field can be tricked. On a platform where a wallet signature can authorize moving funds, each of these is a path to unauthorized action.
This issue hardens the wallet-signature flow against replay and related attacks: single-use challenges with tight expiry, binding of each challenge to its purpose and domain, rigorous signature verification, and protection against the specific ways signed-message schemes get bypassed. It makes signature-based auth as robust as the funds it protects.
π― Objectives
Guarantee each challenge is single-use so a captured signature cannot be replayed.
Bind every challenge to its purpose, domain and a tight expiry.
Verify signatures rigorously against the full, canonical challenge.
Prevent cross-context and cross-domain replay of signed challenges.
Challenges are single-use. When a challenge is issued it is recorded (challenge store) with a unique nonce; when a signed challenge is verified, the challenge is atomically consumed so it cannot be verified a second time. The consume-on-verify must be atomic (a check-then-delete race would allow a brief replay window under concurrency), so use an atomic operation β consuming and validating in one step. A replayed signature of an already-consumed challenge is rejected. This is the core replay defense and must be tested under concurrency.
Tight expiry: challenges expire quickly (seconds to a couple of minutes), because the smaller the validity window, the smaller the opportunity to capture and replay. Expired challenges are rejected and purged. The expiry is enforced server-side against the challenge's issue time, never trusted from the client.
Binding is what prevents cross-context replay. Each challenge embeds: a unique nonce, the issuing domain/origin (so a challenge signed for Nester cannot be replayed against another service, and vice versa), the specific purpose/action it authorizes (login vs authorizing a particular transaction β a login challenge must not be reusable to authorize a fund movement), the user/account it is for, and the expiry. Verification checks every one of these, not just the signature. A signature that is cryptographically valid but for a challenge bound to a different purpose, domain, or account must be rejected β cryptographic validity alone is not authorization.
Signature verification must be rigorous and use the correct primitive for Stellar (ed25519 over the canonical challenge bytes). Define the canonical serialization of the challenge precisely so the bytes signed and the bytes verified are identical β an ambiguous encoding is a bypass. Verify the signature is by the key that owns the claimed account. Reject malformed, truncated or wrong-length signatures explicitly rather than letting them fall through.
Abuse protection: rate-limit challenge issuance and verification per account/IP (reuse the platform limiter) so the flow cannot be used to brute-force or to spam. Monitor for anomalies β a burst of failed verifications is a signal β feeding the fraud/anomaly engine (companion issue). Every authorization-relevant signature event is audited (companion audit-log issue).
βοΈ Technical Notes
Harden apps/api/internal/service/challenge_store.go (tested at apps/api/internal/service/challenge_store_test.go) β atomic consume-on-verify, tight expiry, and the binding fields. Signature verification lives in the auth/stellar path; ensure it uses the correct ed25519 verification against the canonical challenge and validates account ownership. Challenge storage with atomic consumption suits Redis (atomic ops, TTL-native) or a database with an atomic delete-returning; document the choice and the atomicity guarantee. The canonical challenge format follows or extends the relevant Stellar signing convention (SEP-0010 web-auth is the reference pattern for Stellar challenge-response β align with it where applicable and document any deviation).
Tests (critical): a consumed challenge cannot be verified again, including under concurrent replay attempts (atomicity); an expired challenge is rejected; a challenge signed for one purpose/domain/account is rejected when presented for another; malformed/truncated signatures are rejected; account-ownership mismatch is rejected; and rate limiting bounds issuance/verification. Extend apps/api/internal/service/challenge_store_test.go.
π¦ Expected Outcome
Every wallet-signature challenge is single-use, tightly expiring, and bound to its exact purpose, domain and account, with the consume-on-verify atomic against concurrent replay. A captured signature cannot be replayed, reused for a different action, or replayed against another domain. Signature verification is rigorous over a canonical encoding with account-ownership checks and explicit rejection of malformed input. The flow is rate-limited, monitored and audited. Signature-based authorization is as robust as the funds it guards.
β Acceptance Criteria
Challenges are single-use with atomic consume-on-verify; a concurrent-replay test proves a consumed challenge cannot be verified twice.
Challenges have a tight server-enforced expiry; expired challenges are rejected and purged.
Each challenge binds nonce, domain/origin, purpose/action, account and expiry, and verification checks all of them; cross-purpose, cross-domain and wrong-account replays are rejected, proven by tests.
Signature verification uses correct ed25519 over a documented canonical encoding, validates account ownership, and explicitly rejects malformed/truncated signatures.
The flow aligns with the Stellar challenge-response convention (SEP-0010 reference), with any deviation documented.
Challenge issuance and verification are rate-limited per account/IP, and failed-verification bursts feed the anomaly engine.
Authorization-relevant signature events are audited.
feat(security): wallet-signature replay protection and challenge hardening
π Overview
Nester authenticates and authorizes actions with Stellar wallet signatures β there is a challenge store (apps/api/internal/service/challenge_store.go) implementing the standard challenge-response pattern where the server issues a challenge, the user signs it with their wallet, and the server verifies the signature to prove key ownership. This is the right pattern, but its security depends entirely on details that are easy to get subtly wrong: a challenge that can be replayed lets an attacker who captured one signed challenge reuse it; a challenge without a tight expiry widens the replay window; a challenge not bound to a specific action or domain can be signed in one context and replayed in another; and signature verification that does not check every field can be tricked. On a platform where a wallet signature can authorize moving funds, each of these is a path to unauthorized action.
This issue hardens the wallet-signature flow against replay and related attacks: single-use challenges with tight expiry, binding of each challenge to its purpose and domain, rigorous signature verification, and protection against the specific ways signed-message schemes get bypassed. It makes signature-based auth as robust as the funds it protects.
π― Objectives
π§© Requirements
Challenges are single-use. When a challenge is issued it is recorded (challenge store) with a unique nonce; when a signed challenge is verified, the challenge is atomically consumed so it cannot be verified a second time. The consume-on-verify must be atomic (a check-then-delete race would allow a brief replay window under concurrency), so use an atomic operation β consuming and validating in one step. A replayed signature of an already-consumed challenge is rejected. This is the core replay defense and must be tested under concurrency.
Tight expiry: challenges expire quickly (seconds to a couple of minutes), because the smaller the validity window, the smaller the opportunity to capture and replay. Expired challenges are rejected and purged. The expiry is enforced server-side against the challenge's issue time, never trusted from the client.
Binding is what prevents cross-context replay. Each challenge embeds: a unique nonce, the issuing domain/origin (so a challenge signed for Nester cannot be replayed against another service, and vice versa), the specific purpose/action it authorizes (login vs authorizing a particular transaction β a login challenge must not be reusable to authorize a fund movement), the user/account it is for, and the expiry. Verification checks every one of these, not just the signature. A signature that is cryptographically valid but for a challenge bound to a different purpose, domain, or account must be rejected β cryptographic validity alone is not authorization.
Signature verification must be rigorous and use the correct primitive for Stellar (ed25519 over the canonical challenge bytes). Define the canonical serialization of the challenge precisely so the bytes signed and the bytes verified are identical β an ambiguous encoding is a bypass. Verify the signature is by the key that owns the claimed account. Reject malformed, truncated or wrong-length signatures explicitly rather than letting them fall through.
Abuse protection: rate-limit challenge issuance and verification per account/IP (reuse the platform limiter) so the flow cannot be used to brute-force or to spam. Monitor for anomalies β a burst of failed verifications is a signal β feeding the fraud/anomaly engine (companion issue). Every authorization-relevant signature event is audited (companion audit-log issue).
βοΈ Technical Notes
Harden apps/api/internal/service/challenge_store.go (tested at apps/api/internal/service/challenge_store_test.go) β atomic consume-on-verify, tight expiry, and the binding fields. Signature verification lives in the auth/stellar path; ensure it uses the correct ed25519 verification against the canonical challenge and validates account ownership. Challenge storage with atomic consumption suits Redis (atomic ops, TTL-native) or a database with an atomic delete-returning; document the choice and the atomicity guarantee. The canonical challenge format follows or extends the relevant Stellar signing convention (SEP-0010 web-auth is the reference pattern for Stellar challenge-response β align with it where applicable and document any deviation).
Frontend signing is in apps/dapp/frontend/lib/stellar/transaction.ts and the wallet flow (apps/dapp/frontend/components/connect-wallet.tsx, apps/dapp/frontend/hooks/useNesterAuth.ts) β ensure the client signs the canonical challenge and includes the binding fields. Rate limiting reuses the middleware; monitoring feeds the anomaly engine; events are audited. Coordinate with the auth-endpoint pentest (#589).
Tests (critical): a consumed challenge cannot be verified again, including under concurrent replay attempts (atomicity); an expired challenge is rejected; a challenge signed for one purpose/domain/account is rejected when presented for another; malformed/truncated signatures are rejected; account-ownership mismatch is rejected; and rate limiting bounds issuance/verification. Extend apps/api/internal/service/challenge_store_test.go.
π¦ Expected Outcome
Every wallet-signature challenge is single-use, tightly expiring, and bound to its exact purpose, domain and account, with the consume-on-verify atomic against concurrent replay. A captured signature cannot be replayed, reused for a different action, or replayed against another domain. Signature verification is rigorous over a canonical encoding with account-ownership checks and explicit rejection of malformed input. The flow is rate-limited, monitored and audited. Signature-based authorization is as robust as the funds it guards.
β Acceptance Criteria
π¨ Contribution Rule
devbranchmainwill be automatically rejecteddev