openzeppelin_relayer/services/signer/stellar/
aws_kms_signer.rs

1//! # Stellar AWS KMS Signer Implementation
2//!
3//! This module provides a Stellar signer implementation that uses the AWS KMS API
4//! for secure key management and cryptographic operations.
5
6use super::StellarSignTrait;
7use crate::{
8    domain::{
9        attach_signatures_to_envelope, parse_transaction_xdr,
10        stellar::{create_signature_payload, create_transaction_signature_payload},
11        SignTransactionResponse, SignXdrTransactionResponseStellar,
12    },
13    models::{Address, NetworkTransactionData, SignerError},
14    services::{signer::Signer, AwsKmsService, AwsKmsStellarService},
15};
16
17use async_trait::async_trait;
18use sha2::{Digest, Sha256};
19use soroban_rs::xdr::{
20    DecoratedSignature, Hash, Limits, ReadXdr, Signature, SignatureHint, Transaction,
21    TransactionEnvelope, WriteXdr,
22};
23use tokio::sync::OnceCell;
24use tracing::debug;
25
26pub type DefaultAwsKmsService = AwsKmsService;
27
28pub struct AwsKmsSigner<T = DefaultAwsKmsService>
29where
30    T: AwsKmsStellarService,
31{
32    aws_kms_service: T,
33    /// Cached signature hint (last 4 bytes of the public key), computed once on first use.
34    cached_hint: OnceCell<SignatureHint>,
35}
36
37impl AwsKmsSigner<DefaultAwsKmsService> {
38    /// Creates a new AwsKmsSigner with the default AwsKmsService
39    pub fn new(aws_kms_service: DefaultAwsKmsService) -> Self {
40        Self {
41            aws_kms_service,
42            cached_hint: OnceCell::new(),
43        }
44    }
45}
46
47#[cfg(test)]
48impl<T: AwsKmsStellarService> AwsKmsSigner<T> {
49    /// Creates a new AwsKmsSigner with a custom service implementation for testing
50    pub fn new_for_testing(aws_kms_service: T) -> Self {
51        Self {
52            aws_kms_service,
53            cached_hint: OnceCell::new(),
54        }
55    }
56}
57
58#[async_trait]
59impl<T: AwsKmsStellarService> Signer for AwsKmsSigner<T> {
60    async fn address(&self) -> Result<Address, SignerError> {
61        self.aws_kms_service
62            .get_stellar_address()
63            .await
64            .map_err(|e| SignerError::KeyError(e.to_string()))
65    }
66
67    async fn sign_transaction(
68        &self,
69        tx: NetworkTransactionData,
70    ) -> Result<SignTransactionResponse, SignerError> {
71        let stellar_data = tx
72            .get_stellar_transaction_data()
73            .map_err(|e| SignerError::SigningError(format!("Failed to get tx data: {e}")))?;
74
75        let passphrase = &stellar_data.network_passphrase;
76        let hash_bytes: [u8; 32] = Sha256::digest(passphrase.as_bytes()).into();
77        let network_id = Hash(hash_bytes);
78
79        // Sign based on transaction input type
80        let signature = match &stellar_data.transaction_input {
81            crate::models::TransactionInput::Operations(_) => {
82                // Build transaction from operations and sign
83                let transaction = Transaction::try_from(stellar_data).map_err(|e| {
84                    SignerError::SigningError(format!(
85                        "Failed to build Stellar transaction from operations: {e}"
86                    ))
87                })?;
88
89                self.sign_transaction_directly(&transaction, &network_id)
90                    .await?
91            }
92            crate::models::TransactionInput::UnsignedXdr(xdr)
93            | crate::models::TransactionInput::SignedXdr { xdr, .. }
94            | crate::models::TransactionInput::SorobanGasAbstraction { xdr, .. } => {
95                // Parse the XDR envelope and sign
96                let envelope =
97                    TransactionEnvelope::from_xdr_base64(xdr, Limits::none()).map_err(|e| {
98                        SignerError::SigningError(format!(
99                            "Failed to parse Stellar transaction XDR '{}...': {}",
100                            &xdr[..std::cmp::min(50, xdr.len())],
101                            e
102                        ))
103                    })?;
104
105                self.sign_envelope(&envelope, &network_id).await?
106            }
107        };
108
109        Ok(SignTransactionResponse::Stellar(
110            crate::domain::SignTransactionResponseStellar { signature },
111        ))
112    }
113}
114
115impl<T: AwsKmsStellarService> AwsKmsSigner<T> {
116    /// Sign a transaction envelope
117    async fn sign_envelope(
118        &self,
119        envelope: &TransactionEnvelope,
120        network_id: &Hash,
121    ) -> Result<DecoratedSignature, SignerError> {
122        // Create the appropriate signature payload based on envelope type
123        let payload = create_signature_payload(envelope, network_id)
124            .map_err(|e| SignerError::SigningError(format!("Failed to create payload: {e}")))?;
125
126        // Serialize and hash the payload
127        let payload_bytes = payload
128            .to_xdr(Limits::none())
129            .map_err(|e| SignerError::SigningError(format!("Failed to serialize payload: {e}")))?;
130
131        let hash = Sha256::digest(&payload_bytes);
132
133        // Sign the hash using AWS KMS
134        let signature_bytes = self
135            .aws_kms_service
136            .sign_stellar(&hash)
137            .await
138            .map_err(|e| {
139                SignerError::SigningError(format!("AWS KMS signing operation failed: {e}"))
140            })?;
141
142        // Create decorated signature with improved error handling
143        self.create_decorated_signature(signature_bytes).await
144    }
145
146    /// Sign a transaction directly from Transaction struct
147    async fn sign_transaction_directly(
148        &self,
149        transaction: &Transaction,
150        network_id: &Hash,
151    ) -> Result<DecoratedSignature, SignerError> {
152        // Create signature payload for the transaction
153        let payload = create_transaction_signature_payload(transaction, network_id);
154
155        // Serialize and hash the payload
156        let payload_bytes = payload
157            .to_xdr(Limits::none())
158            .map_err(|e| SignerError::SigningError(format!("Failed to serialize payload: {e}")))?;
159
160        let hash = Sha256::digest(&payload_bytes);
161
162        // Sign the hash using AWS KMS
163        let signature_bytes = self
164            .aws_kms_service
165            .sign_stellar(&hash)
166            .await
167            .map_err(|e| {
168                SignerError::SigningError(format!("AWS KMS signing operation failed: {e}"))
169            })?;
170
171        // Create decorated signature with improved error handling
172        self.create_decorated_signature(signature_bytes).await
173    }
174
175    /// Helper function to create a DecoratedSignature from signature bytes
176    async fn create_decorated_signature(
177        &self,
178        signature_bytes: Vec<u8>,
179    ) -> Result<DecoratedSignature, SignerError> {
180        // Validate signature length for Ed25519
181        if signature_bytes.len() != 64 {
182            return Err(SignerError::SigningError(format!(
183                "AWS KMS returned invalid Ed25519 signature length: expected 64 bytes, got {}",
184                signature_bytes.len()
185            )));
186        }
187
188        let hint = self.get_signature_hint().await?;
189
190        // Convert signature bytes to BytesM<64>
191        let signature_bytes_m =
192            soroban_rs::xdr::BytesM::try_from(signature_bytes).map_err(|_| {
193                SignerError::SigningError(
194                    "Failed to convert signature to BytesM format".to_string(),
195                )
196            })?;
197
198        Ok(DecoratedSignature {
199            hint,
200            signature: Signature(signature_bytes_m),
201        })
202    }
203
204    /// Get the signature hint for this signer (last 4 bytes of the public key).
205    ///
206    /// The hint is computed once and cached for the lifetime of the signer,
207    /// since it is deterministic for a given KMS key.
208    async fn get_signature_hint(&self) -> Result<SignatureHint, SignerError> {
209        self.cached_hint
210            .get_or_try_init(|| async {
211                let address = self
212                    .aws_kms_service
213                    .get_stellar_address()
214                    .await
215                    .map_err(|e| {
216                        SignerError::SigningError(format!(
217                            "Failed to retrieve Stellar address from AWS KMS: {e}"
218                        ))
219                    })?;
220                super::derive_signature_hint(&address)
221            })
222            .await
223            .cloned()
224    }
225}
226
227#[async_trait]
228impl<T: AwsKmsStellarService> StellarSignTrait for AwsKmsSigner<T> {
229    async fn sign_xdr_transaction(
230        &self,
231        unsigned_xdr: &str,
232        network_passphrase: &str,
233    ) -> Result<SignXdrTransactionResponseStellar, SignerError> {
234        debug!("Signing Stellar XDR transaction with AWS KMS");
235
236        // Parse the unsigned XDR
237        let mut envelope = parse_transaction_xdr(unsigned_xdr, false)
238            .map_err(|e| SignerError::SigningError(format!("Invalid XDR: {e}")))?;
239
240        // Create network ID from passphrase
241        let hash_bytes: [u8; 32] = Sha256::digest(network_passphrase.as_bytes()).into();
242        let network_id = Hash(hash_bytes);
243
244        // Sign the envelope
245        let signature = self.sign_envelope(&envelope, &network_id).await?;
246
247        // Attach the signature to the envelope
248        attach_signatures_to_envelope(&mut envelope, vec![signature.clone()])
249            .map_err(|e| SignerError::SigningError(format!("Failed to attach signature: {e}")))?;
250
251        // Serialize the signed envelope
252        let signed_xdr = envelope.to_xdr_base64(Limits::none()).map_err(|e| {
253            SignerError::SigningError(format!("Failed to serialize signed XDR: {e}"))
254        })?;
255
256        Ok(SignXdrTransactionResponseStellar {
257            signed_xdr,
258            signature,
259        })
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::{
267        models::{StellarTransactionData, TransactionInput},
268        services::{AwsKmsError, MockAwsKmsStellarService},
269    };
270    use soroban_rs::xdr::{SequenceNumber, TransactionV0, TransactionV0Envelope, Uint256};
271    use stellar_strkey::ed25519::PublicKey;
272
273    #[tokio::test]
274    async fn test_address() {
275        use stellar_strkey::ed25519::PublicKey as StrKeyPublicKey;
276        let test_pk = StrKeyPublicKey([0u8; 32]);
277        let test_address = test_pk.to_string();
278
279        let mut mock_service = MockAwsKmsStellarService::new();
280        let test_address_for_mock = test_address.clone();
281        mock_service
282            .expect_get_stellar_address()
283            .times(1)
284            .returning(move || {
285                let addr = test_address_for_mock.clone();
286                Box::pin(async { Ok(Address::Stellar(addr)) })
287            });
288
289        let signer = AwsKmsSigner::new_for_testing(mock_service);
290        let result = signer.address().await.unwrap();
291
292        match result {
293            Address::Stellar(addr) => {
294                assert_eq!(addr, test_address);
295            }
296            _ => panic!("Expected Stellar address"),
297        }
298    }
299
300    #[tokio::test]
301    async fn test_sign_xdr_transaction_success() {
302        // Create test Stellar address - use all zeros public key
303        use stellar_strkey::ed25519::PublicKey as StrKeyPublicKey;
304        let test_pk = StrKeyPublicKey([0u8; 32]);
305        let test_address = test_pk.to_string();
306        let source_pk = PublicKey::from_string(&test_address).unwrap();
307
308        // Create a simple unsigned transaction envelope
309        let tx = TransactionV0 {
310            source_account_ed25519: Uint256(source_pk.0),
311            fee: 100,
312            seq_num: SequenceNumber(1),
313            time_bounds: None,
314            memo: soroban_rs::xdr::Memo::None,
315            operations: vec![].try_into().unwrap(),
316            ext: soroban_rs::xdr::TransactionV0Ext::V0,
317        };
318
319        let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
320            tx,
321            signatures: vec![].try_into().unwrap(), // No signatures - unsigned
322        });
323
324        let unsigned_xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
325        let network_passphrase = "Test SDF Network ; September 2015";
326
327        let mut mock_service = MockAwsKmsStellarService::new();
328
329        // Mock getting the address (called once for hint)
330        let test_address_for_mock = test_address.clone();
331        mock_service
332            .expect_get_stellar_address()
333            .times(1)
334            .returning(move || {
335                let addr = test_address_for_mock.clone();
336                Box::pin(async move { Ok(Address::Stellar(addr)) })
337            });
338
339        // Mock signing
340        mock_service.expect_sign_stellar().times(1).returning(|_| {
341            let sig = vec![1u8; 64]; // Valid Ed25519 signature length
342            Box::pin(async move { Ok(sig) })
343        });
344
345        let signer = AwsKmsSigner::new_for_testing(mock_service);
346
347        let result = signer
348            .sign_xdr_transaction(&unsigned_xdr, network_passphrase)
349            .await
350            .unwrap();
351
352        // Verify the response
353        assert!(!result.signed_xdr.is_empty());
354        assert_eq!(result.signature.hint.0.len(), 4);
355        assert_eq!(result.signature.signature.0.len(), 64);
356
357        // Verify the signed XDR can be parsed back
358        let signed_envelope =
359            TransactionEnvelope::from_xdr_base64(&result.signed_xdr, Limits::none())
360                .expect("Should be able to parse signed XDR");
361
362        // Verify it now has a signature
363        match signed_envelope {
364            TransactionEnvelope::TxV0(v0_env) => {
365                assert_eq!(
366                    v0_env.signatures.len(),
367                    1,
368                    "Should have exactly one signature"
369                );
370            }
371            _ => panic!("Expected V0 envelope"),
372        }
373    }
374
375    #[tokio::test]
376    async fn test_sign_xdr_transaction_invalid_xdr() {
377        let mock_service = MockAwsKmsStellarService::new();
378
379        let signer = AwsKmsSigner::new_for_testing(mock_service);
380        let invalid_xdr = "INVALID_BASE64_XDR_DATA";
381        let network_passphrase = "Test SDF Network ; September 2015";
382
383        let result = signer
384            .sign_xdr_transaction(invalid_xdr, network_passphrase)
385            .await;
386
387        assert!(result.is_err());
388        match result.err().unwrap() {
389            SignerError::SigningError(msg) => {
390                assert!(msg.contains("Invalid XDR"));
391            }
392            _ => panic!("Expected SigningError"),
393        }
394    }
395
396    #[tokio::test]
397    async fn test_sign_transaction_with_operations_input_success() {
398        use stellar_strkey::ed25519::PublicKey as StrKeyPublicKey;
399        let test_pk = StrKeyPublicKey([0u8; 32]);
400        let test_address = test_pk.to_string();
401
402        let mut mock_service = MockAwsKmsStellarService::new();
403        mock_service.expect_sign_stellar().times(1).returning(|_| {
404            Box::pin(async {
405                Ok(vec![0u8; 64]) // 64-byte Ed25519 signature
406            })
407        });
408        let test_address_for_mock = test_address.clone();
409        mock_service
410            .expect_get_stellar_address()
411            .times(1)
412            .returning(move || {
413                let addr = test_address_for_mock.clone();
414                Box::pin(async { Ok(Address::Stellar(addr)) })
415            });
416
417        let signer = AwsKmsSigner::new_for_testing(mock_service);
418
419        let tx_data = StellarTransactionData {
420            source_account: test_address,
421            fee: Some(100),
422            sequence_number: Some(1),
423            transaction_input: TransactionInput::Operations(vec![]),
424            memo: None,
425            valid_until: None,
426            network_passphrase: "Test SDF Network ; September 2015".to_string(),
427            signatures: Vec::new(),
428            hash: None,
429            simulation_transaction_data: None,
430            signed_envelope_xdr: None,
431            transaction_result_xdr: None,
432        };
433
434        let result = signer
435            .sign_transaction(NetworkTransactionData::Stellar(tx_data))
436            .await;
437
438        assert!(result.is_ok());
439    }
440
441    #[tokio::test]
442    async fn test_address_error_handling() {
443        let mut mock_service = MockAwsKmsStellarService::new();
444        mock_service
445            .expect_get_stellar_address()
446            .times(1)
447            .returning(|| {
448                Box::pin(async { Err(AwsKmsError::GetError("Invalid configuration".to_string())) })
449            });
450
451        let signer = AwsKmsSigner::new_for_testing(mock_service);
452        let result = signer.address().await;
453
454        assert!(result.is_err());
455        match result.err().unwrap() {
456            SignerError::KeyError(msg) => {
457                assert!(msg.contains("Invalid configuration"));
458            }
459            _ => panic!("Expected KeyError"),
460        }
461    }
462
463    #[tokio::test]
464    async fn test_sign_transaction_with_invalid_signature_length() {
465        use stellar_strkey::ed25519::PublicKey as StrKeyPublicKey;
466        let test_pk = StrKeyPublicKey([0u8; 32]);
467        let test_address = test_pk.to_string();
468
469        let mut mock_service = MockAwsKmsStellarService::new();
470        mock_service.expect_sign_stellar().times(1).returning(|_| {
471            Box::pin(async {
472                Ok(vec![0u8; 32]) // Invalid length: 32 bytes instead of 64
473            })
474        });
475
476        let signer = AwsKmsSigner::new_for_testing(mock_service);
477        let tx_data = StellarTransactionData {
478            source_account: test_address,
479            fee: Some(100),
480            sequence_number: Some(1),
481            transaction_input: TransactionInput::Operations(vec![]),
482            memo: None,
483            valid_until: None,
484            network_passphrase: "Test SDF Network ; September 2015".to_string(),
485            signatures: Vec::new(),
486            hash: None,
487            simulation_transaction_data: None,
488            signed_envelope_xdr: None,
489            transaction_result_xdr: None,
490        };
491
492        let result = signer
493            .sign_transaction(NetworkTransactionData::Stellar(tx_data))
494            .await;
495
496        assert!(result.is_err());
497        match result.err().unwrap() {
498            SignerError::SigningError(msg) => {
499                assert!(msg.contains("invalid Ed25519 signature length"));
500                assert!(msg.contains("expected 64 bytes, got 32"));
501            }
502            _ => panic!("Expected SigningError about signature length"),
503        }
504    }
505
506    #[tokio::test]
507    async fn test_sign_transaction_with_kms_service_error() {
508        use stellar_strkey::ed25519::PublicKey as StrKeyPublicKey;
509        let test_pk = StrKeyPublicKey([0u8; 32]);
510        let test_address = test_pk.to_string();
511
512        let mut mock_service = MockAwsKmsStellarService::new();
513        mock_service.expect_sign_stellar().times(1).returning(|_| {
514            Box::pin(async {
515                Err(AwsKmsError::SignError(
516                    "KMS service unavailable".to_string(),
517                ))
518            })
519        });
520
521        let signer = AwsKmsSigner::new_for_testing(mock_service);
522        let tx_data = StellarTransactionData {
523            source_account: test_address,
524            fee: Some(100),
525            sequence_number: Some(1),
526            transaction_input: TransactionInput::Operations(vec![]),
527            memo: None,
528            valid_until: None,
529            network_passphrase: "Test SDF Network ; September 2015".to_string(),
530            signatures: Vec::new(),
531            hash: None,
532            simulation_transaction_data: None,
533            signed_envelope_xdr: None,
534            transaction_result_xdr: None,
535        };
536
537        let result = signer
538            .sign_transaction(NetworkTransactionData::Stellar(tx_data))
539            .await;
540
541        assert!(result.is_err());
542        match result.err().unwrap() {
543            SignerError::SigningError(msg) => {
544                assert!(msg.contains("AWS KMS signing operation failed"));
545                assert!(msg.contains("KMS service unavailable"));
546            }
547            _ => panic!("Expected SigningError about KMS service"),
548        }
549    }
550
551    #[tokio::test]
552    async fn test_sign_xdr_hint_retrieval_failure() {
553        // Tests the get_signature_hint() error path when get_stellar_address fails
554        // inside the OnceCell init closure
555        let mut mock_service = MockAwsKmsStellarService::new();
556        mock_service
557            .expect_get_stellar_address()
558            .times(1)
559            .returning(|| {
560                Box::pin(async { Err(AwsKmsError::GetError("key not found".to_string())) })
561            });
562        // sign_stellar succeeds but hint retrieval will fail
563        mock_service
564            .expect_sign_stellar()
565            .times(1)
566            .returning(|_| Box::pin(async { Ok(vec![1u8; 64]) }));
567
568        let signer = AwsKmsSigner::new_for_testing(mock_service);
569
570        use stellar_strkey::ed25519::PublicKey as StrKeyPublicKey;
571        let test_pk = StrKeyPublicKey([0u8; 32]);
572        let test_address = test_pk.to_string();
573
574        let tx_data = StellarTransactionData {
575            source_account: test_address,
576            fee: Some(100),
577            sequence_number: Some(1),
578            transaction_input: TransactionInput::Operations(vec![]),
579            memo: None,
580            valid_until: None,
581            network_passphrase: "Test SDF Network ; September 2015".to_string(),
582            signatures: Vec::new(),
583            hash: None,
584            simulation_transaction_data: None,
585            signed_envelope_xdr: None,
586            transaction_result_xdr: None,
587        };
588
589        let result = signer
590            .sign_transaction(NetworkTransactionData::Stellar(tx_data))
591            .await;
592
593        assert!(result.is_err());
594        match result.unwrap_err() {
595            SignerError::SigningError(msg) => {
596                assert!(msg.contains("Failed to retrieve Stellar address from AWS KMS"));
597            }
598            e => panic!("Expected SigningError about address retrieval, got: {e:?}"),
599        }
600    }
601}