Skip to main content

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