Skip to main content

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    use super::*;
226    use crate::associations::{
227        InstallationKeyContext, MemberIdentifier, SignatureKind,
228        test_utils::{MockSmartContractSignatureVerifier, WalletTestExt},
229        verified_signature::VerifiedSignature,
230    };
231    use alloy::signers::Signer;
232    use alloy::signers::local::PrivateKeySigner;
233    use prost::Message;
234    use xmtp_common::rand_hexstring;
235    use xmtp_cryptography::{CredentialSign, XmtpInstallationCredential};
236
237    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
238    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
239    async fn test_recoverable_ecdsa() {
240        let wallet = PrivateKeySigner::random();
241        let signature_text = "test signature body";
242
243        let sig_bytes: Vec<u8> = wallet
244            .sign_message(signature_text.as_bytes())
245            .await
246            .unwrap()
247            .into();
248        let verified_sig = VerifiedSignature::from_recoverable_ecdsa(signature_text, &sig_bytes)
249            .expect("should succeed");
250
251        assert_eq!(verified_sig.signer, wallet.member_identifier());
252        assert_eq!(verified_sig.kind, SignatureKind::Erc191);
253        assert_eq!(verified_sig.raw_bytes, sig_bytes);
254    }
255
256    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
257    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
258    async fn test_recoverable_ecdsa_incorrect() {
259        let wallet = PrivateKeySigner::random();
260        let signature_text = "test signature body";
261
262        let sig_bytes: Vec<u8> = wallet
263            .sign_message(signature_text.as_bytes())
264            .await
265            .unwrap()
266            .into();
267
268        let verified_sig =
269            VerifiedSignature::from_recoverable_ecdsa("wrong text again", &sig_bytes).unwrap();
270        assert_ne!(verified_sig.signer, wallet.member_identifier());
271    }
272
273    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
274    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
275    async fn test_installation_key() {
276        let key = XmtpInstallationCredential::new();
277        let verifying_key = key.verifying_key();
278        let signature_text = "test signature text";
279        let sig = key
280            .credential_sign::<InstallationKeyContext>(signature_text)
281            .unwrap();
282
283        let verified_sig =
284            VerifiedSignature::from_installation_key(signature_text, sig.as_slice(), verifying_key)
285                .expect("should succeed");
286        let expected = MemberIdentifier::installation(verifying_key.as_bytes().to_vec());
287        assert_eq!(expected, verified_sig.signer);
288        assert_eq!(SignatureKind::InstallationKey, verified_sig.kind);
289        assert_eq!(verified_sig.raw_bytes, sig.as_slice());
290
291        // Make sure it fails with the wrong signature text
292        VerifiedSignature::from_installation_key(
293            "wrong signature text",
294            sig.as_slice(),
295            verifying_key,
296        )
297        .expect_err("should fail with incorrect signature text");
298
299        // Make sure it fails with the wrong verifying key
300        VerifiedSignature::from_installation_key(
301            signature_text,
302            sig.as_slice(),
303            XmtpInstallationCredential::new().verifying_key(),
304        )
305        .expect_err("should fail with incorrect verifying key");
306    }
307
308    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
309    #[cfg_attr(not(target_arch = "wasm32"), test)]
310    fn validate_good_key_round_trip() {
311        let proto_bytes = vec![
312            10, 79, 8, 192, 195, 165, 174, 203, 153, 231, 213, 23, 26, 67, 10, 65, 4, 216, 84, 174,
313            252, 198, 225, 219, 168, 239, 166, 62, 233, 206, 108, 53, 155, 87, 132, 8, 43, 91, 36,
314            91, 81, 93, 213, 67, 241, 69, 5, 31, 249, 186, 129, 119, 144, 4, 44, 54, 76, 185, 95,
315            61, 23, 231, 72, 7, 169, 18, 70, 113, 79, 173, 82, 13, 37, 146, 201, 43, 174, 180, 33,
316            125, 43, 18, 70, 18, 68, 10, 64, 7, 136, 100, 172, 155, 247, 230, 255, 253, 247, 78,
317            50, 212, 226, 41, 78, 239, 183, 136, 247, 122, 88, 155, 245, 219, 183, 215, 202, 42,
318            89, 162, 128, 96, 96, 120, 131, 17, 70, 38, 231, 2, 27, 91, 29, 66, 110, 128, 140, 1,
319            42, 217, 185, 2, 181, 208, 100, 143, 143, 219, 159, 174, 1, 233, 191, 16, 1,
320        ];
321        let account_address = "0x220ca99fb7fafa18cb623d924794dde47b4bc2e9";
322
323        let proto = LegacySignedPublicKeyProto::decode(proto_bytes.as_slice()).unwrap();
324        let validated_key = ValidatedLegacySignedPublicKey::try_from(proto)
325            .expect("Key should validate successfully");
326        let proto: LegacySignedPublicKeyProto = validated_key.into();
327        let validated_key = ValidatedLegacySignedPublicKey::try_from(proto)
328            .expect("Key should still validate successfully");
329        assert_eq!(validated_key.account_address(), account_address);
330    }
331
332    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
333    #[cfg_attr(not(target_arch = "wasm32"), test)]
334    fn validate_malformed_key() {
335        let proto_bytes = vec![
336            10, 79, 8, 192, 195, 165, 174, 203, 153, 231, 213, 23, 26, 67, 10, 65, 4, 216, 84, 174,
337            252, 198, 225, 219, 168, 239, 166, 62, 233, 206, 108, 53, 155, 87, 132, 8, 43, 91, 36,
338            91, 81, 93, 213, 67, 241, 69, 5, 31, 249, 186, 129, 119, 144, 4, 44, 54, 76, 185, 95,
339            61, 23, 231, 72, 7, 169, 18, 70, 113, 79, 173, 82, 13, 37, 146, 201, 43, 174, 180, 33,
340            125, 43, 18, 70, 18, 68, 10, 64, 7, 136, 100, 172, 155, 247, 230, 255, 253, 247, 78,
341            50, 212, 226, 41, 78, 239, 183, 136, 247, 122, 88, 155, 245, 219, 183, 215, 202, 42,
342            89, 162, 128, 96, 96, 120, 131, 17, 70, 38, 231, 2, 27, 91, 29, 66, 110, 128, 140, 1,
343            42, 217, 185, 2, 181, 208, 100, 143, 143, 219, 159, 174, 1, 233, 191, 16, 1,
344        ];
345        let mut proto = LegacySignedPublicKeyProto::decode(proto_bytes.as_slice()).unwrap();
346        proto.key_bytes[0] += 1; // Corrupt the serialized key data
347        assert!(matches!(
348            ValidatedLegacySignedPublicKey::try_from(proto),
349            Err(super::SignatureError::Invalid)
350        ));
351    }
352
353    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
354    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
355    async fn test_smart_contract_wallet() {
356        let mock_verifier = MockSmartContractSignatureVerifier::new(true);
357        let chain_id: u64 = 24;
358        let account_address = rand_hexstring();
359        let account_id = AccountId::new(format!("eip155:{chain_id}"), account_address.clone());
360        let signature_text = "test_smart_contract_wallet_signature";
361        let signature_bytes = &[1, 2, 3];
362        let mut block_number = Some(1);
363
364        let verified_sig = VerifiedSignature::from_smart_contract_wallet(
365            signature_text,
366            mock_verifier,
367            signature_bytes,
368            account_id,
369            &mut block_number,
370        )
371        .await
372        .expect("should validate");
373        assert_eq!(
374            verified_sig.signer,
375            MemberIdentifier::eth(account_address).unwrap()
376        );
377        assert_eq!(verified_sig.kind, SignatureKind::Erc1271);
378        assert_eq!(verified_sig.raw_bytes, signature_bytes);
379        assert_eq!(verified_sig.chain_id, Some(chain_id));
380    }
381}