Chain-Agnostic Wallet Authentication for Rust
Type-safe Rust SDK for CAIP-122 Sign-In with X. Construct, parse, validate, and verify wallet authentication messages across any blockchain.
Quick Start | CLI | Architecture | API docs
| Crate | Description | |
|---|---|---|
siwx |
Core data model, parser, validator, Verifier trait |
|
siwx-evm |
EIP-191 + optional EIP-1271 / EIP-6492 — Ethereum, Polygon, Arbitrum, … | |
siwx-svm |
Ed25519 verification — Solana | |
siwx-cli |
CLI tool for message generation, parsing, and verification |
CAIP-122 standardises wallet-based authentication across blockchains — the chain-agnostic successor to EIP-4361 (SIWE). This SDK provides:
- Message construction — build CAIP-122 challenge messages with a builder API
- Message parsing — ABNF-strict
FromStr; timestamps keep the original RFC 3339 string - Temporal & domain validation —
AuthOptsrequires domain and nonce; default clock skew 60s - Signature verification — pluggable
Verifiertrait (CHAIN_NAME/NAMESPACE) over original bytes - CLI tool — generate, parse, and verify messages from the command line with JSON output
The core siwx crate is chain-agnostic; chain-specific logic is in companion crates.
use siwx::{SiwxMessage, Verifier};
use siwx_evm::EvmVerifier;
let message = SiwxMessage::new(
"example.com", // domain
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // address
"https://example.com/login", // uri
"1", // chain_id (EIP-155)
siwx::nonce::generate_default(), // nonce (≥ 8 alphanumeric)
)?
.with_statement("Sign in to Example")?;
// Render into the EIP-4361 signing string via the chain's Verifier.
// `format_message` is a `Verifier` default method — the chain label comes
// from `EvmVerifier::CHAIN_NAME` ("Ethereum"), so it can never drift.
let signing_input = EvmVerifier::format_message(&message);
// → "example.com wants you to sign in with your Ethereum account:\n0xd8dA…"authenticate is fail-fast. There is no canonical rewrite of the signed text.
- Size ≤
MAX_MESSAGE_BYTES - Reject CR
- ABNF parse (
FromStr) message.validate(opts)—AuthOpts.domain/noncerequired; defaultclock_skew60s- Preamble
chain_name==Verifier::CHAIN_NAME validate_address(EVM: EIP-55)validate_chain_idverifyover the originalraw_messagebytes
A trailing LF is rejected (UnexpectedTrailing). If a client join("\n")s a leftover newline, call raw.trim_end_matches('\n') before authenticate. The library does not trim.
use siwx::{authenticate, AuthOpts, Verifier};
use siwx_evm::EvmVerifier;
// Inputs (typically supplied by the frontend / session store):
// signing_input: String — the exact CAIP-122 text the wallet signed
// signature_bytes: &[u8] — raw bytes returned by the wallet
// expected_nonce: String — nonce your backend issued in step 1
// Feature `eip1271` is off by default. `eip6492` (default off, implies `eip1271`)
// is for counterfactual accounts; then `EvmVerifier::with_rpc_for_chain(...)`.
let auth = authenticate(
&EvmVerifier::new(),
&signing_input,
&signature_bytes,
&AuthOpts::new("example.com", expected_nonce).with_chain_id("1"),
).await?;
// auth.address() is the authenticated wallet
let _ = auth;Shell (macOS / Linux):
curl -fsSL https://sh.qntx.org/siwx | shPowerShell (Windows):
irm https://sh.qntx.org/siwx/ps | iexOr via Cargo:
cargo install siwx-clisiwx evm message \
--domain example.com \
--address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \
--uri https://example.com/login \
--chain-id 1 \
--statement "Sign in to Example"siwx svm message \
--domain example.com \
--address GwAF45zjfyGzUbd3i3hXxzGeuchzEZXwpRYHZM5912F1 \
--uri https://example.com/login \
--chain-id 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d--domain and --nonce are required (server-issued). Optional --uri /
--scheme / --chain-id bind those claims. A trailing LF in --message is
rejected; trim before calling if the client left one.
siwx evm verify \
--message "..." \
--signature 0x... \
--domain example.com \
--nonce L8s2Mf7kGxPQN9a4z
siwx svm verify \
--message "..." \
--signature 0x... \
--domain example.com \
--nonce L8s2Mf7kGxPQN9a4zEIP-1271 / EIP-6492 (features eip1271 / eip6492, both default off): --rpc
and --rpc-chain-id must appear as pairs, same order, repeatable. Bare --rpc
is rejected. Those flags exist only when the binary is built with the feature:
cargo install siwx-cli --features eip1271
# or: cargo install siwx-cli --features eip6492siwx evm verify \
--message "..." \
--signature 0x... \
--domain example.com \
--nonce L8s2Mf7kGxPQN9a4z \
--rpc-chain-id 1 --rpc https://eth.example \
--rpc-chain-id 137 --rpc https://polygon.exampleAll commands support --json for programmatic / agent consumption:
siwx --json evm message --domain example.com --address 0x... --uri https://... --chain-id 1{
"chain": "ethereum",
"message": "example.com wants you to sign in with your Ethereum account:\n...",
"domain": "example.com",
"address": "0x...",
"uri": "https://...",
"version": "1",
"chain_id": "1",
"nonce": "L8s2Mf7kGxPQN9a4z",
"issued_at": "2024-01-01T00:00:00Z"
}sequenceDiagram
participant Frontend
participant Backend
participant Wallet
Backend->>Frontend: 1. Challenge (SiwxMessage as text)
Frontend->>Wallet: 2. personal_sign / signMessage
Wallet-->>Frontend: 3. Signature bytes
Frontend->>Backend: 4. Message text + Signature
Backend->>Backend: 5. Size → CR → Parse → Validate → chain_name → address → chain_id → Verify original bytes
example.com wants you to sign in with your Ethereum account:
0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
Sign in to Example
URI: https://example.com/login
Version: 1
Chain ID: 1
Nonce: L8s2Mf7kGxPQN9a4z
Issued At: 2024-01-01T00:00:00Z
Chain-specific crates implement the Verifier trait:
pub trait Verifier: Send + Sync {
/// Ecosystem label embedded in the CAIP-122 preamble — e.g. "Ethereum",
/// "Solana". Required, so new chains can never ship without one.
const CHAIN_NAME: &'static str;
/// CAIP-2 namespace, e.g. "eip155" / "solana".
const NAMESPACE: &'static str;
/// Verify `signature` over `raw_message` (exact wallet bytes), binding
/// identity to `message.address()`.
fn verify(
&self,
message: &SiwxMessage,
raw_message: &str,
signature: &[u8],
) -> impl Future<Output = Result<(), SiwxError>> + Send;
/// Render `message` into the chain's canonical signing string.
/// Default impl calls `SiwxMessage::to_sign_string(Self::CHAIN_NAME)`.
fn format_message(message: &SiwxMessage) -> String { /* ... */ }
}| Verifier | Crate | Signature Type | Async |
|---|---|---|---|
EvmVerifier |
siwx-evm |
EIP-191; optional EIP-1271 (eip1271 + RPC); optional EIP-6492 (eip6492 + RPC) |
Yes |
Ed25519Verifier |
siwx-svm |
Ed25519 (pubkey from message.address()) |
No |
Implement Verifier for your target chain — just declare the chain label and
plug in your verification logic; format_message is handled for you.
use siwx::{SiwxError, SiwxMessage, Verifier};
pub struct MyChainVerifier;
impl Verifier for MyChainVerifier {
const CHAIN_NAME: &'static str = "MyChain";
const NAMESPACE: &'static str = "mychain";
async fn verify(
&self,
message: &SiwxMessage,
raw_message: &str,
signature: &[u8],
) -> Result<(), SiwxError> {
// Verify `signature` over `raw_message`, bind identity to message.address()
todo!()
}
}
// `MyChainVerifier::format_message(&msg)` now renders:
// "{domain} wants you to sign in with your MyChain account:\n{address}\n..."| Feature | Crate | Description |
|---|---|---|
serde |
siwx |
Serialize / Deserialize for SiwxMessage |
eip1271 |
siwx-evm |
Smart-contract signature verification via RPC (with_rpc_for_chain / with_rpc_map; default off) |
eip6492 |
siwx-evm |
ERC-6492 counterfactual signatures (eip6492 = ["eip1271"], default off) |
eip1271 |
siwx-cli |
Enables paired evm verify --rpc-chain-id <id> --rpc <url> |
eip6492 |
siwx-cli |
Forwards to siwx-evm/eip6492 (implies eip1271 RPC pairs) |
See SECURITY.md for production integration boundaries (nonce store, RPC trust).
| Standard | Relationship |
|---|---|
| CAIP-122 | Core specification — Sign-In with X abstract data model |
| CAIP-2 | Blockchain ID format (namespace:reference) |
| CAIP-10 | Account ID format (chain_id:account_address) |
| EIP-4361 | Sign-In with Ethereum — the EVM namespace profile |
| EIP-191 | Ethereum personal message signatures |
| EIP-1271 | Smart contract signature validation |
| EIP-6492 | Counterfactual / predeploy smart-account signatures |
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT License (LICENSE-MIT or https://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project shall be dual-licensed as above, without any additional terms or conditions.