Skip to main content

xmtp_id/associations/
signature.rs

1use ed25519_dalek::{DigestSigner, Signature, VerifyingKey};
2use sha2::{Digest as _, Sha512};
3use std::array::TryFromSliceError;
4use thiserror::Error;
5use xmtp_common::{ErrorCode, RetryableError};
6use xmtp_cryptography::{
7    CredentialSign, CredentialVerify, SignerError, SigningContextProvider,
8    XmtpInstallationCredential,
9};
10
11#[derive(Debug, Error, ErrorCode)]
12pub enum SignatureError {
13    #[error(transparent)]
14    #[error_code(inherit)]
15    CryptoSignatureError(#[from] xmtp_cryptography::signature::SignatureError),
16    #[error(transparent)]
17    #[error_code(inherit)]
18    VerifierError(#[from] crate::scw_verifier::VerifierError),
19    /// Ed25519 signature failed.
20    ///
21    /// Ed25519 signature verification failed. Not retryable.
22    #[error("ed25519 Signature failed {0}")]
23    Ed25519Error(#[from] ed25519_dalek::SignatureError),
24    /// Slice conversion error.
25    ///
26    /// Byte slice conversion failed. Not retryable.
27    #[error(transparent)]
28    TryFromSliceError(#[from] TryFromSliceError),
29    /// Signature validation failed.
30    ///
31    /// Signature did not verify. Not retryable.
32    #[error("Signature validation failed")]
33    Invalid,
34    #[error(transparent)]
35    #[error_code(inherit)]
36    AddressValidationError(#[from] xmtp_cryptography::signature::IdentifierValidationError),
37    /// URL parse error.
38    ///
39    /// URL parsing failed. Not retryable.
40    #[error(transparent)]
41    UrlParseError(#[from] url::ParseError),
42    /// Decode error.
43    ///
44    /// Protobuf decoding failed. Not retryable.
45    #[error(transparent)]
46    DecodeError(#[from] prost::DecodeError),
47    #[error(transparent)]
48    #[error_code(inherit)]
49    AccountIdError(#[from] AccountIdError),
50    /// Signer error.
51    ///
52    /// Cryptographic signer operation failed. Not retryable.
53    #[error(transparent)]
54    Signer(#[from] SignerError),
55    /// Invalid public key.
56    ///
57    /// Public key is not valid. Not retryable.
58    #[error("Invalid public key")]
59    InvalidPublicKey,
60    /// Invalid client data.
61    ///
62    /// Client data is malformed. Not retryable.
63    #[error("client_data is invalid")]
64    InvalidClientData,
65    /// Alloy signer error.
66    ///
67    /// Ethereum signer failed. Not retryable.
68    #[error(transparent)]
69    SignerError(#[from] alloy::signers::Error),
70    /// Alloy signature error.
71    ///
72    /// Ethereum signature parsing failed. Not retryable.
73    #[error(transparent)]
74    Signature(#[from] alloy::primitives::SignatureError),
75}
76
77impl RetryableError for SignatureError {
78    fn is_retryable(&self) -> bool {
79        match self {
80            // Smart contract wallet verification goes through an RPC provider;
81            // transient provider/IO failures must surface as retryable so the
82            // welcome sync path does not permanently advance the cursor past
83            // welcomes involving SCW users. See xmtp/libxmtp#3394.
84            SignatureError::VerifierError(e) => e.is_retryable(),
85            _ => false,
86        }
87    }
88}
89
90/// Xmtp Installation Credential for Specialized for XMTP Identity
91pub struct InboxIdInstallationCredential;
92
93pub struct InstallationKeyContext;
94pub struct PublicContext;
95
96impl CredentialSign<InboxIdInstallationCredential> for XmtpInstallationCredential {
97    type Error = SignatureError;
98
99    fn credential_sign<T: SigningContextProvider>(
100        &self,
101        text: impl AsRef<str>,
102    ) -> Result<Vec<u8>, Self::Error> {
103        let mut prehashed: Sha512 = Sha512::new();
104        prehashed.update(text.as_ref());
105        let context = self.with_context(T::context())?;
106        let sig = context
107            .try_sign_digest(prehashed)
108            .map_err(SignatureError::from)?;
109        Ok(sig.to_bytes().into())
110    }
111}
112
113impl CredentialVerify<InboxIdInstallationCredential> for ed25519_dalek::VerifyingKey {
114    type Error = SignatureError;
115
116    fn credential_verify<T: SigningContextProvider>(
117        &self,
118        signature_text: impl AsRef<str>,
119        signature_bytes: &[u8; 64],
120    ) -> Result<(), Self::Error> {
121        let signature = Signature::from_bytes(signature_bytes);
122        let mut prehashed = Sha512::new();
123        prehashed.update(signature_text.as_ref());
124        self.verify_prehashed(prehashed, Some(T::context()), &signature)?;
125        Ok(())
126    }
127}
128
129impl SigningContextProvider for InstallationKeyContext {
130    fn context() -> &'static [u8] {
131        crate::constants::INSTALLATION_KEY_SIGNATURE_CONTEXT
132    }
133}
134
135impl SigningContextProvider for PublicContext {
136    fn context() -> &'static [u8] {
137        crate::constants::PUBLIC_SIGNATURE_CONTEXT
138    }
139}
140
141pub fn verify_signed_with_public_context(
142    signature_text: impl AsRef<str>,
143    signature_bytes: &[u8; 64],
144    public_key: &[u8; 32],
145) -> Result<(), SignatureError> {
146    let verifying_key = VerifyingKey::from_bytes(public_key)?;
147    verifying_key.credential_verify::<PublicContext>(signature_text, signature_bytes)
148}
149
150#[derive(Clone, Debug, PartialEq)]
151pub enum SignatureKind {
152    // We might want to have some sort of LegacyErc191 Signature Kind for the `CreateIdentity` signatures only
153    Erc191,
154    Erc1271,
155    InstallationKey,
156    P256,
157}
158
159impl std::fmt::Display for SignatureKind {
160    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
161        match self {
162            SignatureKind::Erc191 => write!(f, "erc-191"),
163            SignatureKind::Erc1271 => write!(f, "erc-1271"),
164            SignatureKind::InstallationKey => write!(f, "installation-key"),
165            SignatureKind::P256 => write!(f, "p256"),
166        }
167    }
168}
169
170#[derive(Debug, Error, ErrorCode)]
171pub enum AccountIdError {
172    /// Invalid chain ID.
173    ///
174    /// Chain ID is not a u64 in canonical decimal form. Not retryable.
175    #[error("Chain ID is not a u64 in canonical decimal form")]
176    InvalidChainId,
177    /// Missing EIP-155 prefix.
178    ///
179    /// Chain ID is not prefixed with `eip155:`. Not retryable.
180    #[error("Chain ID is not prefixed with eip155:")]
181    MissingEip155Prefix,
182    /// Invalid account address.
183    ///
184    /// Account address is not `0x` followed by 40 hexadecimal characters.
185    /// Not retryable.
186    #[error("Account address is not 0x followed by 40 hexadecimal characters")]
187    InvalidAddress,
188}
189
190// CAIP-10[https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md]
191#[derive(Debug, Clone, PartialEq)]
192pub struct AccountId {
193    pub(crate) chain_id: String,
194    pub(crate) account_address: String,
195}
196
197impl AccountId {
198    pub fn new(chain_id: String, account_address: String) -> Self {
199        AccountId {
200            chain_id,
201            account_address,
202        }
203    }
204
205    pub fn new_evm(chain_id: u64, account_address: String) -> Self {
206        Self::new(format!("eip155:{}", chain_id), account_address)
207    }
208
209    pub fn is_evm_chain(&self) -> bool {
210        self.chain_id.starts_with("eip155")
211    }
212
213    pub fn get_account_address(&self) -> &str {
214        &self.account_address
215    }
216
217    pub fn get_chain_id(&self) -> &str {
218        &self.chain_id
219    }
220
221    /// The chain id of an `eip155` account, whose reference must be the
222    /// chain id in canonical decimal form: no sign, no leading zero.
223    pub fn get_chain_id_u64(&self) -> Result<u64, AccountIdError> {
224        let reference = self
225            .chain_id
226            .strip_prefix("eip155:")
227            .ok_or(AccountIdError::MissingEip155Prefix)?;
228        reference
229            .parse::<u64>()
230            .ok()
231            .filter(|chain_id| chain_id.to_string() == reference)
232            .ok_or(AccountIdError::InvalidChainId)
233    }
234
235    /// The chain id of an `eip155` account id in the only verifiable form:
236    /// the chain id as [`Self::get_chain_id_u64`] requires, and an address of
237    /// `0x` followed by 40 hexadecimal characters. Checked before any chain
238    /// access, so a malformed account id is never routed.
239    // implements: IDENT-060
240    pub fn eip155_chain_id(&self) -> Result<u64, AccountIdError> {
241        let chain_id = self.get_chain_id_u64()?;
242        self.account_address
243            .strip_prefix("0x")
244            .is_some_and(|hex| hex.len() == 40 && hex.bytes().all(|b| b.is_ascii_hexdigit()))
245            .then_some(chain_id)
246            .ok_or(AccountIdError::InvalidAddress)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::SignatureError;
253    use crate::scw_verifier::VerifierError;
254    use xmtp_common::RetryableError;
255
256    #[xmtp_common::test]
257    fn test_signature_error_verifier_retryable_propagates() {
258        // Retryable verifier error (NoVerifier) should make SignatureError retryable.
259        let err = SignatureError::VerifierError(VerifierError::NoVerifier("eip155:1".to_string()));
260        assert!(
261            err.is_retryable(),
262            "SignatureError wrapping a retryable VerifierError must be retryable"
263        );
264    }
265
266    #[xmtp_common::test]
267    fn test_signature_error_verifier_non_retryable_propagates() {
268        // Non-retryable verifier error (MalformedEipUrl) should stay non-retryable.
269        let err = SignatureError::VerifierError(VerifierError::MalformedEipUrl);
270        assert!(
271            !err.is_retryable(),
272            "SignatureError wrapping a non-retryable VerifierError must not be retryable"
273        );
274    }
275
276    #[xmtp_common::test]
277    fn test_signature_error_non_verifier_variants_not_retryable() {
278        // Terminal crypto/parse failures are never retryable.
279        assert!(!SignatureError::Invalid.is_retryable());
280        assert!(!SignatureError::InvalidPublicKey.is_retryable());
281        assert!(!SignatureError::InvalidClientData.is_retryable());
282    }
283}