A lightweight Rust implementation of CAIP-122 (Sign in With X) for Solana, following the Solana Wallet Standard and Phantom Wallet's Sign In With Solana protocol.
No Solana SDK dependency -- uses ed25519-dalek 2.x, bs58, and time to keep it lightweight.
Add siws to your Cargo.toml:
[dependencies]
siws = { git = "ssh://git@github.com/MeteoraAg/siws-rs.git" }SIWS exposes two main structs:
SiwsMessage-- parse, construct, and validate CAIP-122 / EIP-4361 formatted messagesSiwsOutput-- verify Ed25519 signatures and validate sign-in outputs
SiwsMessage is analogous to Solana Wallet Standard's SolanaSignInInput, while SiwsOutput is analogous to SolanaSignInOutput.
authenticate combines signature verification, message parsing, address-public key matching, and field validation:
use siws::message::ValidateOptions;
use siws::output::SiwsOutput;
use time::OffsetDateTime;
fn handle_sign_in(output: SiwsOutput) -> Result<(), Box<dyn std::error::Error>> {
let message = output.authenticate(ValidateOptions {
domain: Some("meteora.ag".into()),
nonce: Some("server-generated-nonce".into()),
time: Some(OffsetDateTime::now_utc()),
})?;
println!("Authenticated wallet: {}", message.address);
println!("Chain: {:?}", message.chain_id);
Ok(())
}This will:
- Verify the Ed25519 signature
- Parse the signed message into a
SiwsMessage - Check that the message address matches the signer's public key
- Validate domain, nonce, and timestamps against the provided options
If you want to handle parsing and validation separately:
use siws::message::{SiwsMessage, ValidateOptions};
use siws::output::SiwsOutput;
use time::OffsetDateTime;
fn handle_sign_in(output: SiwsOutput) -> Result<(), Box<dyn std::error::Error>> {
// 1. Verify the Ed25519 signature
output.verify()?;
// 2. Parse the message
let message = SiwsMessage::try_from(&output.signed_message)?;
// 3. Validate fields
message.validate(ValidateOptions {
domain: Some("meteora.ag".into()),
nonce: Some("server-generated-nonce".into()),
time: Some(OffsetDateTime::now_utc()),
})?;
Ok(())
}use axum::{http::StatusCode, Json};
use siws::message::ValidateOptions;
use siws::output::SiwsOutput;
use time::OffsetDateTime;
async fn verify_handler(
Json(output): Json<SiwsOutput>,
) -> Result<String, (StatusCode, String)> {
let message = output
.authenticate(ValidateOptions {
domain: Some("meteora.ag".into()),
nonce: Some("expected-nonce".into()),
time: Some(OffsetDateTime::now_utc()),
})
.map_err(|e| (StatusCode::UNAUTHORIZED, e.to_string()))?;
Ok(format!("Authenticated: {}", message.address))
}Parse a CAIP-122 / EIP-4361 ABNF formatted message:
use std::str::FromStr;
use siws::message::SiwsMessage;
let message = SiwsMessage::from_str(
"meteora.ag wants you to sign in with your Solana account:\n\
7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU\n\
\n\
Sign in to access your Meteora referral dashboard.\n\
\n\
URI: https://meteora.ag\n\
Version: 1\n\
Chain ID: mainnet\n\
Nonce: abc123\n\
Issued At: 2026-04-02T10:30:00.000Z\n\
Expiration Time: 2026-04-02T10:35:00.000Z",
).unwrap();
assert_eq!(message.domain, "meteora.ag");
assert_eq!(message.chain_id, Some("mainnet".into()));Build an ABNF-formatted message from structured input:
use siws::message::SiwsMessage;
let message = SiwsMessage {
domain: "meteora.ag".into(),
address: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU".into(),
statement: Some("Sign in to access your Meteora referral dashboard.".into()),
version: Some("1".into()),
chain_id: Some("mainnet".into()),
..Default::default()
};
let message_string = String::from(&message);| Field | Purpose |
|---|---|
domain |
Reject if message domain doesn't match |
nonce |
Reject if message nonce doesn't match |
time |
Reject if message is expired, issued in the future, or not yet valid |
All fields are optional. Omitted fields skip that check.
ValidateError variants:
| Variant | Meaning |
|---|---|
DomainMismatch |
Message domain doesn't match expected domain |
NonceMismatch |
Message nonce doesn't match expected nonce |
Expired |
Message expiration time has passed |
IssuedInFuture |
Message issuedAt is after the check time |
NotYetValid |
Message notBefore is after the check time |
VerifyError variants:
| Variant | Meaning |
|---|---|
VerificationFailure |
Ed25519 signature verification failed |
AddressMismatch |
Message address doesn't match the signer's public key |
MessageParse |
Failed to parse the signed message |
MessageValidate |
Field validation failed |
SiwsOutput |
Invalid public key or signature bytes |
SiwsOutput derives serde Serialize/Deserialize with camelCase field names for Solana Wallet Standard compatibility:
{
"account": { "publicKey": [/* 32 bytes */] },
"signedMessage": [/* UTF-8 message bytes */],
"signature": [/* 64 bytes */]
}This library has not undergone a security audit. Use at your own risk.
If you discover a vulnerability, please report it privately to the maintainers.
MIT