xmtp_id/associations/
verified_signature.rs

1#![allow(dead_code)]
2use super::{
3    AccountId, InstallationKeyContext, MemberIdentifier, SignatureError, SignatureKind,
4    ValidatedLegacySignedPublicKey, ident, to_lower_s,
5};
6use crate::scw_verifier::SmartContractSignatureVerifier;
7use alloy::primitives::Signature as EtherSignature;
8use alloy::signers::k256::ecdsa::VerifyingKey as EcdsaVerifyingKey;
9use alloy::signers::utils::public_key_to_address;
10use base64::Engine;
11use base64::prelude::BASE64_URL_SAFE_NO_PAD;
12use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
13use xmtp_cryptography::CredentialVerify;
14use xmtp_cryptography::hash::sha256_bytes;
15use xmtp_cryptography::signature::h160addr_to_string;
16use xmtp_proto::xmtp::message_contents::SignedPublicKey as LegacySignedPublicKeyProto;
17
18#[derive(Debug, Clone)]
19pub struct VerifiedSignature {
20    pub signer: MemberIdentifier,
21    pub kind: SignatureKind,
22    pub raw_bytes: Vec<u8>,
23    pub chain_id: Option<u64>,
24}
25
26#[derive(serde::Deserialize)]
27struct ClientDataJson {
28    origin: String,
29    challenge: String,
30}
31
32impl VerifiedSignature {
33    pub fn new(
34        signer: MemberIdentifier,
35        kind: SignatureKind,
36        raw_bytes: Vec<u8>,
37        chain_id: Option<u64>,
38    ) -> Self {
39        Self {
40            signer,
41            kind,
42            raw_bytes,
43            chain_id,
44        }
45    }
46
47    /**
48     * Verifies an ECDSA signature against the provided signature text.
49     * Returns a VerifiedSignature if the signature is valid, otherwise returns an error.
50     */
51    pub fn from_recoverable_ecdsa<Text: AsRef<str>>(
52        signature_text: Text,
53        signature_bytes: &[u8],
54    ) -> Result<Self, SignatureError> {
55        let normalized_signature_bytes = to_lower_s(signature_bytes)?;
56        let signature = EtherSignature::try_from(normalized_signature_bytes.as_slice())?;
57        let address = signature.recover_address_from_msg(signature_text.as_ref())?;
58        let address = h160addr_to_string(address);
59
60        Ok(Self::new(
61            MemberIdentifier::eth(address)?,
62            SignatureKind::Erc191,
63            normalized_signature_bytes.to_vec(),
64            None,
65        ))
66    }
67
68    /**
69     * Verifies an ECDSA signature against the provided signature text and ensures that the recovered
70     * address matches the expected address.
71     */
72    pub fn from_recoverable_ecdsa_with_expected_address<Text: AsRef<str>>(
73        signature_text: Text,
74        signature_bytes: &[u8],
75        expected_address: Text,
76    ) -> Result<Self, SignatureError> {
77        let partially_verified = Self::from_recoverable_ecdsa(signature_text, signature_bytes)?;
78        if partially_verified
79            .signer
80            .eth_address()
81            .ok_or(SignatureError::Invalid)?
82            .to_lowercase()
83            != expected_address.as_ref().to_lowercase()
84        {
85            return Err(SignatureError::Invalid);
86        }
87
88        Ok(partially_verified)
89    }
90
91    /**
92     * Verifies an installation key signature against the provided signature text and verifying key bytes.
93     * Returns a VerifiedSignature if the signature is valid, otherwise returns an error.
94     */
95    pub fn from_installation_key<Text: AsRef<str>>(
96        signature_text: Text,
97        signature_bytes: &[u8],
98        verifying_key: ed25519_dalek::VerifyingKey,
99    ) -> Result<Self, SignatureError> {
100        verifying_key.credential_verify::<InstallationKeyContext>(
101            signature_text,
102            signature_bytes.try_into()?,
103        )?;
104        Ok(Self::new(
105            MemberIdentifier::installation(verifying_key.as_bytes().to_vec()),
106            SignatureKind::InstallationKey,
107            signature_bytes.to_vec(),
108            None,
109        ))
110    }
111
112    pub fn from_passkey<Text: AsRef<str>>(
113        signature_text: Text,
114        public_key: &[u8],
115        signature: &[u8],
116        authenticator_data: &[u8],
117        client_data_json: &[u8],
118    ) -> Result<Self, SignatureError> {
119        let client_data: ClientDataJson = serde_json::from_slice(client_data_json)
120            .map_err(|_| SignatureError::InvalidClientData)?;
121
122        let signature_text = BASE64_URL_SAFE_NO_PAD.encode(signature_text.as_ref());
123        if signature_text != client_data.challenge {
124            // Challenge needs to match signature text
125            return Err(SignatureError::InvalidClientData);
126        }
127
128        // 1. Parse the public key from raw bytes
129        let verifying_key = VerifyingKey::from_sec1_bytes(public_key)
130            .map_err(|_| SignatureError::InvalidPublicKey)?;
131
132        // 2. Parse the signature
133        let signature = Signature::from_der(signature).map_err(|_| SignatureError::Invalid)?;
134
135        // 3. Hash the client data
136        let client_data_hash = sha256_bytes(client_data_json);
137
138        // 4. Construct the verification data (authenticator_data + client_data_hash)
139        let mut verification_data =
140            Vec::with_capacity(authenticator_data.len() + client_data_hash.len());
141        verification_data.extend_from_slice(authenticator_data);
142        verification_data.extend_from_slice(&client_data_hash);
143
144        // 5. Verify the signature
145        verifying_key.verify(&verification_data, &signature)?;
146
147        Ok(Self::new(
148            MemberIdentifier::Passkey(ident::Passkey {
149                key: public_key.to_vec(),
150                relying_party: Some(client_data.origin),
151            }),
152            SignatureKind::P256,
153            signature.to_vec(),
154            None,
155        ))
156    }
157
158    /// Verifies a legacy delegated signature and recovers the wallet address responsible
159    /// associated with the signer.
160    pub fn from_legacy_delegated<Text: AsRef<str>>(
161        signature_text: Text,
162        signature_bytes: &[u8],
163        signed_public_key_proto: LegacySignedPublicKeyProto,
164    ) -> Result<Self, SignatureError> {
165        let verified_legacy_signature =
166            Self::from_recoverable_ecdsa(signature_text, signature_bytes)?;
167        let signed_public_key: ValidatedLegacySignedPublicKey =
168            signed_public_key_proto.try_into()?;
169        let public_key = EcdsaVerifyingKey::from_sec1_bytes(&signed_public_key.public_key_bytes)?;
170        let address = h160addr_to_string(public_key_to_address(&public_key));
171
172        if MemberIdentifier::eth(address)? != verified_legacy_signature.signer {
173            return Err(SignatureError::Invalid);
174        }
175
176        Ok(Self::new(
177            MemberIdentifier::eth(signed_public_key.account_address)?,
178            SignatureKind::LegacyDelegated,
179            // Must use the wallet signature bytes, since those are the ones we care about making unique.
180            // This protects against using the legacy key more than once in the Identity Update Log
181            signed_public_key.wallet_signature.raw_bytes,
182            None,
183        ))
184    }
185
186    /// Verifies a smart contract wallet signature using the provided signature verifier.
187    pub async fn from_smart_contract_wallet<Text: AsRef<str>>(
188        signature_text: Text,
189        signature_verifier: impl SmartContractSignatureVerifier,
190        signature_bytes: &[u8],
191        account_id: AccountId,
192        block_number: &mut Option<u64>,
193    ) -> Result<Self, SignatureError> {
194        let response = signature_verifier
195            .is_valid_signature(
196                account_id.clone(),
197                alloy::primitives::eip191_hash_message(signature_text.as_ref()).into(),
198                signature_bytes.to_vec().into(),
199                *block_number,
200            )
201            .await?;
202
203        if response.is_valid {
204            // set the block the signature was validated on
205            *block_number = response.block_number;
206
207            Ok(Self::new(
208                MemberIdentifier::eth(account_id.get_account_address())?,
209                SignatureKind::Erc1271,
210                signature_bytes.to_vec(),
211                Some(account_id.get_chain_id_u64()?),
212            ))
213        } else {
214            tracing::error!(
215                "Smart contract wallet signature is invalid {:?}",
216                response.error
217            );
218            Err(SignatureError::Invalid)
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    #[cfg(target_arch = "wasm32")]
226    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker);
227
228    use super::*;
229    use crate::associations::{
230        InstallationKeyContext, MemberIdentifier, SignatureKind,
231        test_utils::{MockSmartContractSignatureVerifier, WalletTestExt},
232        verified_signature::VerifiedSignature,
233    };
234    use alloy::signers::Signer;
235    use alloy::signers::local::PrivateKeySigner;
236    use prost::Message;
237    use xmtp_common::rand_hexstring;
238    use xmtp_cryptography::{CredentialSign, XmtpInstallationCredential};
239
240    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
241    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
242    async fn test_recoverable_ecdsa() {
243        let wallet = PrivateKeySigner::random();
244        let signature_text = "test signature body";
245
246        let sig_bytes: Vec<u8> = wallet
247            .sign_message(signature_text.as_bytes())
248            .await
249            .unwrap()
250            .into();
251        let verified_sig = VerifiedSignature::from_recoverable_ecdsa(signature_text, &sig_bytes)
252            .expect("should succeed");
253
254        assert_eq!(verified_sig.signer, wallet.member_identifier());
255        assert_eq!(verified_sig.kind, SignatureKind::Erc191);
256        assert_eq!(verified_sig.raw_bytes, sig_bytes);
257    }
258
259    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
260    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
261    async fn test_recoverable_ecdsa_incorrect() {
262        let wallet = PrivateKeySigner::random();
263        let signature_text = "test signature body";
264
265        let sig_bytes: Vec<u8> = wallet
266            .sign_message(signature_text.as_bytes())
267            .await
268            .unwrap()
269            .into();
270
271        let verified_sig =
272            VerifiedSignature::from_recoverable_ecdsa("wrong text again", &sig_bytes).unwrap();
273        assert_ne!(verified_sig.signer, wallet.member_identifier());
274    }
275
276    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
277    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
278    async fn test_installation_key() {
279        let key = XmtpInstallationCredential::new();
280        let verifying_key = key.verifying_key();
281        let signature_text = "test signature text";
282        let sig = key
283            .credential_sign::<InstallationKeyContext>(signature_text)
284            .unwrap();
285
286        let verified_sig =
287            VerifiedSignature::from_installation_key(signature_text, sig.as_slice(), verifying_key)
288                .expect("should succeed");
289        let expected = MemberIdentifier::installation(verifying_key.as_bytes().to_vec());
290        assert_eq!(expected, verified_sig.signer);
291        assert_eq!(SignatureKind::InstallationKey, verified_sig.kind);
292        assert_eq!(verified_sig.raw_bytes, sig.as_slice());
293
294        // Make sure it fails with the wrong signature text
295        VerifiedSignature::from_installation_key(
296            "wrong signature text",
297            sig.as_slice(),
298            verifying_key,
299        )
300        .expect_err("should fail with incorrect signature text");
301
302        // Make sure it fails with the wrong verifying key
303        VerifiedSignature::from_installation_key(
304            signature_text,
305            sig.as_slice(),
306            XmtpInstallationCredential::new().verifying_key(),
307        )
308        .expect_err("should fail with incorrect verifying key");
309    }
310
311    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
312    #[cfg_attr(not(target_arch = "wasm32"), test)]
313    fn validate_good_key_round_trip() {
314        let proto_bytes = vec![
315            10, 79, 8, 192, 195, 165, 174, 203, 153, 231, 213, 23, 26, 67, 10, 65, 4, 216, 84, 174,
316            252, 198, 225, 219, 168, 239, 166, 62, 233, 206, 108, 53, 155, 87, 132, 8, 43, 91, 36,
317            91, 81, 93, 213, 67, 241, 69, 5, 31, 249, 186, 129, 119, 144, 4, 44, 54, 76, 185, 95,
318            61, 23, 231, 72, 7, 169, 18, 70, 113, 79, 173, 82, 13, 37, 146, 201, 43, 174, 180, 33,
319            125, 43, 18, 70, 18, 68, 10, 64, 7, 136, 100, 172, 155, 247, 230, 255, 253, 247, 78,
320            50, 212, 226, 41, 78, 239, 183, 136, 247, 122, 88, 155, 245, 219, 183, 215, 202, 42,
321            89, 162, 128, 96, 96, 120, 131, 17, 70, 38, 231, 2, 27, 91, 29, 66, 110, 128, 140, 1,
322            42, 217, 185, 2, 181, 208, 100, 143, 143, 219, 159, 174, 1, 233, 191, 16, 1,
323        ];
324        let account_address = "0x220ca99fb7fafa18cb623d924794dde47b4bc2e9";
325
326        let proto = LegacySignedPublicKeyProto::decode(proto_bytes.as_slice()).unwrap();
327        let validated_key = ValidatedLegacySignedPublicKey::try_from(proto)
328            .expect("Key should validate successfully");
329        let proto: LegacySignedPublicKeyProto = validated_key.into();
330        let validated_key = ValidatedLegacySignedPublicKey::try_from(proto)
331            .expect("Key should still validate successfully");
332        assert_eq!(validated_key.account_address(), account_address);
333    }
334
335    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
336    #[cfg_attr(not(target_arch = "wasm32"), test)]
337    fn validate_malformed_key() {
338        let proto_bytes = vec![
339            10, 79, 8, 192, 195, 165, 174, 203, 153, 231, 213, 23, 26, 67, 10, 65, 4, 216, 84, 174,
340            252, 198, 225, 219, 168, 239, 166, 62, 233, 206, 108, 53, 155, 87, 132, 8, 43, 91, 36,
341            91, 81, 93, 213, 67, 241, 69, 5, 31, 249, 186, 129, 119, 144, 4, 44, 54, 76, 185, 95,
342            61, 23, 231, 72, 7, 169, 18, 70, 113, 79, 173, 82, 13, 37, 146, 201, 43, 174, 180, 33,
343            125, 43, 18, 70, 18, 68, 10, 64, 7, 136, 100, 172, 155, 247, 230, 255, 253, 247, 78,
344            50, 212, 226, 41, 78, 239, 183, 136, 247, 122, 88, 155, 245, 219, 183, 215, 202, 42,
345            89, 162, 128, 96, 96, 120, 131, 17, 70, 38, 231, 2, 27, 91, 29, 66, 110, 128, 140, 1,
346            42, 217, 185, 2, 181, 208, 100, 143, 143, 219, 159, 174, 1, 233, 191, 16, 1,
347        ];
348        let mut proto = LegacySignedPublicKeyProto::decode(proto_bytes.as_slice()).unwrap();
349        proto.key_bytes[0] += 1; // Corrupt the serialized key data
350        assert!(matches!(
351            ValidatedLegacySignedPublicKey::try_from(proto),
352            Err(super::SignatureError::Invalid)
353        ));
354    }
355
356    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
357    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
358    async fn test_smart_contract_wallet() {
359        let mock_verifier = MockSmartContractSignatureVerifier::new(true);
360        let chain_id: u64 = 24;
361        let account_address = rand_hexstring();
362        let account_id = AccountId::new(format!("eip155:{chain_id}"), account_address.clone());
363        let signature_text = "test_smart_contract_wallet_signature";
364        let signature_bytes = &[1, 2, 3];
365        let mut block_number = Some(1);
366
367        let verified_sig = VerifiedSignature::from_smart_contract_wallet(
368            signature_text,
369            mock_verifier,
370            signature_bytes,
371            account_id,
372            &mut block_number,
373        )
374        .await
375        .expect("should validate");
376        assert_eq!(
377            verified_sig.signer,
378            MemberIdentifier::eth(account_address).unwrap()
379        );
380        assert_eq!(verified_sig.kind, SignatureKind::Erc1271);
381        assert_eq!(verified_sig.raw_bytes, signature_bytes);
382        assert_eq!(verified_sig.chain_id, Some(chain_id));
383    }
384}