openzeppelin_relayer/services/signer/stellar/
aws_kms_signer.rs1use 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_hint: OnceCell<SignatureHint>,
35}
36
37impl AwsKmsSigner<DefaultAwsKmsService> {
38 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 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 let signature = match &stellar_data.transaction_input {
81 crate::models::TransactionInput::Operations(_) => {
82 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 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 async fn sign_envelope(
118 &self,
119 envelope: &TransactionEnvelope,
120 network_id: &Hash,
121 ) -> Result<DecoratedSignature, SignerError> {
122 let payload = create_signature_payload(envelope, network_id)
124 .map_err(|e| SignerError::SigningError(format!("Failed to create payload: {e}")))?;
125
126 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 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 self.create_decorated_signature(signature_bytes).await
144 }
145
146 async fn sign_transaction_directly(
148 &self,
149 transaction: &Transaction,
150 network_id: &Hash,
151 ) -> Result<DecoratedSignature, SignerError> {
152 let payload = create_transaction_signature_payload(transaction, network_id);
154
155 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 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 self.create_decorated_signature(signature_bytes).await
173 }
174
175 async fn create_decorated_signature(
177 &self,
178 signature_bytes: Vec<u8>,
179 ) -> Result<DecoratedSignature, SignerError> {
180 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 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 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 let mut envelope = parse_transaction_xdr(unsigned_xdr, false)
238 .map_err(|e| SignerError::SigningError(format!("Invalid XDR: {e}")))?;
239
240 let hash_bytes: [u8; 32] = Sha256::digest(network_passphrase.as_bytes()).into();
242 let network_id = Hash(hash_bytes);
243
244 let signature = self.sign_envelope(&envelope, &network_id).await?;
246
247 attach_signatures_to_envelope(&mut envelope, vec![signature.clone()])
249 .map_err(|e| SignerError::SigningError(format!("Failed to attach signature: {e}")))?;
250
251 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 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 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(), });
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 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_service.expect_sign_stellar().times(1).returning(|_| {
341 let sig = vec![1u8; 64]; 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 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 let signed_envelope =
359 TransactionEnvelope::from_xdr_base64(&result.signed_xdr, Limits::none())
360 .expect("Should be able to parse signed XDR");
361
362 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]) })
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]) })
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 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 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}