xmtp_id/associations/
signature.rs

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
56/// Xmtp Installation Credential for Specialized for XMTP Identity
57pub 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    // We might want to have some sort of LegacyErc191 Signature Kind for the `CreateIdentity` signatures only
119    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// CAIP-10[https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md]
147#[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
189/// Decode the `legacy_signed_private_key` to legacy private / public key pairs & sign the `signature_text` with the private key.
190pub 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
264/// Converts a signature to use the lower-s value to prevent signature malleability
265pub fn to_lower_s(sig_bytes: &[u8]) -> Result<Vec<u8>, SignatureError> {
266    // Check if we have a recovery id byte
267    let (sig_data, recovery_id) = match sig_bytes.len() {
268        64 => (sig_bytes, None),                       // No recovery id
269        65 => (&sig_bytes[..64], Some(sig_bytes[64])), // Recovery id present
270        _ => return Err(SignatureError::Invalid),
271    };
272
273    // Parse the signature bytes into a K256Signature
274    let sig = K256Signature::try_from(sig_data)?;
275
276    // If s is already normalized (lower-s), return the original bytes
277    let normalized = match sig.normalize_s() {
278        None => sig_data.to_vec(),
279        Some(normalized) => normalized.to_bytes().to_vec(),
280    };
281
282    // Add back recovery id if it was present
283    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        // Create a test wallet
303        let signer = LocalSigner::random();
304
305        // Sign a test message
306        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        // Test normalizing an already normalized signature
311        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        // Create a signature with high-s value by manipulating the s component
318        let mut high_s_sig = sig_bytes.clone();
319        // Flip bits in the s component (last 32 bytes) to create a high-s value
320        for byte in high_s_sig[32..64].iter_mut() {
321            *byte = !*byte;
322        }
323
324        // Normalize the manipulated signature
325        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        // Verify the normalized signature is valid
332        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        // Test with invalid signature bytes
340        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        // Test with wrong length
345        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}