This crate provides a pure Rust implementation of EIP-4361: Sign In With Ethereum.
signinwithethereum = "0.7"| Feature | Description |
|---|---|
serde |
Serialization/deserialization support |
alloy |
EIP-1271 contract wallet and EIP-6492 counterfactual wallet signature verification |
typed-builder |
Builder pattern for VerificationOpts |
Parsing is done via the Message implementation of FromStr:
# use signinwithethereum::Message;
let msg = "example.com wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://example.com\nVersion: 1\nChain ID: 1\nNonce: 32891756\nIssued At: 2021-09-30T16:25:24Z";
let message: Message = msg.parse().unwrap();The parser validates:
- EIP-55 checksummed address
- Alphanumeric nonce (minimum 8 characters)
- RFC 3339 timestamps
- RFC 3986 URI and domain
- Optional
scheme://prefix per EIP-4361 - Printable ASCII statement (no control characters)
Verification and authentication is performed via EIP-191, using the address field of the Message as the expected signer. This returns the Ethereum public key of the signer:
# use signinwithethereum::Message;
# use hex::FromHex;
# let msg = "localhost:4361 wants you to sign in with your Ethereum account:\n0x6Da01670d8fc844e736095918bbE11fE8D564163\n\nSIWE Notepad Example\n\nURI: http://localhost:4361\nVersion: 1\nChain ID: 1\nNonce: kEWepMt9knR6lWJ6A\nIssued At: 2021-12-07T18:28:18.807Z";
# let message: Message = msg.parse().unwrap();
# let signature = <[u8; 65]>::from_hex("6228b3ecd7bf2df018183aeab6b6f1db1e9f4e3cbe24560404112e25363540eb679934908143224d746bbb5e1aa65ab435684081f4dbb74a0fec57f98f40f5051c").unwrap();
let signer: Vec<u8> = message.verify_eip191(&signature).unwrap();Time constraints (expiration and not-before) can be validated at current or specific times:
# use signinwithethereum::Message;
# use time::OffsetDateTime;
# let msg = "example.com wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://example.com\nVersion: 1\nChain ID: 1\nNonce: 32891756\nIssued At: 2021-09-30T16:25:24Z";
# let message: Message = msg.parse().unwrap();
assert!(message.valid_now());
// equivalent to
assert!(message.valid_at(&OffsetDateTime::now_utc()));Combined verification of time constraints, field bindings, and authentication can be done in a single call with verify:
# use hex::FromHex;
# use signinwithethereum::{Message, VerificationOpts};
# use time::{format_description::well_known::Rfc3339, OffsetDateTime};
# #[tokio::main]
# async fn main() {
# let msg = "localhost:4361 wants you to sign in with your Ethereum account:\n0x6Da01670d8fc844e736095918bbE11fE8D564163\n\nSIWE Notepad Example\n\nURI: http://localhost:4361\nVersion: 1\nChain ID: 1\nNonce: kEWepMt9knR6lWJ6A\nIssued At: 2021-12-07T18:28:18.807Z";
# let message: Message = msg.parse().unwrap();
# let signature = <[u8; 65]>::from_hex("6228b3ecd7bf2df018183aeab6b6f1db1e9f4e3cbe24560404112e25363540eb679934908143224d746bbb5e1aa65ab435684081f4dbb74a0fec57f98f40f5051c").unwrap();
let opts = VerificationOpts {
domain: Some("localhost:4361".parse().unwrap()),
nonce: Some("kEWepMt9knR6lWJ6A".into()),
timestamp: Some(OffsetDateTime::parse("2021-12-08T00:00:00Z", &Rfc3339).unwrap()),
..Default::default()
};
message.verify(&signature, &opts).await.unwrap();
# }Message instances serialize as their EIP-4361 string representation via the Display trait:
# use signinwithethereum::Message;
# let msg = "example.com wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://example.com\nVersion: 1\nChain ID: 1\nNonce: 32891756\nIssued At: 2021-09-30T16:25:24Z";
# let message: Message = msg.parse().unwrap();
let formatted = message.to_string();
assert!(formatted.contains("wants you to sign in"));EIP-191 Personal-Signature pre-hash signing input:
# use signinwithethereum::Message;
# let msg = "example.com wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://example.com\nVersion: 1\nChain ID: 1\nNonce: 32891756\nIssued At: 2021-09-30T16:25:24Z";
# let message: Message = msg.parse().unwrap();
let eip191_bytes: Vec<u8> = message.eip191_bytes().unwrap();EIP-191 Personal-Signature hash (Keccak-256 of the above):
# use signinwithethereum::Message;
# let msg = "example.com wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://example.com\nVersion: 1\nChain ID: 1\nNonce: 32891756\nIssued At: 2021-09-30T16:25:24Z";
# let message: Message = msg.parse().unwrap();
let eip191_hash: [u8; 32] = message.eip191_hash().unwrap();With the alloy feature enabled, verify() supports:
- EIP-1271 -- signature verification for deployed contract wallets (e.g. Safe, Argent)
- EIP-6492 -- signature verification for counterfactual (not yet deployed) contract wallets
Provide an RPC URL in the verification options. The verification order follows the EIP-6492 specification:
- EIP-6492 -- if the signature has the magic suffix, verify via the universal off-chain validator
- EOA -- try standard
ecrecoverfor 65-byte signatures - EIP-1271 -- fall back to on-chain
isValidSignatureif EOA verification fails
use hex::FromHex;
use signinwithethereum::{Message, TimeStamp, VerificationOpts};
use std::str::FromStr;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
#[tokio::main]
async fn main() {
let msg = r#"localhost:4361 wants you to sign in with your Ethereum account:
0x6Da01670d8fc844e736095918bbE11fE8D564163
SIWE Notepad Example
URI: http://localhost:4361
Version: 1
Chain ID: 1
Nonce: kEWepMt9knR6lWJ6A
Issued At: 2021-12-07T18:28:18.807Z"#;
let message: Message = msg.parse().unwrap();
let signature = <[u8; 65]>::from_hex(r#"6228b3ecd7bf2df018183aeab6b6f1db1e9f4e3cbe24560404112e25363540eb679934908143224d746bbb5e1aa65ab435684081f4dbb74a0fec57f98f40f5051c"#).unwrap();
let verification_opts = VerificationOpts {
domain: Some("localhost:4361".parse().unwrap()),
nonce: Some("kEWepMt9knR6lWJ6A".into()),
timestamp: Some(OffsetDateTime::parse("2021-12-08T00:00:00Z", &Rfc3339).unwrap()),
..Default::default()
};
if let Err(e) = message.verify(&signature, &verification_opts).await {
println!("Verification failed: {e}");
}
}cargo testTo run tests that require on-chain verification (EIP-1271 / EIP-6492), enable the alloy feature and provide an Ethereum mainnet RPC URL:
ETH_RPC_URL="https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY" cargo test --features alloyThis crate is the actively maintained successor to the siwe crate (v0.6), which is no longer maintained.
- siwe = "0.6"
+ signinwithethereum = "0.7"Rename the import:
- use siwe::{Message, VerificationOpts};
+ use signinwithethereum::{Message, VerificationOpts};If you used the ethers feature for EIP-1271 contract wallet verification, switch to alloy:
- siwe = { version = "0.6", features = ["ethers"] }
+ signinwithethereum = { version = "0.7", features = ["alloy"] }And replace the provider in VerificationOpts:
let opts = VerificationOpts {
- rpc_provider: Some("https://eth.llamarpc.com".try_into().unwrap()),
+ rpc_url: Some("https://eth.llamarpc.com".into()),
..Default::default()
};The Message struct now has a scheme: Option<String> field. If you construct Message values directly (rather than parsing), add it:
let msg = Message {
+ scheme: None,
domain: "example.com".parse().unwrap(),
// ...
};See CHANGELOG.md for the full list of breaking changes.