eth_ecdsa_verifier/
lib.rs

1use std::error::Error;
2
3use easy_hasher::easy_hasher;
4
5type DynamicResult<T> = Result<T, Box<dyn Error>>;
6
7/// Validates an ECDSA signature against a given public Ethereum address.
8///
9/// # Arguments
10/// * `public_key` - A `0x`-prefixed Ethereum address string (e.g., "0xabc...")
11/// * `message` - The original message bytes that were signed
12/// * `signature_hex` - A 65-byte ECDSA signature in raw binary format (r, s, v)
13///
14/// # Returns
15/// * `Ok(true)` if the signature is valid and matches the address  
16/// * `Ok(false)` if the signature does not match the address  
17/// * `Err` if parsing or recovery fails
18pub fn validate_ecdsa_signature(
19    public_key: &str,
20    message: &[u8],
21    signature_hex: &[u8],
22) -> DynamicResult<bool> {
23    // Strip "0x" prefix for consistency
24    let public_key_hex = public_key.replace("0x", "");
25
26    // Recover address from signature + message
27    let recovered_key = recover_address_from_eth_signature(signature_hex, message)?;
28    let recovered_key_hex = hex::encode(recovered_key);
29
30    // Compare lowercase versions for address match
31    Ok(recovered_key_hex.to_lowercase() == public_key_hex.to_lowercase())
32}
33
34pub fn validate_ecdsa_signature_string(
35    public_key: String,
36    message: String,
37    signature_hex: String,
38) -> DynamicResult<bool> {
39    // Strip "0x" prefix for consistency
40    let public_key_hex = public_key.replace("0x", "");
41    let signature_hex = signature_hex.replace("0x", "");
42
43    // generate all the bytes
44    let signature_bytes = hex::decode(signature_hex)?;
45    let message_bytes = message.as_bytes();
46
47    // Recover address from signature + message
48    let recovered_key = recover_address_from_eth_signature(&signature_bytes, message_bytes)?;
49    let recovered_key_hex = hex::encode(recovered_key);
50
51    // Compare lowercase versions for address match
52    Ok(recovered_key_hex.to_lowercase() == public_key_hex.to_lowercase())
53}
54
55/// Recovers an Ethereum address from a raw ECDSA signature and original message.
56///
57/// # Arguments
58/// * `metamask_signature` - A 65-byte ECDSA signature (r, s, v)
59/// * `message` - The original signed message
60///
61/// # Returns
62/// * `Ok(Vec<u8>)` - The recovered 20-byte Ethereum address
63/// * `Err` - On decoding or recovery failure
64fn recover_address_from_eth_signature(
65    metamask_signature: &[u8],
66    message: &[u8],
67) -> DynamicResult<Vec<u8>> {
68    // First 64 bytes are the signature
69    let signature_bytes: [u8; 64] = metamask_signature[0..64].try_into()?;
70    let signature_bytes_64 = libsecp256k1::Signature::parse_standard(&signature_bytes)?;
71
72    // Final byte is the recovery ID
73    let recovery_id = metamask_signature[64];
74    let recovery_id_byte = libsecp256k1::RecoveryId::parse_rpc(recovery_id)?;
75
76    // Hash the message using Ethereum's prefixed method
77    let message_bytes: [u8; 32] = hash_eth_message(message)
78        .try_into()
79        .map_err(|e| format!("{e:?}"))?;
80    let message_bytes_32 = libsecp256k1::Message::parse(&message_bytes);
81
82    // Recover the public key using secp256k1 recovery
83    let public_key =
84        libsecp256k1::recover(&message_bytes_32, &signature_bytes_64, &recovery_id_byte)?;
85
86    // Convert the recovered public key to an Ethereum address
87    get_address_from_public_key(
88        public_key
89            .serialize_compressed()
90            .to_vec()
91            .try_into()
92            .map_err(|e| format!("{e:?}"))?,
93    )
94}
95
96/// Hashes a message using Ethereum's signed message prefix scheme.
97///
98/// # Arguments
99/// * `message` - The original message bytes
100///
101/// # Returns
102/// * A Keccak256 hash of the prefixed message (Vec<u8>, 32 bytes)
103fn hash_eth_message<T: AsRef<[u8]>>(message: T) -> Vec<u8> {
104    const PREFIX: &str = "\x19Ethereum Signed Message:\n";
105    let msg = message.as_ref();
106    let full = [PREFIX.as_bytes(), msg.len().to_string().as_bytes(), msg].concat();
107    easy_hasher::raw_keccak256(full).to_vec()
108}
109
110/// Converts a compressed public key (33 bytes) to a 20-byte Ethereum address.
111///
112/// # Arguments
113/// * `public_key` - A 33-byte SEC1 compressed public key
114///
115/// # Returns
116/// * `Ok(Vec<u8>)` - The last 20 bytes of the Keccak256 hash of the uncompressed key
117/// * `Err` if decoding fails or key length is incorrect
118fn get_address_from_public_key(public_key: [u8; 33]) -> DynamicResult<Vec<u8>> {
119    // Parse and decompress the SEC1 public key
120    let pub_key_arr: [u8; 33] = public_key[..].try_into()?;
121    let pub_key = libsecp256k1::PublicKey::parse_compressed(&pub_key_arr)?.serialize();
122
123    // Drop the prefix byte (0x04) and hash the remaining 64 bytes
124    let hash = easy_hasher::raw_keccak256(pub_key[1..].to_vec()).to_vec();
125
126    // Ethereum address is the last 20 bytes of the hash
127    let address_bytes: [u8; 20] = hash[12..]
128        .try_into()
129        .map_err(|_| "Invalid address length")?;
130
131    Ok(address_bytes.into())
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_valid_signature() {
140        let message = b"4RvWUp3E9YerY78Kn5UyyEQPTiFs0tIr/mhAeCbwIpY=";
141        let public_key = "0xd1798d6b74ef965d6a60f45e0036f44aed3dfa1b".to_string();
142        let expected_signature = hex::decode(
143            "88bd1f104e132178aea55731be455a5c91b3e15b46f2599e9472d926270d458f4116eea0273fb5dc36238992154afc652aa7c1d91569b596db00146b4e5443fa1b"
144        ).unwrap();
145
146        // Validate that the signature recovers the expected public key
147        let is_valid = validate_ecdsa_signature(&public_key, message, &expected_signature).unwrap();
148        assert!(is_valid, "invalid message or signature");
149    }
150
151    #[test]
152    fn test_valid_string_signature() {
153        let message = "4RvWUp3E9YerY78Kn5UyyEQPTiFs0tIr/mhAeCbwIpY=".to_string();
154        let public_key = "0xd1798d6b74ef965d6a60f45e0036f44aed3dfa1b".to_string();
155        let expected_signature = "0x88bd1f104e132178aea55731be455a5c91b3e15b46f2599e9472d926270d458f4116eea0273fb5dc36238992154afc652aa7c1d91569b596db00146b4e5443fa1b".to_string();
156
157        // Validate that the signature recovers the expected public key
158        let is_valid =
159            validate_ecdsa_signature_string(public_key, message, expected_signature).unwrap();
160        assert!(is_valid, "invalid message or signature");
161    }
162
163    #[test]
164    fn test_invalid_signature() {
165        let message = b"4RvWUp3E9YerY78Kn5UyyEQPTiFs0tIr/mhAeCbwIpY=";
166        let public_key = "0xd1798d6b74ef965d6a60f45e0036f44aed3dfa1b".to_string();
167        let invalid_signature = hex::decode(
168            "98bd1f104e132178aea55731be455a5c91b3e15b46f2599e9472d926270d458f4116eea0273fb5dc36238992154afc652aa7c1d91569b596db00146b4e5443fa1b"
169        ).unwrap();
170
171        // Validate that the signature fails to recovers the expected public key
172        let is_valid = validate_ecdsa_signature(&public_key, message, &invalid_signature).unwrap();
173        assert!(!is_valid);
174    }
175}