1#![allow(dead_code)]
2use super::{
3 AccountId, InstallationKeyContext, MemberIdentifier, SignatureError, SignatureKind, ident,
4};
5use crate::scw_verifier::SmartContractSignatureVerifier;
6use alloy::primitives::Signature as EtherSignature;
7use base64::Engine;
8use base64::prelude::BASE64_URL_SAFE_NO_PAD;
9use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
10use xmtp_cryptography::CredentialVerify;
11use xmtp_cryptography::hash::sha256_bytes;
12use xmtp_cryptography::signature::h160addr_to_string;
13
14#[derive(Debug, Clone)]
15pub struct VerifiedSignature {
16 pub signer: MemberIdentifier,
17 pub kind: SignatureKind,
18 pub replay_key: Vec<u8>,
22 pub chain_id: Option<u64>,
23}
24
25#[derive(serde::Deserialize)]
26struct ClientDataJson {
27 origin: String,
28 challenge: String,
29}
30
31impl VerifiedSignature {
32 pub fn new(
33 signer: MemberIdentifier,
34 kind: SignatureKind,
35 replay_key: Vec<u8>,
36 chain_id: Option<u64>,
37 ) -> Self {
38 Self {
39 signer,
40 kind,
41 replay_key,
42 chain_id,
43 }
44 }
45
46 pub fn from_recoverable_ecdsa<Text: AsRef<str>>(
50 signature_text: Text,
51 signature_bytes: &[u8],
52 ) -> Result<Self, SignatureError> {
53 let Ok(bytes @ [.., 0 | 1 | 27 | 28]) = <[u8; 65]>::try_from(signature_bytes) else {
54 return Err(SignatureError::Invalid);
55 };
56 let signature = EtherSignature::from_raw_array(&bytes)?.normalized_s();
57 let address = signature.recover_address_from_msg(signature_text.as_ref())?;
58
59 Ok(Self::new(
60 MemberIdentifier::eth(h160addr_to_string(address))?,
61 SignatureKind::Erc191,
62 signature.as_rsy().to_vec(),
63 None,
64 ))
65 }
66
67 pub fn from_recoverable_ecdsa_with_expected_address<Text: AsRef<str>>(
72 signature_text: Text,
73 signature_bytes: &[u8],
74 expected_address: Text,
75 ) -> Result<Self, SignatureError> {
76 let partially_verified = Self::from_recoverable_ecdsa(signature_text, signature_bytes)?;
77 if partially_verified
78 .signer
79 .eth_address()
80 .ok_or(SignatureError::Invalid)?
81 .to_lowercase()
82 != expected_address.as_ref().to_lowercase()
83 {
84 return Err(SignatureError::Invalid);
85 }
86
87 Ok(partially_verified)
88 }
89
90 pub fn from_installation_key<Text: AsRef<str>>(
95 signature_text: Text,
96 signature_bytes: &[u8],
97 verifying_key: ed25519_dalek::VerifyingKey,
98 ) -> Result<Self, SignatureError> {
99 verifying_key.credential_verify::<InstallationKeyContext>(
100 signature_text,
101 signature_bytes.try_into()?,
102 )?;
103 Ok(Self::new(
104 MemberIdentifier::installation(verifying_key.as_bytes().to_vec()),
105 SignatureKind::InstallationKey,
106 signature_bytes.to_vec(),
107 None,
108 ))
109 }
110
111 pub fn from_passkey<Text: AsRef<str>>(
114 signature_text: Text,
115 public_key: &[u8],
116 signature: &[u8],
117 authenticator_data: &[u8],
118 client_data_json: &[u8],
119 ) -> Result<Self, SignatureError> {
120 let client_data: ClientDataJson = serde_json::from_slice(client_data_json)
121 .map_err(|_| SignatureError::InvalidClientData)?;
122
123 let signature_text = BASE64_URL_SAFE_NO_PAD.encode(signature_text.as_ref());
124 if signature_text != client_data.challenge {
125 return Err(SignatureError::InvalidClientData);
127 }
128
129 let verifying_key = VerifyingKey::from_sec1_bytes(public_key)
131 .map_err(|_| SignatureError::InvalidPublicKey)?;
132
133 let signature = Signature::from_der(signature).map_err(|_| SignatureError::Invalid)?;
135
136 let client_data_hash = sha256_bytes(client_data_json);
138
139 let mut verification_data =
141 Vec::with_capacity(authenticator_data.len() + client_data_hash.len());
142 verification_data.extend_from_slice(authenticator_data);
143 verification_data.extend_from_slice(&client_data_hash);
144
145 verifying_key.verify(&verification_data, &signature)?;
147
148 Ok(Self::new(
149 MemberIdentifier::Passkey(ident::Passkey {
150 key: public_key.to_vec(),
151 relying_party: Some(client_data.origin),
152 }),
153 SignatureKind::P256,
154 signature.normalize_s().unwrap_or(signature).to_vec(),
155 None,
156 ))
157 }
158
159 pub async fn from_smart_contract_wallet<Text: AsRef<str>>(
161 signature_text: Text,
162 signature_verifier: impl SmartContractSignatureVerifier,
163 signature_bytes: &[u8],
164 account_id: AccountId,
165 block_number: &mut Option<u64>,
166 ) -> Result<Self, SignatureError> {
167 let chain_id = account_id.eip155_chain_id()?;
169 let response = signature_verifier
170 .is_valid_signature(
171 account_id.clone(),
172 alloy::primitives::eip191_hash_message(signature_text.as_ref()).into(),
173 signature_bytes.to_vec().into(),
174 *block_number,
175 )
176 .await?;
177
178 if response.is_valid {
179 *block_number = response.block_number;
181
182 Ok(Self::new(
183 MemberIdentifier::eth(account_id.get_account_address())?,
184 SignatureKind::Erc1271,
185 signature_bytes.to_vec(),
186 Some(chain_id),
187 ))
188 } else {
189 tracing::error!(
190 "Smart contract wallet signature is invalid {:?}",
191 response.error
192 );
193 Err(SignatureError::Invalid)
194 }
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::{
202 InboxOwner,
203 associations::{
204 InstallationKeyContext, MemberIdentifier, SignatureKind,
205 test_utils::{
206 MockSmartContractSignatureVerifier, WalletTestExt, ecdsa_negated_s_alias,
207 ecdsa_recovery_byte_alias, p256_negated_s_alias,
208 },
209 unverified::UnverifiedSignature,
210 verified_signature::VerifiedSignature,
211 },
212 utils::passkey::PasskeyUser,
213 };
214 use alloy::signers::Signer;
215 use alloy::signers::local::PrivateKeySigner;
216 use xmtp_common::rand_hexstring;
217 use xmtp_cryptography::{CredentialSign, XmtpInstallationCredential};
218
219 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
220 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
221 async fn test_recoverable_ecdsa() {
223 let wallet = PrivateKeySigner::random();
224 let signature_text = "test signature body";
225
226 let signature = wallet
227 .sign_message(signature_text.as_bytes())
228 .await
229 .unwrap();
230 let verified_sig =
231 VerifiedSignature::from_recoverable_ecdsa(signature_text, &signature.as_bytes())
232 .expect("should succeed");
233
234 assert_eq!(verified_sig.signer, wallet.member_identifier());
235 assert_eq!(verified_sig.kind, SignatureKind::Erc191);
236 assert_eq!(verified_sig.replay_key, signature.as_rsy());
237 }
238
239 #[xmtp_common::test(unwrap_try = true)]
243 async fn identity_replay_aliases_share_a_wallet_key() {
245 let wallet = PrivateKeySigner::random();
246 let text = "replay alias";
247 let signature = wallet.sign_message(text.as_bytes()).await?;
248 let bytes = signature.as_bytes().to_vec();
249 let negated = ecdsa_negated_s_alias(&bytes);
250
251 for form in [
252 ecdsa_recovery_byte_alias(&bytes),
253 ecdsa_recovery_byte_alias(&negated),
254 negated,
255 bytes.clone(),
256 ] {
257 let verified = VerifiedSignature::from_recoverable_ecdsa(text, &form)?;
258 assert_eq!(verified.signer, wallet.member_identifier());
259 assert_eq!(verified.replay_key, signature.as_rsy());
260 }
261
262 for v in [2, 26, 29, 35, 37, 255] {
263 let form = [&bytes[..64], &[v]].concat();
264 assert!(VerifiedSignature::from_recoverable_ecdsa(text, &form).is_err());
265 }
266 assert!(VerifiedSignature::from_recoverable_ecdsa(text, &bytes[..64]).is_err());
267 assert!(
268 VerifiedSignature::from_recoverable_ecdsa(text, &[bytes.as_slice(), &[0]].concat())
269 .is_err()
270 );
271 }
272
273 #[xmtp_common::test(unwrap_try = true)]
276 async fn identity_replay_aliases_share_a_passkey_key() {
278 let passkey = PasskeyUser::new().await;
279 let text = "replay alias";
280 let UnverifiedSignature::Passkey(signature) = passkey.sign(text)? else {
281 unreachable!("a passkey signs with a passkey signature")
282 };
283 let verify = |der: &[u8]| {
284 VerifiedSignature::from_passkey(
285 text,
286 &signature.public_key,
287 der,
288 &signature.authenticator_data,
289 &signature.client_data_json,
290 )
291 };
292
293 let original = verify(&signature.signature)?;
294 let negated = verify(&p256_negated_s_alias(&signature.signature))?;
295 assert_eq!(original.signer, negated.signer);
296 assert_eq!(original.replay_key, negated.replay_key);
297 let low_s = Signature::from_slice(&original.replay_key)?;
298 assert!(low_s.normalize_s().is_none());
299 }
300
301 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
302 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
303 async fn test_recoverable_ecdsa_incorrect() {
305 let wallet = PrivateKeySigner::random();
306 let signature_text = "test signature body";
307
308 let sig_bytes: Vec<u8> = wallet
309 .sign_message(signature_text.as_bytes())
310 .await
311 .unwrap()
312 .into();
313
314 let verified_sig =
315 VerifiedSignature::from_recoverable_ecdsa("wrong text again", &sig_bytes).unwrap();
316 assert_ne!(verified_sig.signer, wallet.member_identifier());
317 }
318
319 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
320 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
321 async fn test_installation_key() {
323 let key = XmtpInstallationCredential::new();
324 let verifying_key = key.verifying_key();
325 let signature_text = "test signature text";
326 let sig = key
327 .credential_sign::<InstallationKeyContext>(signature_text)
328 .unwrap();
329
330 let verified_sig =
331 VerifiedSignature::from_installation_key(signature_text, sig.as_slice(), verifying_key)
332 .expect("should succeed");
333 let expected = MemberIdentifier::installation(verifying_key.as_bytes().to_vec());
334 assert_eq!(expected, verified_sig.signer);
335 assert_eq!(SignatureKind::InstallationKey, verified_sig.kind);
336 assert_eq!(verified_sig.replay_key, sig.as_slice());
337
338 VerifiedSignature::from_installation_key(
340 "wrong signature text",
341 sig.as_slice(),
342 verifying_key,
343 )
344 .expect_err("should fail with incorrect signature text");
345
346 VerifiedSignature::from_installation_key(
348 signature_text,
349 sig.as_slice(),
350 XmtpInstallationCredential::new().verifying_key(),
351 )
352 .expect_err("should fail with incorrect verifying key");
353 }
354
355 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
356 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
357 async fn test_smart_contract_wallet() {
358 let mock_verifier = MockSmartContractSignatureVerifier::new(true);
359 let chain_id: u64 = 24;
360 let account_address = rand_hexstring();
361 let account_id = AccountId::new(format!("eip155:{chain_id}"), account_address.clone());
362 let signature_text = "test_smart_contract_wallet_signature";
363 let signature_bytes = &[1, 2, 3];
364 let mut block_number = Some(1);
365
366 let verified_sig = VerifiedSignature::from_smart_contract_wallet(
367 signature_text,
368 mock_verifier,
369 signature_bytes,
370 account_id,
371 &mut block_number,
372 )
373 .await
374 .expect("should validate");
375 assert_eq!(
376 verified_sig.signer,
377 MemberIdentifier::eth(account_address).unwrap()
378 );
379 assert_eq!(verified_sig.kind, SignatureKind::Erc1271);
380 assert_eq!(verified_sig.replay_key, signature_bytes);
381 assert_eq!(verified_sig.chain_id, Some(chain_id));
382 }
383}