Skip to content

Sign In with X

Chain-Agnostic Wallet Authentication for Rust

CI License 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

Crates

Crate Description
siwx crates.io docs.rs Core data model, parser, validator, Verifier trait
siwx-evm crates.io docs.rs EIP-191 + optional EIP-1271 / EIP-6492 — Ethereum, Polygon, Arbitrum, …
siwx-svm crates.io docs.rs Ed25519 verification — Solana
siwx-cli crates.io CLI tool for message generation, parsing, and verification

Overview

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 validationAuthOpts requires domain and nonce; default clock skew 60s
  • Signature verification — pluggable Verifier trait (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.

Quick Start

Construct message (backend)

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…"

Verify signature (backend)

authenticate is fail-fast. There is no canonical rewrite of the signed text.

  1. Size ≤ MAX_MESSAGE_BYTES
  2. Reject CR
  3. ABNF parse (FromStr)
  4. message.validate(opts)AuthOpts.domain / nonce required; default clock_skew 60s
  5. Preamble chain_name == Verifier::CHAIN_NAME
  6. validate_address (EVM: EIP-55)
  7. validate_chain_id
  8. verify over the original raw_message bytes

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;

CLI

Install the CLI

Shell (macOS / Linux):

curl -fsSL https://sh.qntx.org/siwx | sh

PowerShell (Windows):

irm https://sh.qntx.org/siwx/ps | iex

Or via Cargo:

cargo install siwx-cli

Generate message

siwx 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

Verify signature

--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 L8s2Mf7kGxPQN9a4z

EIP-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 eip6492
siwx 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.example

JSON output

All 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"
}

Architecture

Authentication Flow

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
Loading

CAIP-122 Message Format

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

Verifier Trait

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

Extending to New Chains

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..."

Features

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).

Related Standards

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

License

Licensed under either of:

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.


A QuantX open-source project.

QuantX

Code is law. We write both.

About

Rust SDK for CAIP-122: Chain-Agnostic Wallet Authentication.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages