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_cryptography::{
8 CredentialSign, CredentialVerify, SignerError, SigningContextProvider,
9 XmtpInstallationCredential,
10};
11use xmtp_proto::xmtp::message_contents::{
12 SignedPrivateKey as LegacySignedPrivateKeyProto, signed_private_key,
13};
14
15use super::{
16 unverified::{UnverifiedLegacyDelegatedSignature, UnverifiedRecoverableEcdsaSignature},
17 verified_signature::VerifiedSignature,
18};
19
20use alloy::signers::k256::ecdsa::Signature as K256Signature;
21
22#[derive(Debug, Error)]
23pub enum SignatureError {
24 #[error("Malformed legacy key: {0}")]
25 MalformedLegacyKey(String),
26 #[error(transparent)]
27 CryptoSignatureError(#[from] xmtp_cryptography::signature::SignatureError),
28 #[error(transparent)]
29 VerifierError(#[from] crate::scw_verifier::VerifierError),
30 #[error("ed25519 Signature failed {0}")]
31 Ed25519Error(#[from] ed25519_dalek::SignatureError),
32 #[error(transparent)]
33 TryFromSliceError(#[from] TryFromSliceError),
34 #[error("Signature validation failed")]
35 Invalid,
36 #[error(transparent)]
37 AddressValidationError(#[from] xmtp_cryptography::signature::IdentifierValidationError),
38 #[error(transparent)]
39 UrlParseError(#[from] url::ParseError),
40 #[error(transparent)]
41 DecodeError(#[from] prost::DecodeError),
42 #[error(transparent)]
43 AccountIdError(#[from] AccountIdError),
44 #[error(transparent)]
45 Signer(#[from] SignerError),
46 #[error("Invalid public key")]
47 InvalidPublicKey,
48 #[error("client_data is invalid")]
49 InvalidClientData,
50 #[error(transparent)]
51 SignerError(#[from] alloy::signers::Error),
52 #[error(transparent)]
53 Signature(#[from] alloy::primitives::SignatureError),
54}
55
56pub struct InboxIdInstallationCredential;
58
59pub struct InstallationKeyContext;
60pub struct PublicContext;
61
62impl CredentialSign<InboxIdInstallationCredential> for XmtpInstallationCredential {
63 type Error = SignatureError;
64
65 fn credential_sign<T: SigningContextProvider>(
66 &self,
67 text: impl AsRef<str>,
68 ) -> Result<Vec<u8>, Self::Error> {
69 let mut prehashed: Sha512 = Sha512::new();
70 prehashed.update(text.as_ref());
71 let context = self.with_context(T::context())?;
72 let sig = context
73 .try_sign_digest(prehashed)
74 .map_err(SignatureError::from)?;
75 Ok(sig.to_bytes().into())
76 }
77}
78
79impl CredentialVerify<InboxIdInstallationCredential> for ed25519_dalek::VerifyingKey {
80 type Error = SignatureError;
81
82 fn credential_verify<T: SigningContextProvider>(
83 &self,
84 signature_text: impl AsRef<str>,
85 signature_bytes: &[u8; 64],
86 ) -> Result<(), Self::Error> {
87 let signature = Signature::from_bytes(signature_bytes);
88 let mut prehashed = Sha512::new();
89 prehashed.update(signature_text.as_ref());
90 self.verify_prehashed(prehashed, Some(T::context()), &signature)?;
91 Ok(())
92 }
93}
94
95impl SigningContextProvider for InstallationKeyContext {
96 fn context() -> &'static [u8] {
97 crate::constants::INSTALLATION_KEY_SIGNATURE_CONTEXT
98 }
99}
100
101impl SigningContextProvider for PublicContext {
102 fn context() -> &'static [u8] {
103 crate::constants::PUBLIC_SIGNATURE_CONTEXT
104 }
105}
106
107pub fn verify_signed_with_public_context(
108 signature_text: impl AsRef<str>,
109 signature_bytes: &[u8; 64],
110 public_key: &[u8; 32],
111) -> Result<(), SignatureError> {
112 let verifying_key = VerifyingKey::from_bytes(public_key)?;
113 verifying_key.credential_verify::<PublicContext>(signature_text, signature_bytes)
114}
115
116#[derive(Clone, Debug, PartialEq)]
117pub enum SignatureKind {
118 Erc191,
120 Erc1271,
121 InstallationKey,
122 LegacyDelegated,
123 P256,
124}
125
126impl std::fmt::Display for SignatureKind {
127 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
128 match self {
129 SignatureKind::Erc191 => write!(f, "erc-191"),
130 SignatureKind::Erc1271 => write!(f, "erc-1271"),
131 SignatureKind::InstallationKey => write!(f, "installation-key"),
132 SignatureKind::LegacyDelegated => write!(f, "legacy-delegated"),
133 SignatureKind::P256 => write!(f, "p256"),
134 }
135 }
136}
137
138#[derive(Debug, Error)]
139pub enum AccountIdError {
140 #[error("Chain ID is not a valid u64")]
141 InvalidChainId,
142 #[error("Chain ID is not prefixed with eip155:")]
143 MissingEip155Prefix,
144}
145
146#[derive(Debug, Clone, PartialEq)]
148pub struct AccountId {
149 pub(crate) chain_id: String,
150 pub(crate) account_address: String,
151}
152
153impl AccountId {
154 pub fn new(chain_id: String, account_address: String) -> Self {
155 AccountId {
156 chain_id,
157 account_address,
158 }
159 }
160
161 pub fn new_evm(chain_id: u64, account_address: String) -> Self {
162 Self::new(format!("eip155:{}", chain_id), account_address)
163 }
164
165 pub fn is_evm_chain(&self) -> bool {
166 self.chain_id.starts_with("eip155")
167 }
168
169 pub fn get_account_address(&self) -> &str {
170 &self.account_address
171 }
172
173 pub fn get_chain_id(&self) -> &str {
174 &self.chain_id
175 }
176
177 pub fn get_chain_id_u64(&self) -> Result<u64, AccountIdError> {
178 let stripped = self
179 .chain_id
180 .strip_prefix("eip155:")
181 .ok_or(AccountIdError::MissingEip155Prefix)?;
182
183 stripped
184 .parse::<u64>()
185 .map_err(|_| AccountIdError::InvalidChainId)
186 }
187}
188
189pub fn sign_with_legacy_key(
191 signature_text: String,
192 legacy_signed_private_key: Vec<u8>,
193) -> Result<UnverifiedLegacyDelegatedSignature, SignatureError> {
194 let legacy_signed_private_key_proto =
195 LegacySignedPrivateKeyProto::decode(legacy_signed_private_key.as_slice())?;
196 let signed_private_key::Union::Secp256k1(secp256k1) = legacy_signed_private_key_proto
197 .union
198 .ok_or(SignatureError::MalformedLegacyKey(
199 "Missing secp256k1.union field".to_string(),
200 ))?;
201 let legacy_private_key = secp256k1.bytes;
202 let signer = LocalSigner::from_slice(legacy_private_key.as_slice())?;
203 let signature = signer.sign_message_sync(signature_text.as_bytes())?;
204
205 let legacy_signed_public_key_proto =
206 legacy_signed_private_key_proto
207 .public_key
208 .ok_or(SignatureError::MalformedLegacyKey(
209 "Missing public_key field".to_string(),
210 ))?;
211
212 Ok(UnverifiedLegacyDelegatedSignature::new(
213 UnverifiedRecoverableEcdsaSignature::new(signature.as_bytes().to_vec()),
214 legacy_signed_public_key_proto,
215 ))
216}
217
218#[derive(Clone, Debug)]
219pub struct ValidatedLegacySignedPublicKey {
220 pub(crate) account_address: String,
221 pub(crate) serialized_key_data: Vec<u8>,
222 pub(crate) wallet_signature: VerifiedSignature,
223 pub(crate) public_key_bytes: Vec<u8>,
224 pub(crate) created_ns: u64,
225}
226
227impl ValidatedLegacySignedPublicKey {
228 fn header_text() -> String {
229 let label = "Create Identity".to_string();
230 format!("XMTP : {}", label)
231 }
232
233 fn body_text(serialized_legacy_key: &[u8]) -> String {
234 hex::encode(serialized_legacy_key)
235 }
236
237 fn footer_text() -> String {
238 "For more info: https://xmtp.org/signatures/".to_string()
239 }
240
241 pub fn text(serialized_legacy_key: &[u8]) -> String {
242 format!(
243 "{}\n{}\n\n{}",
244 Self::header_text(),
245 Self::body_text(serialized_legacy_key),
246 Self::footer_text()
247 )
248 .to_string()
249 }
250
251 pub fn account_address(&self) -> String {
252 self.account_address.clone()
253 }
254
255 pub fn key_bytes(&self) -> Vec<u8> {
256 self.public_key_bytes.clone()
257 }
258
259 pub fn created_ns(&self) -> u64 {
260 self.created_ns
261 }
262}
263
264pub fn to_lower_s(sig_bytes: &[u8]) -> Result<Vec<u8>, SignatureError> {
266 let (sig_data, recovery_id) = match sig_bytes.len() {
268 64 => (sig_bytes, None), 65 => (&sig_bytes[..64], Some(sig_bytes[64])), _ => return Err(SignatureError::Invalid),
271 };
272
273 let sig = K256Signature::try_from(sig_data)?;
275
276 let normalized = match sig.normalize_s() {
278 None => sig_data.to_vec(),
279 Some(normalized) => normalized.to_bytes().to_vec(),
280 };
281
282 if let Some(rid) = recovery_id {
284 let mut result = normalized;
285 result.push(rid);
286 Ok(result)
287 } else {
288 Ok(normalized)
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::to_lower_s;
295 use alloy::signers::k256::ecdsa::Signature as K256Signature;
296 use alloy::signers::k256::elliptic_curve::scalar::IsHigh;
297 use alloy::signers::{SignerSync, local::LocalSigner};
298 use wasm_bindgen_test::wasm_bindgen_test;
299
300 #[xmtp_common::test]
301 fn test_to_lower_s() {
302 let signer = LocalSigner::random();
304
305 let message = "test message";
307 let signature = signer.sign_message_sync(message.as_bytes()).unwrap();
308 let sig_bytes: Vec<u8> = signature.into();
309
310 let normalized = to_lower_s(&sig_bytes).unwrap();
312 assert_eq!(
313 normalized, sig_bytes,
314 "Already normalized signature should not change"
315 );
316
317 let mut high_s_sig = sig_bytes.clone();
319 for byte in high_s_sig[32..64].iter_mut() {
321 *byte = !*byte;
322 }
323
324 let normalized_high_s = to_lower_s(&high_s_sig).unwrap();
326 assert_ne!(
327 normalized_high_s, high_s_sig,
328 "High-s signature should be normalized"
329 );
330
331 let recovered_sig = K256Signature::try_from(&normalized_high_s.as_slice()[..64]).unwrap();
333 let is_high: bool = recovered_sig.s().is_high().into();
334 assert!(!is_high, "Normalized signature should have low-s value");
335 }
336
337 #[wasm_bindgen_test(unsupported = test)]
338 fn test_invalid_signature() {
339 let invalid_sig = vec![0u8; 65];
341 let result = to_lower_s(&invalid_sig);
342 assert!(result.is_err(), "Should fail with invalid signature");
343
344 let wrong_length = vec![0u8; 63];
346 let result = to_lower_s(&wrong_length);
347 assert!(result.is_err(), "Should fail with wrong length");
348 }
349}