Skip to main content

alibi_plugins/siwe/
crypto.rs

1use super::config::{SiweCallbackResult, SiweVerification, SiweVerifier};
2use super::parse::valid_address;
3use async_trait::async_trait;
4use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
5use k256::elliptic_curve::scalar::IsHigh;
6use sha3::{Digest, Keccak256};
7
8/// Secure EIP-191 verifier for externally owned Ethereum accounts. Contract
9/// wallets can use a custom [`SiweVerifier`] backed by the application's RPC.
10#[derive(Debug, Clone, Copy, Default)]
11pub struct Eip191Verifier;
12
13impl Eip191Verifier {
14    /// Recover the checksummed signer from a 65-byte `r,s,v` signature or an
15    /// EIP-2098 compact signature. Invalid encodings return `None`.
16    #[must_use]
17    pub fn recover_address(message: &str, signature: &str) -> Option<String> {
18        Self::recover_hash_address(&ethereum_message_hash(message), signature)
19    }
20
21    /// Recover a personal-sign signer from its already computed digest. This
22    /// supports application-owned contract-wallet and JSON-RPC integrations.
23    #[must_use]
24    pub fn recover_hash_address(digest: &[u8; 32], signature: &str) -> Option<String> {
25        use std::fmt::Write;
26
27        let bytes = hex_bytes(signature)?;
28        let (signature_3, recovery) = match bytes.len() {
29            65 => {
30                let signature_2 = Signature::from_slice(bytes.get(..64)?).ok()?;
31                let encoded = *bytes.get(64)?;
32                let recovery = match encoded {
33                    0 | 1 => encoded,
34                    27 | 28 => encoded - 27,
35                    _ => return None,
36                };
37                (signature_2, recovery)
38            }
39            64 => {
40                let mut compact: [u8; 64] = bytes.try_into().ok()?;
41                let recovery = *compact.get(32)? >> 7;
42                if let Some(first) = compact.get_mut(32) {
43                    *first &= 127;
44                }
45                (Signature::from_slice(&compact).ok()?, recovery)
46            }
47            _ => return None,
48        };
49        // Reject noncanonical signatures rather than allowing an alternative
50        // high-S encoding to bypass the verifier's signature policy.
51        if signature_3.s().is_high().into() {
52            return None;
53        }
54        let key = VerifyingKey::recover_from_prehash(
55            digest,
56            &signature_3,
57            RecoveryId::try_from(recovery).ok()?,
58        )
59        .ok()?;
60        let public_key = key.to_sec1_point(false);
61        let hash = Keccak256::digest(public_key.as_bytes().get(1..)?);
62        let mut address = String::from("0x");
63
64        for byte in hash.get(12..)? {
65            write!(address, "{byte:02x}").ok()?;
66        }
67        checksum_address(&address)
68    }
69}
70
71#[async_trait]
72impl SiweVerifier for Eip191Verifier {
73    async fn verify_message(&self, input: SiweVerification) -> SiweCallbackResult<bool> {
74        Ok(Self::recover_address(&input.message, &input.signature)
75            .is_some_and(|address| address == input.address))
76    }
77}
78
79/// EIP-191 personal-sign message digest. The prefix uses the UTF-8 byte count,
80/// including any non-ASCII statement or SIWE field in the original message.
81#[must_use]
82pub fn ethereum_message_hash(message: &str) -> [u8; 32] {
83    let mut digest = Keccak256::new();
84    digest.update(b"\x19Ethereum Signed Message:\n");
85    digest.update(message.len().to_string().as_bytes());
86    digest.update(message.as_bytes());
87    digest.finalize().into()
88}
89
90pub(super) fn checksum_address(address: &str) -> Option<String> {
91    if !valid_address(address) {
92        return None;
93    }
94    let lower = address.get(2..)?.to_ascii_lowercase();
95    let digest = Keccak256::digest(lower.as_bytes());
96    let mut checksum = String::from("0x");
97    for (index, character) in lower.bytes().enumerate() {
98        let byte = *digest.get(index / 2)?;
99        let nibble = if index % 2 == 0 { byte >> 4 } else { byte & 15 };
100        checksum.push(char::from(if nibble >= 8 {
101            character.to_ascii_uppercase()
102        } else {
103            character
104        }));
105    }
106    Some(checksum)
107}
108
109fn hex_bytes(value: &str) -> Option<Vec<u8>> {
110    let value = value.strip_prefix("0x")?;
111    if !value.len().is_multiple_of(2) {
112        return None;
113    }
114    value
115        .as_bytes()
116        .as_chunks::<2>()
117        .0
118        .iter()
119        .map(|pair| {
120            let upper = char::from(*pair.first()?).to_digit(16)?;
121            let lower = char::from(*pair.get(1)?).to_digit(16)?;
122            u8::try_from((upper << 4) | lower).ok()
123        })
124        .collect()
125}