A simple 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.
SIWS can be easily installed by including the siws
crate as a dependency inside your project's Cargo.toml
:
[dependencies]
# ...other dependencies
siws = "0.0.1"
# ...other dependencies
SIWS exposes two main structs - SiwsMessage
for message validation, and SiwsOutput
for sign-in verification.
SiwsMessage
is analogous to Solana Wallet Standard's SolanaSignInInput
, while SiwsOutput
is analogous to SolanaSignInOutput
.
Using these, you can verify the sign in request, and validate the sign-in message.
You will mainly want to use the SiwsOutput
struct, as its primary purpose is to provide you with simple methods to verify its signature.
However, if you wish to validate the SIWS Message (which you should), you can extract it from SiwsOutput
's signed_message
field using SiwsMessage::try_from
.
The below example code shows a complete Rust program using actix-web
, time
, and siws
to receive a JSON object containing the SIWS Output, creating a SIWS message from it, verifying the signature and validating the message.
Cargo.toml
:
[package]
name = "siws-server-example"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.5.1"
siws = { path = "../../siws-rs" }
time = "0.3.36"
src/main.rs
use actix_web::{error, web, App, HttpServer, Result};
use siws::message::{SiwsMessage, ValidateOptions};
use siws::output::SiwsOutput;
use time::OffsetDateTime;
async fn validate_and_verify(output: web::Json<SiwsOutput>) -> Result<String> {
// Read the message from output.signed_message
let message = SiwsMessage::try_from(&output.signed_message).map_err(error::ErrorBadRequest)?;
// Validate the message
message
.validate(ValidateOptions {
domain: Some("www.exmaple.com".into()), // Ensure domain is www.example.com
nonce: Some("1337nonce".into()), // Ensure nonce is 1337nonce
time: Some(OffsetDateTime::now_utc()) // Validate IAT, EXP, and NBF according to current time
})
.map_err(error::ErrorBadRequest)?;
output.verify().map_err(error::ErrorBadRequest)?;
Ok(String::from("Successfully verified!"))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().route("/", web::post().to(validate_and_verify)))
.bind(("127.0.0.1", 8080))?
.run()
.await
}
SIWS Output
derives serde
's Serialize
and Deserialize
traits, and also automatically renames all of its fields as camelCase
for simpler Solana Wallet support.
Whenever you have a SIWS Output, all you need to do is call its verify
method to verify its signature. You can construct a SIWS Output by parsing a JSON string.
See tests/integration_tests.rs
for details.
fn verify_from_json_message() -> Result<(), VerifyError> {
let json = include_str!("test_message.json");
let output: SiwsOutput = serde_json::from_str(json).unwrap();
output.verify()?; // Result<(), VerifyError>
Ok(())
}
From the previous example, if you wanted to also validate the SIWS Message against a certain domain, nonce, or time, you can do the following:
let message = SiwsMessage::try_from(&output.signed_message).map_err(error::ErrorBadRequest)?;
message.validate(ValidateOptions {
...
})?; // Result<(), ValidateError>
The SiwsMessage
struct is used to serialize/deserialize the SIWS Message from/to its ABNF form.
Additional methods are implemented to support parsing it from a &Vec<u8>
and &[u8]
, as Solana Wallet-signed messages usually come as UTF-8 byte arrays.
You can parse a SIWS message from any string that adheres to its specified ABNF:
fn example_from_str() -> Result<(), ParseError> {
let msg = SiwsMessage::from_str(
"\
www.example.com wants you to sign in with your Solana account:\n\
BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW\n\
\n\
This is some test statement\n\
\n\
URI: test_uri\n\
Version: 1\n\
Chain ID: mainnet\n\
Nonce: abcdefgh\n\
Issued At: 2024-04-24T17:19:02.991469647Z\n\
Expiration Time: 2024-04-24T23:19:02.991482123Z\n\
Not Before: 2024-04-24T18:19:02.99148447Z\n\
Request ID: test_rid\n\
Resources:\n\
- https://www.example.com/test_one\n\
- https://www.example.com/test_two\
",
)?;
// Do something with the message
Ok(())
}
You can get the ABNF-compliant string for your SIWS Message by using String::from
:
let siws_message = SiwsMessage {
domain: "www.exmaple.com".into(),
address: "someaddress".into(),
..Default::default()
};
let message_string = String::from(&siws_message);
print!("{}", message_string);
This project aims to provide basic functionality of Sign in With Solana to Rust developers. As such, it's intended to be kept small and manageable.
Contributing to this repository is highly encouraged.
If you find any bugs, please try cloning the repository and fixing them yourself, then opening a PR with your proposed fixes.
The project is also open to new features, however feature requests should be discussed through issues beforehand to align with the minimalist nature of the project.
This library has not undergone security audits.
If you or anyone you know wants to audit siws-rs
, please contact the authors directly.