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 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 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 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 return Err(SignatureError::InvalidClientData);
126 }
127
128 let verifying_key = VerifyingKey::from_sec1_bytes(public_key)
130 .map_err(|_| SignatureError::InvalidPublicKey)?;
131
132 let signature = Signature::from_der(signature).map_err(|_| SignatureError::Invalid)?;
134
135 let client_data_hash = sha256_bytes(client_data_json);
137
138 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 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 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 signed_public_key.wallet_signature.raw_bytes,
182 None,
183 ))
184 }
185
186 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 *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 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 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; 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}