openzeppelin_relayer/services/aws_kms/
mod.rs

1//! # AWS KMS Service Module
2//!
3//! This module provides integration with AWS KMS for secure key management
4//! and cryptographic operations such as public key retrieval and message signing.
5//!
6//! Supports EVM (secp256k1/ECDSA), Solana (Ed25519), and Stellar (Ed25519) networks.
7//!
8//! ## Features
9//!
10//! - Service account authentication using credential providers
11//! - Public key retrieval from KMS
12//! - Message signing via KMS for multiple key types
13//!
14//! ## Architecture
15//!
16//! ```text
17//! AwsKmsService (implements AwsKmsEvmService, AwsKmsSolanaService, AwsKmsStellarService)
18//!   ├── Authentication (via AwsKmsClient)
19//!   ├── Public Key Retrieval (via AwsKmsClient)
20//!   └── Message Signing (via AwsKmsClient)
21//! ```
22//! is based on
23//! ```text
24//! AwsKmsClient (implements AwsKmsK256, AwsKmsEd25519)
25//!   ├── Authentication (via shared credentials)
26//!   ├── Public Key Retrieval in DER Encoding
27//!   └── Message Signing (ECDSA for secp256k1, Ed25519 for EdDSA)
28//! ```
29//! `AwsKmsK256` and `AwsKmsEd25519` are mocked with `mockall` for unit testing
30//! and injected into `AwsKmsService`
31//!
32
33use alloy::primitives::keccak256;
34use async_trait::async_trait;
35use aws_config::{meta::region::RegionProviderChain, BehaviorVersion, Region};
36use aws_sdk_kms::{
37    primitives::Blob,
38    types::{MessageType, SigningAlgorithmSpec},
39    Client,
40};
41use once_cell::sync::Lazy;
42use serde::Serialize;
43use std::collections::HashMap;
44use tokio::sync::RwLock;
45
46use crate::{
47    models::{Address, AwsKmsSignerConfig},
48    services::signer::evm::utils::recover_evm_signature_from_der,
49    utils::{
50        self, derive_ethereum_address_from_der, derive_solana_address_from_der,
51        derive_stellar_address_from_der,
52    },
53};
54use tracing::debug;
55
56#[cfg(test)]
57use mockall::{automock, mock};
58
59#[derive(Clone, Debug, thiserror::Error, Serialize)]
60pub enum AwsKmsError {
61    #[error("AWS KMS response parse error: {0}")]
62    ParseError(String),
63    #[error("AWS KMS config error: {0}")]
64    ConfigError(String),
65    #[error("AWS KMS get error: {0}")]
66    GetError(String),
67    #[error("AWS KMS signing error: {0}")]
68    SignError(String),
69    #[error("AWS KMS permissions error: {0}")]
70    PermissionError(String),
71    #[error("AWS KMS public key error: {0}")]
72    RecoveryError(#[from] utils::Secp256k1Error),
73    #[error("AWS KMS conversion error: {0}")]
74    ConvertError(String),
75    #[error("AWS KMS Other error: {0}")]
76    Other(String),
77}
78
79pub type AwsKmsResult<T> = Result<T, AwsKmsError>;
80
81#[async_trait]
82#[cfg_attr(test, automock)]
83pub trait AwsKmsEvmService: Send + Sync {
84    /// Returns the EVM address derived from the configured public key.
85    async fn get_evm_address(&self) -> AwsKmsResult<Address>;
86    /// Signs a payload using the EVM signing scheme (hashes before signing).
87    ///
88    /// This method applies keccak256 hashing before signing.
89    ///
90    /// **Use for:**
91    /// - Raw transaction data (TxLegacy, TxEip1559)
92    /// - EIP-191 personal messages
93    ///
94    /// **Note:** For EIP-712 typed data, use `sign_hash_evm()` to avoid double-hashing.
95    async fn sign_payload_evm(&self, payload: &[u8]) -> AwsKmsResult<Vec<u8>>;
96
97    /// Signs a pre-computed hash using the EVM signing scheme (no hashing).
98    ///
99    /// This method signs the hash directly without applying keccak256.
100    ///
101    /// **Use for:**
102    /// - EIP-712 typed data (already hashed)
103    /// - Pre-computed message digests
104    ///
105    /// **Note:** For raw data, use `sign_payload_evm()` instead.
106    async fn sign_hash_evm(&self, hash: &[u8; 32]) -> AwsKmsResult<Vec<u8>>;
107}
108
109#[async_trait]
110#[cfg_attr(test, automock)]
111pub trait AwsKmsK256: Send + Sync {
112    /// Fetches the DER-encoded public key from AWS KMS.
113    async fn get_der_public_key<'a, 'b>(&'a self, key_id: &'b str) -> AwsKmsResult<Vec<u8>>;
114    /// Signs a digest using EcdsaSha256 spec. Returns DER-encoded signature
115    async fn sign_digest<'a, 'b>(
116        &'a self,
117        key_id: &'b str,
118        digest: [u8; 32],
119    ) -> AwsKmsResult<Vec<u8>>;
120}
121
122/// Trait for Ed25519 (EdDSA) operations with AWS KMS.
123/// Used for Solana and Stellar signing.
124#[async_trait]
125#[cfg_attr(test, automock)]
126pub trait AwsKmsEd25519: Send + Sync {
127    /// Fetches the DER-encoded Ed25519 public key from AWS KMS.
128    async fn get_ed25519_public_key<'a, 'b>(&'a self, key_id: &'b str) -> AwsKmsResult<Vec<u8>>;
129    /// Signs a message using Ed25519. Returns 64-byte signature.
130    /// Uses ED25519_SHA_512 algorithm with RAW message type.
131    async fn sign_ed25519<'a, 'b>(
132        &'a self,
133        key_id: &'b str,
134        message: &'b [u8],
135    ) -> AwsKmsResult<Vec<u8>>;
136}
137
138/// Trait for Solana-specific AWS KMS operations
139#[async_trait]
140#[cfg_attr(test, automock)]
141pub trait AwsKmsSolanaService: Send + Sync {
142    /// Returns the Solana address derived from the configured Ed25519 public key.
143    async fn get_solana_address(&self) -> AwsKmsResult<Address>;
144    /// Signs a message using Ed25519 for Solana.
145    async fn sign_solana(&self, message: &[u8]) -> AwsKmsResult<Vec<u8>>;
146}
147
148/// Trait for Stellar-specific AWS KMS operations
149#[async_trait]
150#[cfg_attr(test, automock)]
151pub trait AwsKmsStellarService: Send + Sync {
152    /// Returns the Stellar address derived from the configured Ed25519 public key.
153    async fn get_stellar_address(&self) -> AwsKmsResult<Address>;
154    /// Signs a message using Ed25519 for Stellar.
155    async fn sign_stellar(&self, message: &[u8]) -> AwsKmsResult<Vec<u8>>;
156}
157
158#[cfg(test)]
159mock! {
160    pub AwsKmsClient { }
161    impl Clone for AwsKmsClient {
162        fn clone(&self) -> Self;
163    }
164
165    #[async_trait]
166    impl AwsKmsK256 for AwsKmsClient {
167        async fn get_der_public_key<'a, 'b>(&'a self, key_id: &'b str) -> AwsKmsResult<Vec<u8>>;
168        async fn sign_digest<'a, 'b>(
169            &'a self,
170            key_id: &'b str,
171            digest: [u8; 32],
172        ) -> AwsKmsResult<Vec<u8>>;
173    }
174
175    #[async_trait]
176    impl AwsKmsEd25519 for AwsKmsClient {
177        async fn get_ed25519_public_key<'a, 'b>(&'a self, key_id: &'b str) -> AwsKmsResult<Vec<u8>>;
178        async fn sign_ed25519<'a, 'b>(
179            &'a self,
180            key_id: &'b str,
181            message: &'b [u8],
182        ) -> AwsKmsResult<Vec<u8>>;
183    }
184}
185
186// Global cache for secp256k1 public keys - HashMap keyed by kms_key_id
187static KMS_DER_PK_CACHE: Lazy<RwLock<HashMap<String, Vec<u8>>>> =
188    Lazy::new(|| RwLock::new(HashMap::new()));
189
190// Global cache for Ed25519 public keys - HashMap keyed by kms_key_id
191static KMS_ED25519_PK_CACHE: Lazy<RwLock<HashMap<String, Vec<u8>>>> =
192    Lazy::new(|| RwLock::new(HashMap::new()));
193
194#[derive(Debug, Clone)]
195pub struct AwsKmsClient {
196    inner: Client,
197}
198
199#[async_trait]
200impl AwsKmsK256 for AwsKmsClient {
201    async fn get_der_public_key<'a, 'b>(&'a self, key_id: &'b str) -> AwsKmsResult<Vec<u8>> {
202        // Try cache first with minimal lock time
203        let cached = {
204            let cache_read = KMS_DER_PK_CACHE.read().await;
205            cache_read.get(key_id).cloned()
206        };
207        if let Some(cached) = cached {
208            return Ok(cached);
209        }
210
211        // Fetch from AWS KMS
212        let get_output = self
213            .inner
214            .get_public_key()
215            .key_id(key_id)
216            .send()
217            .await
218            .map_err(|e| {
219                AwsKmsError::GetError(format!(
220                    "Failed to get secp256k1 public key for key '{key_id}': {e:?}"
221                ))
222            })?;
223
224        let der_pk_blob = get_output
225            .public_key
226            .ok_or(AwsKmsError::GetError(
227                "No public key blob found".to_string(),
228            ))?
229            .into_inner();
230
231        // Cache the result
232        let mut cache_write = KMS_DER_PK_CACHE.write().await;
233        cache_write.insert(key_id.to_string(), der_pk_blob.clone());
234        drop(cache_write);
235
236        Ok(der_pk_blob)
237    }
238
239    async fn sign_digest<'a, 'b>(
240        &'a self,
241        key_id: &'b str,
242        digest: [u8; 32],
243    ) -> AwsKmsResult<Vec<u8>> {
244        // Sign the digest with the AWS KMS
245        let sign_result = self
246            .inner
247            .sign()
248            .key_id(key_id)
249            .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256)
250            .message_type(MessageType::Digest)
251            .message(Blob::new(digest))
252            .send()
253            .await;
254
255        // Process the result, extract DER signature
256        let der_signature = sign_result
257            .map_err(|e| AwsKmsError::PermissionError(e.to_string()))?
258            .signature
259            .ok_or(AwsKmsError::SignError(
260                "Signature not found in response".to_string(),
261            ))?
262            .into_inner();
263
264        Ok(der_signature)
265    }
266}
267
268#[async_trait]
269impl AwsKmsEd25519 for AwsKmsClient {
270    async fn get_ed25519_public_key<'a, 'b>(&'a self, key_id: &'b str) -> AwsKmsResult<Vec<u8>> {
271        // Try cache first with minimal lock time
272        let cached = {
273            let cache_read = KMS_ED25519_PK_CACHE.read().await;
274            cache_read.get(key_id).cloned()
275        };
276        if let Some(cached) = cached {
277            return Ok(cached);
278        }
279
280        // Fetch from AWS KMS
281        let get_output = self
282            .inner
283            .get_public_key()
284            .key_id(key_id)
285            .send()
286            .await
287            .map_err(|e| {
288                AwsKmsError::GetError(format!(
289                    "Failed to get Ed25519 public key for key '{key_id}': {e:?}"
290                ))
291            })?;
292
293        let der_pk_blob = get_output
294            .public_key
295            .ok_or(AwsKmsError::GetError(
296                "No public key blob found".to_string(),
297            ))?
298            .into_inner();
299
300        // Cache the result
301        let mut cache_write = KMS_ED25519_PK_CACHE.write().await;
302        cache_write.insert(key_id.to_string(), der_pk_blob.clone());
303        drop(cache_write);
304
305        Ok(der_pk_blob)
306    }
307
308    async fn sign_ed25519<'a, 'b>(
309        &'a self,
310        key_id: &'b str,
311        message: &'b [u8],
312    ) -> AwsKmsResult<Vec<u8>> {
313        debug!("Signing Ed25519 message with AWS KMS, key_id: {}", key_id);
314
315        // Sign the message with Ed25519 using ED25519_SHA_512 algorithm
316        // Note: ED25519_SHA_512 requires MessageType::Raw - we pass the raw message
317        let sign_result = self
318            .inner
319            .sign()
320            .key_id(key_id)
321            .signing_algorithm(SigningAlgorithmSpec::Ed25519Sha512)
322            .message_type(MessageType::Raw)
323            .message(Blob::new(message))
324            .send()
325            .await;
326
327        // Process the result, extract signature
328        let signature = sign_result
329            .map_err(|e| AwsKmsError::SignError(e.to_string()))?
330            .signature
331            .ok_or(AwsKmsError::SignError(
332                "Signature not found in response".to_string(),
333            ))?
334            .into_inner();
335
336        // Ed25519 signatures should be 64 bytes
337        if signature.len() != 64 {
338            return Err(AwsKmsError::SignError(format!(
339                "Invalid Ed25519 signature length: expected 64 bytes, got {}",
340                signature.len()
341            )));
342        }
343
344        Ok(signature)
345    }
346}
347
348#[derive(Debug, Clone)]
349pub struct AwsKmsService<T: AwsKmsK256 + AwsKmsEd25519 + Clone = AwsKmsClient> {
350    pub kms_key_id: String,
351    client: T,
352}
353
354impl AwsKmsService<AwsKmsClient> {
355    pub async fn new(config: AwsKmsSignerConfig) -> AwsKmsResult<Self> {
356        let region_provider =
357            RegionProviderChain::first_try(config.region.map(Region::new)).or_default_provider();
358
359        let auth_config = aws_config::defaults(BehaviorVersion::latest())
360            .region(region_provider)
361            .load()
362            .await;
363        let client = AwsKmsClient {
364            inner: Client::new(&auth_config),
365        };
366
367        Ok(Self {
368            kms_key_id: config.key_id,
369            client,
370        })
371    }
372}
373
374#[cfg(test)]
375impl<T: AwsKmsK256 + AwsKmsEd25519 + Clone> AwsKmsService<T> {
376    pub fn new_for_testing(client: T, config: AwsKmsSignerConfig) -> Self {
377        Self {
378            client,
379            kms_key_id: config.key_id,
380        }
381    }
382}
383
384impl<T: AwsKmsK256 + AwsKmsEd25519 + Clone> AwsKmsService<T> {
385    /// Common signing logic for EVM signatures.
386    ///
387    /// This internal helper eliminates duplication between `sign_payload_evm` and `sign_hash_evm`.
388    ///
389    /// # Parameters
390    /// * `digest` - The 32-byte hash to sign
391    /// * `original_bytes` - The original message bytes for recovery verification (if applicable)
392    /// * `use_prehash_recovery` - If true, recovers using hash directly; if false, uses original bytes
393    async fn sign_and_recover_evm(
394        &self,
395        digest: [u8; 32],
396        original_bytes: &[u8],
397        use_prehash_recovery: bool,
398    ) -> AwsKmsResult<Vec<u8>> {
399        // Sign the digest with AWS KMS
400        let der_signature = self.client.sign_digest(&self.kms_key_id, digest).await?;
401
402        // Get public key
403        let der_pk = self.client.get_der_public_key(&self.kms_key_id).await?;
404
405        // Use shared signature recovery logic
406        recover_evm_signature_from_der(
407            &der_signature,
408            &der_pk,
409            digest,
410            original_bytes,
411            use_prehash_recovery,
412        )
413        .map_err(|e| AwsKmsError::ParseError(e.to_string()))
414    }
415
416    /// Signs a payload using the EVM signing scheme (hashes before signing).
417    ///
418    /// This method applies keccak256 hashing before signing.
419    ///
420    /// **Use for:**
421    /// - Raw transaction data (TxLegacy, TxEip1559)
422    /// - EIP-191 personal messages
423    ///
424    /// **Note:** For EIP-712 typed data, use `sign_hash_evm()` to avoid double-hashing.
425    pub async fn sign_payload_evm(&self, bytes: &[u8]) -> AwsKmsResult<Vec<u8>> {
426        let digest = keccak256(bytes).0;
427        self.sign_and_recover_evm(digest, bytes, false).await
428    }
429
430    /// Signs a pre-computed hash using the EVM signing scheme (no hashing).
431    ///
432    /// This method signs the hash directly without applying keccak256.
433    ///
434    /// **Use for:**
435    /// - EIP-712 typed data (already hashed)
436    /// - Pre-computed message digests
437    ///
438    /// **Note:** For raw data, use `sign_payload_evm()` instead.
439    pub async fn sign_hash_evm(&self, hash: &[u8; 32]) -> AwsKmsResult<Vec<u8>> {
440        self.sign_and_recover_evm(*hash, hash, true).await
441    }
442}
443
444#[async_trait]
445impl<T: AwsKmsK256 + AwsKmsEd25519 + Clone> AwsKmsEvmService for AwsKmsService<T> {
446    async fn get_evm_address(&self) -> AwsKmsResult<Address> {
447        let der = self.client.get_der_public_key(&self.kms_key_id).await?;
448        let eth_address = derive_ethereum_address_from_der(&der)
449            .map_err(|e| AwsKmsError::ParseError(e.to_string()))?;
450        Ok(Address::Evm(eth_address))
451    }
452
453    async fn sign_payload_evm(&self, message: &[u8]) -> AwsKmsResult<Vec<u8>> {
454        let digest = keccak256(message).0;
455        self.sign_and_recover_evm(digest, message, false).await
456    }
457
458    async fn sign_hash_evm(&self, hash: &[u8; 32]) -> AwsKmsResult<Vec<u8>> {
459        // Delegates to the implementation method on AwsKmsService
460        self.sign_and_recover_evm(*hash, hash, true).await
461    }
462}
463
464#[async_trait]
465impl<T: AwsKmsK256 + AwsKmsEd25519 + Clone> AwsKmsSolanaService for AwsKmsService<T> {
466    async fn get_solana_address(&self) -> AwsKmsResult<Address> {
467        let der = self.client.get_ed25519_public_key(&self.kms_key_id).await?;
468        let solana_address = derive_solana_address_from_der(&der)
469            .map_err(|e| AwsKmsError::ParseError(e.to_string()))?;
470        Ok(Address::Solana(solana_address))
471    }
472
473    async fn sign_solana(&self, message: &[u8]) -> AwsKmsResult<Vec<u8>> {
474        self.client.sign_ed25519(&self.kms_key_id, message).await
475    }
476}
477
478#[async_trait]
479impl<T: AwsKmsK256 + AwsKmsEd25519 + Clone> AwsKmsStellarService for AwsKmsService<T> {
480    async fn get_stellar_address(&self) -> AwsKmsResult<Address> {
481        let der = self.client.get_ed25519_public_key(&self.kms_key_id).await?;
482        let stellar_address = derive_stellar_address_from_der(&der)
483            .map_err(|e| AwsKmsError::ParseError(e.to_string()))?;
484        Ok(Address::Stellar(stellar_address))
485    }
486
487    async fn sign_stellar(&self, message: &[u8]) -> AwsKmsResult<Vec<u8>> {
488        self.client.sign_ed25519(&self.kms_key_id, message).await
489    }
490}
491
492#[cfg(test)]
493pub mod tests {
494    use super::*;
495
496    use alloy::primitives::utils::eip191_message;
497    use k256::{
498        ecdsa::SigningKey,
499        elliptic_curve::rand_core::OsRng,
500        pkcs8::{der::Encode, EncodePublicKey},
501    };
502    use mockall::predicate::{eq, ne};
503
504    /// Test Ed25519 key pair for mocking AWS KMS Ed25519 operations
505    pub struct TestEd25519Keys {
506        pub public_key_der: Vec<u8>,
507        pub public_key_raw: [u8; 32],
508    }
509
510    impl Default for TestEd25519Keys {
511        fn default() -> Self {
512            Self::new()
513        }
514    }
515
516    impl TestEd25519Keys {
517        pub fn new() -> Self {
518            // Well-known test Ed25519 public key (32 bytes)
519            let public_key_raw: [u8; 32] = [
520                0x9d, 0x45, 0x7e, 0x45, 0xe4, 0x16, 0xc4, 0xc6, 0x77, 0x67, 0x6a, 0x42, 0xff, 0x96,
521                0x8e, 0x3c, 0xf8, 0xdc, 0x73, 0xc8, 0xf3, 0x3a, 0x8d, 0x19, 0x81, 0x29, 0x7b, 0xfa,
522                0x3e, 0x00, 0x30, 0xba,
523            ];
524
525            // Ed25519 SPKI format: 12-byte header + 32-byte key
526            let mut public_key_der = vec![
527                0x30, 0x2a, // SEQUENCE, 42 bytes
528                0x30, 0x05, // SEQUENCE, 5 bytes
529                0x06, 0x03, 0x2b, 0x65, 0x70, // OID 1.3.101.112 (Ed25519)
530                0x03, 0x21, // BIT STRING, 33 bytes
531                0x00, // zero unused bits
532            ];
533            public_key_der.extend_from_slice(&public_key_raw);
534
535            Self {
536                public_key_der,
537                public_key_raw,
538            }
539        }
540    }
541
542    pub fn setup_mock_kms_client() -> (MockAwsKmsClient, SigningKey) {
543        let mut client = MockAwsKmsClient::new();
544        let signing_key = SigningKey::random(&mut OsRng);
545        let s = signing_key
546            .verifying_key()
547            .to_public_key_der()
548            .unwrap()
549            .to_der()
550            .unwrap();
551
552        client
553            .expect_get_der_public_key()
554            .with(eq("test-key-id"))
555            .return_const(Ok(s));
556        client
557            .expect_get_der_public_key()
558            .with(ne("test-key-id"))
559            .return_const(Err(AwsKmsError::GetError("Key does not exist".to_string())));
560
561        client
562            .expect_sign_digest()
563            .withf(|key_id, _| key_id.ne("test-key-id"))
564            .return_const(Err(AwsKmsError::SignError(
565                "Key does not exist".to_string(),
566            )));
567
568        let key = signing_key.clone();
569        client
570            .expect_sign_digest()
571            .withf(|key_id, _| key_id.eq("test-key-id"))
572            .returning(move |_, digest| {
573                let (signature, _) = signing_key
574                    .sign_prehash_recoverable(&digest)
575                    .map_err(|e| AwsKmsError::SignError(e.to_string()))?;
576                let der_signature = signature.to_der().as_bytes().to_vec();
577                Ok(der_signature)
578            });
579
580        // Setup Ed25519 mock expectations
581        let test_ed25519_keys = TestEd25519Keys::new();
582        client
583            .expect_get_ed25519_public_key()
584            .with(eq("test-key-id"))
585            .return_const(Ok(test_ed25519_keys.public_key_der.clone()));
586        client
587            .expect_get_ed25519_public_key()
588            .with(ne("test-key-id"))
589            .return_const(Err(AwsKmsError::GetError("Key does not exist".to_string())));
590
591        // Mock Ed25519 signing - return a fixed 64-byte signature
592        client
593            .expect_sign_ed25519()
594            .withf(|key_id, _| key_id.eq("test-key-id"))
595            .returning(|_, _| Ok(vec![0u8; 64]));
596        client
597            .expect_sign_ed25519()
598            .withf(|key_id, _| key_id.ne("test-key-id"))
599            .return_const(Err(AwsKmsError::SignError(
600                "Key does not exist".to_string(),
601            )));
602
603        client.expect_clone().return_once(MockAwsKmsClient::new);
604
605        (client, key)
606    }
607
608    #[tokio::test]
609    async fn test_get_public_key() {
610        let (mock_client, key) = setup_mock_kms_client();
611        let kms = AwsKmsService::new_for_testing(
612            mock_client,
613            AwsKmsSignerConfig {
614                region: Some("us-east-1".to_string()),
615                key_id: "test-key-id".to_string(),
616            },
617        );
618
619        let result = kms.get_evm_address().await;
620        assert!(result.is_ok());
621        if let Ok(Address::Evm(evm_address)) = result {
622            let expected_address = derive_ethereum_address_from_der(
623                key.verifying_key().to_public_key_der().unwrap().as_bytes(),
624            )
625            .unwrap();
626            assert_eq!(expected_address, evm_address);
627        }
628    }
629
630    #[tokio::test]
631    async fn test_get_public_key_fail() {
632        let (mock_client, _) = setup_mock_kms_client();
633        let kms = AwsKmsService::new_for_testing(
634            mock_client,
635            AwsKmsSignerConfig {
636                region: Some("us-east-1".to_string()),
637                key_id: "invalid-key-id".to_string(),
638            },
639        );
640
641        let result = kms.get_evm_address().await;
642        assert!(result.is_err());
643        if let Err(err) = result {
644            assert!(matches!(err, AwsKmsError::GetError(_)))
645        }
646    }
647
648    #[tokio::test]
649    async fn test_sign_digest() {
650        let (mock_client, _) = setup_mock_kms_client();
651        let kms = AwsKmsService::new_for_testing(
652            mock_client,
653            AwsKmsSignerConfig {
654                region: Some("us-east-1".to_string()),
655                key_id: "test-key-id".to_string(),
656            },
657        );
658
659        let message_eip = eip191_message(b"Hello World!");
660        let result = kms.sign_payload_evm(&message_eip).await;
661
662        // We just assert for Ok, since the pubkey recovery indicates the validity of signature
663        assert!(result.is_ok());
664    }
665
666    #[tokio::test]
667    async fn test_sign_digest_fail() {
668        let (mock_client, _) = setup_mock_kms_client();
669        let kms = AwsKmsService::new_for_testing(
670            mock_client,
671            AwsKmsSignerConfig {
672                region: Some("us-east-1".to_string()),
673                key_id: "invalid-key-id".to_string(),
674            },
675        );
676
677        let message_eip = eip191_message(b"Hello World!");
678        let result = kms.sign_payload_evm(&message_eip).await;
679        assert!(result.is_err());
680        if let Err(err) = result {
681            assert!(matches!(err, AwsKmsError::SignError(_)))
682        }
683    }
684
685    #[tokio::test]
686    async fn test_get_solana_address() {
687        let (mock_client, _) = setup_mock_kms_client();
688        let kms = AwsKmsService::new_for_testing(
689            mock_client,
690            AwsKmsSignerConfig {
691                region: Some("us-east-1".to_string()),
692                key_id: "test-key-id".to_string(),
693            },
694        );
695
696        let result = kms.get_solana_address().await;
697        assert!(result.is_ok());
698        if let Ok(Address::Solana(solana_address)) = result {
699            // Verify it's a valid base58-encoded address
700            assert!(!solana_address.is_empty());
701            assert!(solana_address.len() >= 32 && solana_address.len() <= 44);
702            // Verify it matches the expected address from our test key
703            let test_keys = TestEd25519Keys::new();
704            let expected_address = bs58::encode(test_keys.public_key_raw).into_string();
705            assert_eq!(solana_address, expected_address);
706        } else {
707            panic!("Expected Solana address");
708        }
709    }
710
711    #[tokio::test]
712    async fn test_get_solana_address_fail() {
713        let (mock_client, _) = setup_mock_kms_client();
714        let kms = AwsKmsService::new_for_testing(
715            mock_client,
716            AwsKmsSignerConfig {
717                region: Some("us-east-1".to_string()),
718                key_id: "invalid-key-id".to_string(),
719            },
720        );
721
722        let result = kms.get_solana_address().await;
723        assert!(result.is_err());
724        if let Err(err) = result {
725            assert!(matches!(err, AwsKmsError::GetError(_)))
726        }
727    }
728
729    #[tokio::test]
730    async fn test_sign_solana() {
731        let (mock_client, _) = setup_mock_kms_client();
732        let kms = AwsKmsService::new_for_testing(
733            mock_client,
734            AwsKmsSignerConfig {
735                region: Some("us-east-1".to_string()),
736                key_id: "test-key-id".to_string(),
737            },
738        );
739
740        let message = b"Test Solana message";
741        let result = kms.sign_solana(message).await;
742        assert!(result.is_ok());
743        let signature = result.unwrap();
744        assert_eq!(signature.len(), 64); // Ed25519 signatures are 64 bytes
745    }
746
747    #[tokio::test]
748    async fn test_sign_solana_fail() {
749        let (mock_client, _) = setup_mock_kms_client();
750        let kms = AwsKmsService::new_for_testing(
751            mock_client,
752            AwsKmsSignerConfig {
753                region: Some("us-east-1".to_string()),
754                key_id: "invalid-key-id".to_string(),
755            },
756        );
757
758        let message = b"Test Solana message";
759        let result = kms.sign_solana(message).await;
760        assert!(result.is_err());
761        if let Err(err) = result {
762            assert!(matches!(err, AwsKmsError::SignError(_)))
763        }
764    }
765
766    #[tokio::test]
767    async fn test_get_stellar_address() {
768        let (mock_client, _) = setup_mock_kms_client();
769        let kms = AwsKmsService::new_for_testing(
770            mock_client,
771            AwsKmsSignerConfig {
772                region: Some("us-east-1".to_string()),
773                key_id: "test-key-id".to_string(),
774            },
775        );
776
777        let result = kms.get_stellar_address().await;
778        assert!(result.is_ok());
779        if let Ok(Address::Stellar(stellar_address)) = result {
780            // Stellar addresses start with 'G' for public accounts
781            assert!(stellar_address.starts_with('G'));
782            // Stellar addresses are 56 characters long
783            assert_eq!(stellar_address.len(), 56);
784        } else {
785            panic!("Expected Stellar address");
786        }
787    }
788
789    #[tokio::test]
790    async fn test_get_stellar_address_fail() {
791        let (mock_client, _) = setup_mock_kms_client();
792        let kms = AwsKmsService::new_for_testing(
793            mock_client,
794            AwsKmsSignerConfig {
795                region: Some("us-east-1".to_string()),
796                key_id: "invalid-key-id".to_string(),
797            },
798        );
799
800        let result = kms.get_stellar_address().await;
801        assert!(result.is_err());
802        if let Err(err) = result {
803            assert!(matches!(err, AwsKmsError::GetError(_)))
804        }
805    }
806
807    #[tokio::test]
808    async fn test_sign_stellar() {
809        let (mock_client, _) = setup_mock_kms_client();
810        let kms = AwsKmsService::new_for_testing(
811            mock_client,
812            AwsKmsSignerConfig {
813                region: Some("us-east-1".to_string()),
814                key_id: "test-key-id".to_string(),
815            },
816        );
817
818        let message = b"Test Stellar message";
819        let result = kms.sign_stellar(message).await;
820        assert!(result.is_ok());
821        let signature = result.unwrap();
822        assert_eq!(signature.len(), 64); // Ed25519 signatures are 64 bytes
823    }
824
825    #[tokio::test]
826    async fn test_sign_stellar_fail() {
827        let (mock_client, _) = setup_mock_kms_client();
828        let kms = AwsKmsService::new_for_testing(
829            mock_client,
830            AwsKmsSignerConfig {
831                region: Some("us-east-1".to_string()),
832                key_id: "invalid-key-id".to_string(),
833            },
834        );
835
836        let message = b"Test Stellar message";
837        let result = kms.sign_stellar(message).await;
838        assert!(result.is_err());
839        if let Err(err) = result {
840            assert!(matches!(err, AwsKmsError::SignError(_)))
841        }
842    }
843
844    // Note: Ed25519 DER parsing tests are in utils/ed25519.rs
845}