xmtp_id/associations/
signature.rs1use 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 #[error("ed25519 Signature failed {0}")]
23 Ed25519Error(#[from] ed25519_dalek::SignatureError),
24 #[error(transparent)]
28 TryFromSliceError(#[from] TryFromSliceError),
29 #[error("Signature validation failed")]
33 Invalid,
34 #[error(transparent)]
35 #[error_code(inherit)]
36 AddressValidationError(#[from] xmtp_cryptography::signature::IdentifierValidationError),
37 #[error(transparent)]
41 UrlParseError(#[from] url::ParseError),
42 #[error(transparent)]
46 DecodeError(#[from] prost::DecodeError),
47 #[error(transparent)]
48 #[error_code(inherit)]
49 AccountIdError(#[from] AccountIdError),
50 #[error(transparent)]
54 Signer(#[from] SignerError),
55 #[error("Invalid public key")]
59 InvalidPublicKey,
60 #[error("client_data is invalid")]
64 InvalidClientData,
65 #[error(transparent)]
69 SignerError(#[from] alloy::signers::Error),
70 #[error(transparent)]
74 Signature(#[from] alloy::primitives::SignatureError),
75}
76
77impl RetryableError for SignatureError {
78 fn is_retryable(&self) -> bool {
79 match self {
80 SignatureError::VerifierError(e) => e.is_retryable(),
85 _ => false,
86 }
87 }
88}
89
90pub 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 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 #[error("Chain ID is not a u64 in canonical decimal form")]
176 InvalidChainId,
177 #[error("Chain ID is not prefixed with eip155:")]
181 MissingEip155Prefix,
182 #[error("Account address is not 0x followed by 40 hexadecimal characters")]
187 InvalidAddress,
188}
189
190#[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 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 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 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 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 assert!(!SignatureError::Invalid.is_retryable());
280 assert!(!SignatureError::InvalidPublicKey.is_retryable());
281 assert!(!SignatureError::InvalidClientData.is_retryable());
282 }
283}