openzeppelin_relayer/services/signer/stellar/
mod.rs

1// openzeppelin-relayer/src/services/signer/stellar/mod.rs
2//! Stellar signer implementation (local keystore, Google Cloud KMS, AWS KMS, and Turnkey)
3
4mod aws_kms_signer;
5mod google_cloud_kms_signer;
6mod local_signer;
7mod turnkey_signer;
8mod vault_signer;
9
10use async_trait::async_trait;
11use aws_kms_signer::*;
12use google_cloud_kms_signer::*;
13use local_signer::*;
14use turnkey_signer::*;
15use vault_signer::*;
16
17use soroban_rs::xdr::SignatureHint;
18
19use crate::{
20    domain::{SignDataRequest, SignDataResponse, SignTransactionResponse, SignTypedDataRequest},
21    models::{
22        Address, NetworkTransactionData, Signer as SignerDomainModel, SignerConfig,
23        SignerRepoModel, SignerType, TransactionRepoModel, VaultSignerConfig,
24    },
25    services::{
26        signer::{SignXdrTransactionResponseStellar, Signer, SignerError, SignerFactoryError},
27        AwsKmsService, GoogleCloudKmsService, TurnkeyService, VaultConfig, VaultService,
28    },
29};
30
31use super::DataSignerTrait;
32
33/// Derive a `SignatureHint` (last 4 bytes of the Ed25519 public key) from a Stellar address.
34fn derive_signature_hint(address: &Address) -> Result<SignatureHint, SignerError> {
35    match address {
36        Address::Stellar(addr) => {
37            let pk = stellar_strkey::ed25519::PublicKey::from_string(addr).map_err(|e| {
38                SignerError::SigningError(format!("Failed to parse Stellar address '{addr}': {e}"))
39            })?;
40            // pk.0 is [u8; 32], last 4 bytes are the hint
41            let hint_bytes: [u8; 4] = pk.0[28..].try_into().map_err(|_| {
42                SignerError::SigningError(
43                    "Failed to create signature hint from public key".to_string(),
44                )
45            })?;
46            Ok(SignatureHint(hint_bytes))
47        }
48        _ => Err(SignerError::SigningError(format!(
49            "Expected Stellar address, got: {address:?}"
50        ))),
51    }
52}
53
54#[cfg(test)]
55use mockall::automock;
56
57#[cfg_attr(test, automock)]
58/// Trait defining Stellar-specific signing operations
59///
60/// This trait extends the basic signing functionality with methods specific
61/// to the Stellar blockchain, following the same pattern as SolanaSignTrait.
62#[async_trait]
63pub trait StellarSignTrait: Sync + Send {
64    /// Signs a Stellar transaction in XDR format
65    ///
66    /// # Arguments
67    ///
68    /// * `unsigned_xdr` - The unsigned transaction in XDR format
69    /// * `network_passphrase` - The network passphrase for the Stellar network
70    ///
71    /// # Returns
72    ///
73    /// A signed transaction response containing the signed XDR and signature
74    async fn sign_xdr_transaction(
75        &self,
76        unsigned_xdr: &str,
77        network_passphrase: &str,
78    ) -> Result<SignXdrTransactionResponseStellar, SignerError>;
79}
80
81pub enum StellarSigner {
82    Local(Box<LocalSigner>),
83    Vault(VaultSigner<VaultService>),
84    GoogleCloudKms(Box<GoogleCloudKmsSigner>),
85    AwsKms(AwsKmsSigner),
86    Turnkey(TurnkeySigner),
87}
88
89#[async_trait]
90impl Signer for StellarSigner {
91    async fn address(&self) -> Result<Address, SignerError> {
92        match self {
93            Self::Local(s) => s.address().await,
94            Self::Vault(s) => s.address().await,
95            Self::GoogleCloudKms(s) => s.address().await,
96            Self::AwsKms(s) => s.address().await,
97            Self::Turnkey(s) => s.address().await,
98        }
99    }
100
101    async fn sign_transaction(
102        &self,
103        tx: NetworkTransactionData,
104    ) -> Result<SignTransactionResponse, SignerError> {
105        match self {
106            Self::Local(s) => s.sign_transaction(tx).await,
107            Self::Vault(s) => s.sign_transaction(tx).await,
108            Self::GoogleCloudKms(s) => s.sign_transaction(tx).await,
109            Self::AwsKms(s) => s.sign_transaction(tx).await,
110            Self::Turnkey(s) => s.sign_transaction(tx).await,
111        }
112    }
113}
114
115#[async_trait]
116impl StellarSignTrait for StellarSigner {
117    async fn sign_xdr_transaction(
118        &self,
119        unsigned_xdr: &str,
120        network_passphrase: &str,
121    ) -> Result<SignXdrTransactionResponseStellar, SignerError> {
122        match self {
123            Self::Local(s) => {
124                s.sign_xdr_transaction(unsigned_xdr, network_passphrase)
125                    .await
126            }
127            Self::Vault(s) => {
128                s.sign_xdr_transaction(unsigned_xdr, network_passphrase)
129                    .await
130            }
131            Self::GoogleCloudKms(s) => {
132                s.sign_xdr_transaction(unsigned_xdr, network_passphrase)
133                    .await
134            }
135            Self::AwsKms(s) => {
136                s.sign_xdr_transaction(unsigned_xdr, network_passphrase)
137                    .await
138            }
139            Self::Turnkey(s) => {
140                s.sign_xdr_transaction(unsigned_xdr, network_passphrase)
141                    .await
142            }
143        }
144    }
145}
146
147pub struct StellarSignerFactory;
148
149impl StellarSignerFactory {
150    pub fn create_stellar_signer(
151        m: &SignerDomainModel,
152    ) -> Result<StellarSigner, SignerFactoryError> {
153        let signer = match &m.config {
154            SignerConfig::Local(_) => {
155                let local_signer = LocalSigner::new(m)?;
156                StellarSigner::Local(Box::new(local_signer))
157            }
158            SignerConfig::Vault(config) => {
159                let vault_config = VaultConfig::new(
160                    config.address.clone(),
161                    config.role_id.clone(),
162                    config.secret_id.clone(),
163                    config.namespace.clone(),
164                    config
165                        .mount_point
166                        .clone()
167                        .unwrap_or_else(|| "secret".to_string()),
168                    None,
169                );
170                let vault_service = VaultService::new(vault_config);
171
172                StellarSigner::Vault(VaultSigner::new(
173                    m.id.clone(),
174                    config.clone(),
175                    vault_service,
176                ))
177            }
178            SignerConfig::GoogleCloudKms(config) => {
179                let service = GoogleCloudKmsService::new(config)
180                    .map_err(|e| SignerFactoryError::CreationFailed(e.to_string()))?;
181                StellarSigner::GoogleCloudKms(Box::new(GoogleCloudKmsSigner::new(service)))
182            }
183            SignerConfig::Turnkey(config) => {
184                let service = TurnkeyService::new(config.clone())
185                    .map_err(|e| SignerFactoryError::CreationFailed(e.to_string()))?;
186                StellarSigner::Turnkey(TurnkeySigner::new(service))
187            }
188            SignerConfig::AwsKms(config) => {
189                let aws_kms_service = futures::executor::block_on(AwsKmsService::new(
190                    config.clone(),
191                ))
192                .map_err(|e| {
193                    SignerFactoryError::InvalidConfig(format!(
194                        "Failed to create AWS KMS service: {e}"
195                    ))
196                })?;
197                StellarSigner::AwsKms(AwsKmsSigner::new(aws_kms_service))
198            }
199            SignerConfig::VaultTransit(_) => {
200                return Err(SignerFactoryError::UnsupportedType("Vault Transit".into()))
201            }
202            SignerConfig::Cdp(_) => return Err(SignerFactoryError::UnsupportedType("CDP".into())),
203        };
204        Ok(signer)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_derive_signature_hint_valid_stellar_address() {
214        let pk = stellar_strkey::ed25519::PublicKey([0u8; 32]);
215        let address = Address::Stellar(pk.to_string());
216
217        let hint = derive_signature_hint(&address).unwrap();
218        // Last 4 bytes of all-zero key
219        assert_eq!(hint.0, [0u8; 4]);
220    }
221
222    #[test]
223    fn test_derive_signature_hint_extracts_last_four_bytes() {
224        let mut key_bytes = [0u8; 32];
225        key_bytes[28] = 0xAA;
226        key_bytes[29] = 0xBB;
227        key_bytes[30] = 0xCC;
228        key_bytes[31] = 0xDD;
229        let pk = stellar_strkey::ed25519::PublicKey(key_bytes);
230        let address = Address::Stellar(pk.to_string());
231
232        let hint = derive_signature_hint(&address).unwrap();
233        assert_eq!(hint.0, [0xAA, 0xBB, 0xCC, 0xDD]);
234    }
235
236    #[test]
237    fn test_derive_signature_hint_invalid_stellar_address() {
238        let address = Address::Stellar("INVALID_ADDRESS".to_string());
239        let result = derive_signature_hint(&address);
240        assert!(result.is_err());
241        match result.unwrap_err() {
242            SignerError::SigningError(msg) => {
243                assert!(msg.contains("Failed to parse Stellar address"));
244            }
245            e => panic!("Expected SigningError, got: {e:?}"),
246        }
247    }
248
249    #[test]
250    fn test_derive_signature_hint_non_stellar_address() {
251        let address = Address::Evm([0u8; 20]);
252        let result = derive_signature_hint(&address);
253        assert!(result.is_err());
254        match result.unwrap_err() {
255            SignerError::SigningError(msg) => {
256                assert!(msg.contains("Expected Stellar address"));
257            }
258            e => panic!("Expected SigningError, got: {e:?}"),
259        }
260    }
261
262    #[test]
263    fn test_derive_signature_hint_solana_address_rejected() {
264        let address = Address::Solana("SomeBase58Address".to_string());
265        let result = derive_signature_hint(&address);
266        assert!(result.is_err());
267    }
268}