#solana #sign-in #message #standard #wallet #output #verify

siws

Lightweight Sign in With Solana library adhering to the Solana Wallet Standard

3 releases

0.0.3 May 2, 2024
0.0.2 Apr 30, 2024
0.0.1 Apr 24, 2024

#6 in #sign-in


Used in siws-recap

MIT license

27KB
482 lines

SIWS - Sign in With Solana Rust Library

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.

Installation

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

Usage

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.

An End-to-end example

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.

Verify sign-in with SIWS Output

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(())
}

Validate SIWS Message from SIWS Output

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>

SIWS Message

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.

Parse SIWS message from string

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(())
}

Serialize the SIWS message according to its ABNF

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

Contributing

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.

Security

This library has not undergone security audits.

If you or anyone you know wants to audit siws-rs, please contact the authors directly.

Dependencies

~5.5MB
~104K SLoC