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 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 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 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; 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}