1use alloy::signers::{SignerSync, local::LocalSigner};
2use ed25519_dalek::{DigestSigner, Signature, VerifyingKey};
3use prost::Message;
4use sha2::{Digest as _, Sha512};
5use std::array::TryFromSliceError;
6use thiserror::Error;
7use xmtp_common::{ErrorCode, RetryableError};
8use xmtp_cryptography::{
9 CredentialSign, CredentialVerify, SignerError, SigningContextProvider,
10 XmtpInstallationCredential,
11};
12use xmtp_proto::xmtp::message_contents::{
13 SignedPrivateKey as LegacySignedPrivateKeyProto, signed_private_key,
14};
15
16use super::{
17 unverified::{UnverifiedLegacyDelegatedSignature, UnverifiedRecoverableEcdsaSignature},
18 verified_signature::VerifiedSignature,
19};
20
21use alloy::signers::k256::ecdsa::Signature as K256Signature;
22
23#[derive(Debug, Error, ErrorCode)]
24pub enum SignatureError {
25 #[error("Malformed legacy key: {0}")]
29 MalformedLegacyKey(String),
30 #[error(transparent)]
31 #[error_code(inherit)]
32 CryptoSignatureError(#[from] xmtp_cryptography::signature::SignatureError),
33 #[error(transparent)]
34 #[error_code(inherit)]
35 VerifierError(#[from] crate::scw_verifier::VerifierError),
36 #[error("ed25519 Signature failed {0}")]
40 Ed25519Error(#[from] ed25519_dalek::SignatureError),
41 #[error(transparent)]
45 TryFromSliceError(#[from] TryFromSliceError),
46 #[error("Signature validation failed")]
50 Invalid,
51 #[error(transparent)]
52 #[error_code(inherit)]
53 AddressValidationError(#[from] xmtp_cryptography::signature::IdentifierValidationError),
54 #[error(transparent)]
58 UrlParseError(#[from] url::ParseError),
59 #[error(transparent)]
63 DecodeError(#[from] prost::DecodeError),
64 #[error(transparent)]
65 #[error_code(inherit)]
66 AccountIdError(#[from] AccountIdError),
67 #[error(transparent)]
71 Signer(#[from] SignerError),
72 #[error("Invalid public key")]
76 InvalidPublicKey,
77 #[error("client_data is invalid")]
81 InvalidClientData,
82 #[error(transparent)]
86 SignerError(#[from] alloy::signers::Error),
87 #[error(transparent)]
91 Signature(#[from] alloy::primitives::SignatureError),
92}
93
94impl RetryableError for SignatureError {
95 fn is_retryable(&self) -> bool {
96 match self {
97 SignatureError::VerifierError(e) => e.is_retryable(),
102 _ => false,
103 }
104 }
105}
106
107pub struct InboxIdInstallationCredential;
109
110pub struct InstallationKeyContext;
111pub struct PublicContext;
112
113impl CredentialSign<InboxIdInstallationCredential> for XmtpInstallationCredential {
114 type Error = SignatureError;
115
116 fn credential_sign<T: SigningContextProvider>(
117 &self,
118 text: impl AsRef<str>,
119 ) -> Result<Vec<u8>, Self::Error> {
120 let mut prehashed: Sha512 = Sha512::new();
121 prehashed.update(text.as_ref());
122 let context = self.with_context(T::context())?;
123 let sig = context
124 .try_sign_digest(prehashed)
125 .map_err(SignatureError::from)?;
126 Ok(sig.to_bytes().into())
127 }
128}
129
130impl CredentialVerify<InboxIdInstallationCredential> for ed25519_dalek::VerifyingKey {
131 type Error = SignatureError;
132
133 fn credential_verify<T: SigningContextProvider>(
134 &self,
135 signature_text: impl AsRef<str>,
136 signature_bytes: &[u8; 64],
137 ) -> Result<(), Self::Error> {
138 let signature = Signature::from_bytes(signature_bytes);
139 let mut prehashed = Sha512::new();
140 prehashed.update(signature_text.as_ref());
141 self.verify_prehashed(prehashed, Some(T::context()), &signature)?;
142 Ok(())
143 }
144}
145
146impl SigningContextProvider for InstallationKeyContext {
147 fn context() -> &'static [u8] {
148 crate::constants::INSTALLATION_KEY_SIGNATURE_CONTEXT
149 }
150}
151
152impl SigningContextProvider for PublicContext {
153 fn context() -> &'static [u8] {
154 crate::constants::PUBLIC_SIGNATURE_CONTEXT
155 }
156}
157
158pub fn verify_signed_with_public_context(
159 signature_text: impl AsRef<str>,
160 signature_bytes: &[u8; 64],
161 public_key: &[u8; 32],
162) -> Result<(), SignatureError> {
163 let verifying_key = VerifyingKey::from_bytes(public_key)?;
164 verifying_key.credential_verify::<PublicContext>(signature_text, signature_bytes)
165}
166
167#[derive(Clone, Debug, PartialEq)]
168pub enum SignatureKind {
169 Erc191,
171 Erc1271,
172 InstallationKey,
173 LegacyDelegated,
174 P256,
175}
176
177impl std::fmt::Display for SignatureKind {
178 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
179 match self {
180 SignatureKind::Erc191 => write!(f, "erc-191"),
181 SignatureKind::Erc1271 => write!(f, "erc-1271"),
182 SignatureKind::InstallationKey => write!(f, "installation-key"),
183 SignatureKind::LegacyDelegated => write!(f, "legacy-delegated"),
184 SignatureKind::P256 => write!(f, "p256"),
185 }
186 }
187}
188
189#[derive(Debug, Error, ErrorCode)]
190pub enum AccountIdError {
191 #[error("Chain ID is not a valid u64")]
195 InvalidChainId,
196 #[error("Chain ID is not prefixed with eip155:")]
200 MissingEip155Prefix,
201}
202
203#[derive(Debug, Clone, PartialEq)]
205pub struct AccountId {
206 pub(crate) chain_id: String,
207 pub(crate) account_address: String,
208}
209
210impl AccountId {
211 pub fn new(chain_id: String, account_address: String) -> Self {
212 AccountId {
213 chain_id,
214 account_address,
215 }
216 }
217
218 pub fn new_evm(chain_id: u64, account_address: String) -> Self {
219 Self::new(format!("eip155:{}", chain_id), account_address)
220 }
221
222 pub fn is_evm_chain(&self) -> bool {
223 self.chain_id.starts_with("eip155")
224 }
225
226 pub fn get_account_address(&self) -> &str {
227 &self.account_address
228 }
229
230 pub fn get_chain_id(&self) -> &str {
231 &self.chain_id
232 }
233
234 pub fn get_chain_id_u64(&self) -> Result<u64, AccountIdError> {
235 let stripped = self
236 .chain_id
237 .strip_prefix("eip155:")
238 .ok_or(AccountIdError::MissingEip155Prefix)?;
239
240 stripped
241 .parse::<u64>()
242 .map_err(|_| AccountIdError::InvalidChainId)
243 }
244}
245
246pub fn sign_with_legacy_key(
248 signature_text: String,
249 legacy_signed_private_key: Vec<u8>,
250) -> Result<UnverifiedLegacyDelegatedSignature, SignatureError> {
251 let legacy_signed_private_key_proto =
252 LegacySignedPrivateKeyProto::decode(legacy_signed_private_key.as_slice())?;
253 let signed_private_key::Union::Secp256k1(secp256k1) = legacy_signed_private_key_proto
254 .union
255 .ok_or(SignatureError::MalformedLegacyKey(
256 "Missing secp256k1.union field".to_string(),
257 ))?;
258 let legacy_private_key = secp256k1.bytes;
259 let signer = LocalSigner::from_slice(legacy_private_key.as_slice())?;
260 let signature = signer.sign_message_sync(signature_text.as_bytes())?;
261
262 let legacy_signed_public_key_proto =
263 legacy_signed_private_key_proto
264 .public_key
265 .ok_or(SignatureError::MalformedLegacyKey(
266 "Missing public_key field".to_string(),
267 ))?;
268
269 Ok(UnverifiedLegacyDelegatedSignature::new(
270 UnverifiedRecoverableEcdsaSignature::new(signature.as_bytes().to_vec()),
271 legacy_signed_public_key_proto,
272 ))
273}
274
275#[derive(Clone, Debug)]
276pub struct ValidatedLegacySignedPublicKey {
277 pub(crate) account_address: String,
278 pub(crate) serialized_key_data: Vec<u8>,
279 pub(crate) wallet_signature: VerifiedSignature,
280 pub(crate) public_key_bytes: Vec<u8>,
281 pub(crate) created_ns: u64,
282}
283
284impl ValidatedLegacySignedPublicKey {
285 fn header_text() -> String {
286 let label = "Create Identity".to_string();
287 format!("XMTP : {}", label)
288 }
289
290 fn body_text(serialized_legacy_key: &[u8]) -> String {
291 hex::encode(serialized_legacy_key)
292 }
293
294 fn footer_text() -> String {
295 "For more info: https://xmtp.org/signatures/".to_string()
296 }
297
298 pub fn text(serialized_legacy_key: &[u8]) -> String {
299 format!(
300 "{}\n{}\n\n{}",
301 Self::header_text(),
302 Self::body_text(serialized_legacy_key),
303 Self::footer_text()
304 )
305 .to_string()
306 }
307
308 pub fn account_address(&self) -> String {
309 self.account_address.clone()
310 }
311
312 pub fn key_bytes(&self) -> Vec<u8> {
313 self.public_key_bytes.clone()
314 }
315
316 pub fn created_ns(&self) -> u64 {
317 self.created_ns
318 }
319}
320
321pub fn to_lower_s(sig_bytes: &[u8]) -> Result<Vec<u8>, SignatureError> {
323 let (sig_data, recovery_id) = match sig_bytes.len() {
325 64 => (sig_bytes, None), 65 => (&sig_bytes[..64], Some(sig_bytes[64])), _ => return Err(SignatureError::Invalid),
328 };
329
330 let sig = K256Signature::try_from(sig_data)?;
332
333 let normalized = match sig.normalize_s() {
335 None => sig_data.to_vec(),
336 Some(normalized) => normalized.to_bytes().to_vec(),
337 };
338
339 if let Some(rid) = recovery_id {
341 let mut result = normalized;
342 result.push(rid);
343 Ok(result)
344 } else {
345 Ok(normalized)
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::SignatureError;
352 use super::to_lower_s;
353 use crate::scw_verifier::VerifierError;
354 use alloy::signers::k256::ecdsa::Signature as K256Signature;
355 use alloy::signers::k256::elliptic_curve::scalar::IsHigh;
356 use alloy::signers::{SignerSync, local::LocalSigner};
357 use wasm_bindgen_test::wasm_bindgen_test;
358 use xmtp_common::RetryableError;
359
360 #[xmtp_common::test]
361 fn test_signature_error_verifier_retryable_propagates() {
362 let err = SignatureError::VerifierError(VerifierError::NoVerifier("eip155:1".to_string()));
364 assert!(
365 err.is_retryable(),
366 "SignatureError wrapping a retryable VerifierError must be retryable"
367 );
368 }
369
370 #[xmtp_common::test]
371 fn test_signature_error_verifier_non_retryable_propagates() {
372 let err = SignatureError::VerifierError(VerifierError::MalformedEipUrl);
374 assert!(
375 !err.is_retryable(),
376 "SignatureError wrapping a non-retryable VerifierError must not be retryable"
377 );
378 }
379
380 #[xmtp_common::test]
381 fn test_signature_error_non_verifier_variants_not_retryable() {
382 assert!(!SignatureError::Invalid.is_retryable());
384 assert!(!SignatureError::InvalidPublicKey.is_retryable());
385 assert!(!SignatureError::InvalidClientData.is_retryable());
386 assert!(!SignatureError::MalformedLegacyKey("missing field".to_string()).is_retryable(),);
387 }
388
389 #[xmtp_common::test]
390 fn test_to_lower_s() {
391 let signer = LocalSigner::random();
393
394 let message = "test message";
396 let signature = signer.sign_message_sync(message.as_bytes()).unwrap();
397 let sig_bytes: Vec<u8> = signature.into();
398
399 let normalized = to_lower_s(&sig_bytes).unwrap();
401 assert_eq!(
402 normalized, sig_bytes,
403 "Already normalized signature should not change"
404 );
405
406 let mut high_s_sig = sig_bytes.clone();
408 for byte in high_s_sig[32..64].iter_mut() {
410 *byte = !*byte;
411 }
412
413 let normalized_high_s = to_lower_s(&high_s_sig).unwrap();
415 assert_ne!(
416 normalized_high_s, high_s_sig,
417 "High-s signature should be normalized"
418 );
419
420 let recovered_sig = K256Signature::try_from(&normalized_high_s.as_slice()[..64]).unwrap();
422 let is_high: bool = recovered_sig.s().is_high().into();
423 assert!(!is_high, "Normalized signature should have low-s value");
424 }
425
426 #[wasm_bindgen_test(unsupported = test)]
427 fn test_invalid_signature() {
428 let invalid_sig = vec![0u8; 65];
430 let result = to_lower_s(&invalid_sig);
431 assert!(result.is_err(), "Should fail with invalid signature");
432
433 let wrong_length = vec![0u8; 63];
435 let result = to_lower_s(&wrong_length);
436 assert!(result.is_err(), "Should fail with wrong length");
437 }
438}