xmtp_cryptography/
signature.rs

1use std::array::TryFromSliceError;
2
3use alloy::primitives::{self as alloy_types, Address};
4use hex::FromHexError;
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8use crate::{Secret, configuration::ED25519_KEY_LENGTH};
9
10pub fn to_public_key(private_key: &Secret) -> Result<[u8; ED25519_KEY_LENGTH], TryFromSliceError> {
11    let private_key = private_key.as_slice().try_into()?;
12    let mut computed_public_key = [0u8; ED25519_KEY_LENGTH];
13    libcrux_ed25519::secret_to_public(&mut computed_public_key, &private_key);
14    Ok(computed_public_key)
15}
16
17#[derive(Error, Debug)]
18pub enum SignatureError {
19    #[error("Bad address format")]
20    BadAddressFormat(#[from] hex::FromHexError),
21    #[error("supplied signature is not in the proper format")]
22    BadSignatureFormat(#[from] alloy_types::SignatureError),
23    #[error("Signature is not valid for {addr:?}")]
24    BadSignature { addr: String },
25    #[error(transparent)]
26    Signer(#[from] alloy::signers::Error),
27    #[error("unknown data store error")]
28    Unknown,
29}
30
31#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
32pub enum RecoverableSignature {
33    // This Signature is primary used by EVM compatible accounts. It assumes that the recoveryid
34    // is included in the signature and that all messages passed in have not been prefixed
35    // with '\0x19Ethereum....'
36    Eip191Signature(Vec<u8>),
37}
38
39impl RecoverableSignature {
40    pub fn recover_address(&self, predigest_message: &str) -> Result<String, SignatureError> {
41        match self {
42            Self::Eip191Signature(signature_bytes) => {
43                let signature = alloy_types::Signature::try_from(signature_bytes.as_slice())?;
44                let addr = signature.recover_address_from_msg(predigest_message)?;
45                Ok(addr.to_string())
46            }
47        }
48    }
49}
50
51impl From<Vec<u8>> for RecoverableSignature {
52    fn from(value: Vec<u8>) -> Self {
53        RecoverableSignature::Eip191Signature(value)
54    }
55}
56
57impl From<RecoverableSignature> for Vec<u8> {
58    fn from(value: RecoverableSignature) -> Self {
59        match value {
60            RecoverableSignature::Eip191Signature(bytes) => bytes,
61        }
62    }
63}
64/*
65impl From<(ecdsa::Signature<Secp256k1>, RecoveryId)> for RecoverableSignature {
66    fn from((sig, recid): (ecdsa::Signature<Secp256k1>, RecoveryId)) -> Self {
67        let mut bytes = sig.to_vec();
68        bytes.push(recid.to_byte());
69
70        RecoverableSignature::Eip191Signature(bytes)
71    }
72}
73*/
74impl From<alloy::primitives::Signature> for RecoverableSignature {
75    fn from(value: alloy::primitives::Signature) -> Self {
76        RecoverableSignature::Eip191Signature(value.as_bytes().to_vec())
77    }
78}
79
80pub fn h160addr_to_string(bytes: Address) -> String {
81    let mut s = String::from("0x");
82    s.push_str(&hex::encode(bytes));
83    s.to_lowercase()
84}
85
86/// Check if an string is a valid ethereum address (valid hex and length 20).
87pub fn is_valid_ethereum_address<S: AsRef<str>>(address: S) -> bool {
88    let address = address.as_ref();
89    let address = address.strip_prefix("0x").unwrap_or(address);
90
91    if address.len() != 40 {
92        return false;
93    }
94
95    address.chars().all(|c| c.is_ascii_hexdigit())
96}
97
98#[derive(Debug, Error)]
99pub enum IdentifierValidationError {
100    #[error("invalid addresses: {0:?}")]
101    InvalidAddresses(Vec<String>),
102    #[error("address is invalid hex address")]
103    HexDecode(#[from] FromHexError),
104    #[error("generic error: {0}")]
105    Generic(String),
106}
107
108pub fn sanitize_evm_addresses(
109    account_addresses: &[impl AsRef<str>],
110) -> Result<Vec<String>, IdentifierValidationError> {
111    let mut invalid = account_addresses
112        .iter()
113        .filter(|a| !is_valid_ethereum_address(a))
114        .peekable();
115
116    if invalid.peek().is_some() {
117        return Err(IdentifierValidationError::InvalidAddresses(
118            invalid
119                .map(|addr| addr.as_ref().to_string())
120                .collect::<Vec<_>>(),
121        ));
122    }
123
124    Ok(account_addresses
125        .iter()
126        .map(|addr| addr.as_ref().to_lowercase())
127        .collect())
128}
129
130#[cfg(test)]
131pub mod tests {
132    use super::is_valid_ethereum_address;
133
134    use alloy::signers::SignerSync;
135    use alloy::signers::local::PrivateKeySigner;
136
137    pub fn generate_random_signature(msg: &str) -> (String, Vec<u8>) {
138        let signer = PrivateKeySigner::random();
139        let signature = signer.sign_message_sync(msg.as_bytes()).unwrap();
140        (hex::encode(signer.address()), signature.as_bytes().to_vec())
141    }
142
143    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
144    #[cfg_attr(not(target_arch = "wasm32"), test)]
145    fn test_eth_address() {
146        assert!(is_valid_ethereum_address(
147            "0x7e57Aed10441c8879ce08E45805EC01Ee9689c9f"
148        ));
149        assert!(!is_valid_ethereum_address("123"));
150    }
151}