siwx verifies CAIP-122 / EIP-4361-style wallet sign-in messages and signatures.
It does not implement:
- Nonce storage or single-use consumption
- Session cookies / JWT issuance
- Rate limiting or abuse prevention
- Origin/Host HTTP binding beyond string equality on
domain - ENS resolution
Those belong in the application that calls authenticate.
- Server generates a cryptographic nonce (
siwx::nonce::generate_default) and stores it with TTL. - Server builds
SiwxMessageand renders withVerifier::format_message. - Client signs the exact rendered string.
- Server calls
authenticatewithAuthOpts::new(configured_domain, stored_nonce)and, for multi-chain apps,.with_chain_id(...). Binduri/scheme/request_idwhen those claims must match server configuration. Prefer.with_uri(...)to block confused-deputy (honest domain, attacker URI). The library does not require URI host == domain. - On success, atomically invalidate the nonce and create an application session.
- Prefer a short
expiration_time(minutes), single-use nonce consumption, and optionalAuthOpts::with_max_issued_age.
| Input | Rule |
|---|---|
raw_message |
ABNF-parsed; preamble chain_name must equal Verifier::CHAIN_NAME; signature is verified over these original bytes |
AuthOpts.domain / nonce |
Must come from server configuration / store, not the client alone. Exact byte equality. |
RPC URL (eip1271 / eip6492) |
Server-configured only — never take untrusted user URLs (SSRF) |
| Signature | Untrusted; cryptographic verification only |
CLI --trust-message-bindings is deleted. evm verify / svm verify always require --domain and --nonce. Debug self-sign by passing the same values the message already contains; there is no “trust the message” switch.
Parsing and authentication reject messages larger than MAX_MESSAGE_BYTES (16 KiB)
and resource lists larger than MAX_RESOURCES (32).
ABNF uses LF only. Parse rejects:
- CR (
InvalidFormat/CrLf) - a trailing LF after the last field (
UnexpectedTrailing) - out-of-order or unknown fields
If a client join("\n")s lines and leaves a leftover LF, the relying party may
call raw.trim_end_matches('\n') before authenticate (ASCII LF only, not
spaces). The library does not trim.
AuthOpts default clock_skew is 60 seconds. Skew applies only to
expiration_time, not_before, and max_issued_age:
- expired when
now > expiration_time + skew(now == expiration_timeis still valid) - not yet valid when
now + skew < not_before - stale when
issued_at <= now + skewand(now - issued_at) - skew > max_issued_age
The library does not reject a future issued_at. There is no FutureIssuedAt
error. Freshness is a short expiration_time, single-use nonce consumption, and
optional max_issued_age. Official SIWE verify vector "expired message" has
issuedAt after the injected time and must succeed.
- Addresses must be EIP-55 checksummed (
Address::parse_checksummed). All-lowercase is accepted only when that string is the checksum form. - EIP-191 signatures must be 65 bytes and low-s (EIP-2). Without RPC, high-s
is returned as
InvalidSignature. With RPC, any 191 error falls through to EIP-1271. - EIP-1271 / EIP-6492 call
eth_chainIdfirst; mismatch isChainIdMismatchand does not invokeisValidSignatureon the wrong chain. - Deleted
EvmVerifier::with_rpc(url)— a single unscoped URL would hit the wrong chain. Usewith_rpc_for_chain/with_rpc_map.
EIP-1271 is not a pure function. If contract validation logic changes, the application must invalidate sessions (short TTL / webhook). Out of library.
These are not provided by this crate and must not be assumed present when calling the stack “production ready” as a full auth product:
| Residual | Owner |
|---|---|
| Nonce single-use store / atomic consume | Application |
| Session / JWT / cookie issuance and revocation | Application |
HTTP Origin/Host multi-environment policy beyond AuthOpts.domain |
Application |
| Rate limiting, CAPTCHA, device risk | Application / edge |
| Live mainnet/anvil EIP-1271 / EIP-6492 success e2e in CI | App/ops; library tests cover offline selection, magic, and eth_call_bool |
| Third-party security audit reports | Process outside the repo |
Library production-ready means: correct EOA (and optional 1271/6492-with-trusted-RPC)
verification via authenticate, with binding and DoS bounds—not a hosted IdP.
Counterfactual / predeploy smart-account signatures. The 32-byte magic suffix
0x6492 repeated 16 times is checked before EIP-191. Without the feature,
those signatures return InvalidSignature (EIP-6492 not enabled) and do not
fall through to EIP-191.
With the feature, verification is a deployless eth_call (to omitted) of
vendored wevm/ox universalSignatureValidatorBytecode concatenated with
abi.encode(signer, hash, signature). The pin is
siwx-evm/src/bytecode/SOURCE.txt (ox commit SHA + source path). Success is
eth_call_bool: last byte 0x01 and the rest zero (nodes pad return(31,1)
to 32 bytes). Last byte 0x00 is VerificationFailed (EIP-6492 invalid).
Revert or RPC failure is VerificationFailed without the RPC URL in the
string.
No RPC for the message chain (including EvmVerifier::new()) returns
InvalidSignature (EIP-6492 requires RPC) and does not eth_call. The
call is eth_call only (simulation); the library never sends a deployment
transaction.
Do not log signature bytes or the full raw_message. Error strings must not
include RPC URLs (SSRF / internal network leak).
Report vulnerabilities privately to the maintainers via the repository security advisory channel or project contact listed on the GitHub org.