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 TestEd25519Keys {
511        pub fn new() -> Self {
512            // Well-known test Ed25519 public key (32 bytes)
513            let public_key_raw: [u8; 32] = [
514                0x9d, 0x45, 0x7e, 0x45, 0xe4, 0x16, 0xc4, 0xc6, 0x77, 0x67, 0x6a, 0x42, 0xff, 0x96,
515                0x8e, 0x3c, 0xf8, 0xdc, 0x73, 0xc8, 0xf3, 0x3a, 0x8d, 0x19, 0x81, 0x29, 0x7b, 0xfa,
516                0x3e, 0x00, 0x30, 0xba,
517            ];
518
519            // Ed25519 SPKI format: 12-byte header + 32-byte key
520            let mut public_key_der = vec![
521                0x30, 0x2a, // SEQUENCE, 42 bytes
522                0x30, 0x05, // SEQUENCE, 5 bytes
523                0x06, 0x03, 0x2b, 0x65, 0x70, // OID 1.3.101.112 (Ed25519)
524                0x03, 0x21, // BIT STRING, 33 bytes
525                0x00, // zero unused bits
526            ];
527            public_key_der.extend_from_slice(&public_key_raw);
528
529            Self {
530                public_key_der,
531                public_key_raw,
532            }
533        }
534    }
535
536    pub fn setup_mock_kms_client() -> (MockAwsKmsClient, SigningKey) {
537        let mut client = MockAwsKmsClient::new();
538        let signing_key = SigningKey::random(&mut OsRng);
539        let s = signing_key
540            .verifying_key()
541            .to_public_key_der()
542            .unwrap()
543            .to_der()
544            .unwrap();
545
546        client
547            .expect_get_der_public_key()
548            .with(eq("test-key-id"))
549            .return_const(Ok(s));
550        client
551            .expect_get_der_public_key()
552            .with(ne("test-key-id"))
553            .return_const(Err(AwsKmsError::GetError("Key does not exist".to_string())));
554
555        client
556            .expect_sign_digest()
557            .withf(|key_id, _| key_id.ne("test-key-id"))
558            .return_const(Err(AwsKmsError::SignError(
559                "Key does not exist".to_string(),
560            )));
561
562        let key = signing_key.clone();
563        client
564            .expect_sign_digest()
565            .withf(|key_id, _| key_id.eq("test-key-id"))
566            .returning(move |_, digest| {
567                let (signature, _) = signing_key
568                    .sign_prehash_recoverable(&digest)
569                    .map_err(|e| AwsKmsError::SignError(e.to_string()))?;
570                let der_signature = signature.to_der().as_bytes().to_vec();
571                Ok(der_signature)
572            });
573
574        // Setup Ed25519 mock expectations
575        let test_ed25519_keys = TestEd25519Keys::new();
576        client
577            .expect_get_ed25519_public_key()
578            .with(eq("test-key-id"))
579            .return_const(Ok(test_ed25519_keys.public_key_der.clone()));
580        client
581            .expect_get_ed25519_public_key()
582            .with(ne("test-key-id"))
583            .return_const(Err(AwsKmsError::GetError("Key does not exist".to_string())));
584
585        // Mock Ed25519 signing - return a fixed 64-byte signature
586        client
587            .expect_sign_ed25519()
588            .withf(|key_id, _| key_id.eq("test-key-id"))
589            .returning(|_, _| Ok(vec![0u8; 64]));
590        client
591            .expect_sign_ed25519()
592            .withf(|key_id, _| key_id.ne("test-key-id"))
593            .return_const(Err(AwsKmsError::SignError(
594                "Key does not exist".to_string(),
595            )));
596
597        client.expect_clone().return_once(MockAwsKmsClient::new);
598
599        (client, key)
600    }
601
602    #[tokio::test]
603    async fn test_get_public_key() {
604        let (mock_client, key) = setup_mock_kms_client();
605        let kms = AwsKmsService::new_for_testing(
606            mock_client,
607            AwsKmsSignerConfig {
608                region: Some("us-east-1".to_string()),
609                key_id: "test-key-id".to_string(),
610            },
611        );
612
613        let result = kms.get_evm_address().await;
614        assert!(result.is_ok());
615        if let Ok(Address::Evm(evm_address)) = result {
616            let expected_address = derive_ethereum_address_from_der(
617                key.verifying_key().to_public_key_der().unwrap().as_bytes(),
618            )
619            .unwrap();
620            assert_eq!(expected_address, evm_address);
621        }
622    }
623
624    #[tokio::test]
625    async fn test_get_public_key_fail() {
626        let (mock_client, _) = setup_mock_kms_client();
627        let kms = AwsKmsService::new_for_testing(
628            mock_client,
629            AwsKmsSignerConfig {
630                region: Some("us-east-1".to_string()),
631                key_id: "invalid-key-id".to_string(),
632            },
633        );
634
635        let result = kms.get_evm_address().await;
636        assert!(result.is_err());
637        if let Err(err) = result {
638            assert!(matches!(err, AwsKmsError::GetError(_)))
639        }
640    }
641
642    #[tokio::test]
643    async fn test_sign_digest() {
644        let (mock_client, _) = setup_mock_kms_client();
645        let kms = AwsKmsService::new_for_testing(
646            mock_client,
647            AwsKmsSignerConfig {
648                region: Some("us-east-1".to_string()),
649                key_id: "test-key-id".to_string(),
650            },
651        );
652
653        let message_eip = eip191_message(b"Hello World!");
654        let result = kms.sign_payload_evm(&message_eip).await;
655
656        // We just assert for Ok, since the pubkey recovery indicates the validity of signature
657        assert!(result.is_ok());
658    }
659
660    #[tokio::test]
661    async fn test_sign_digest_fail() {
662        let (mock_client, _) = setup_mock_kms_client();
663        let kms = AwsKmsService::new_for_testing(
664            mock_client,
665            AwsKmsSignerConfig {
666                region: Some("us-east-1".to_string()),
667                key_id: "invalid-key-id".to_string(),
668            },
669        );
670
671        let message_eip = eip191_message(b"Hello World!");
672        let result = kms.sign_payload_evm(&message_eip).await;
673        assert!(result.is_err());
674        if let Err(err) = result {
675            assert!(matches!(err, AwsKmsError::SignError(_)))
676        }
677    }
678
679    #[tokio::test]
680    async fn test_get_solana_address() {
681        let (mock_client, _) = setup_mock_kms_client();
682        let kms = AwsKmsService::new_for_testing(
683            mock_client,
684            AwsKmsSignerConfig {
685                region: Some("us-east-1".to_string()),
686                key_id: "test-key-id".to_string(),
687            },
688        );
689
690        let result = kms.get_solana_address().await;
691        assert!(result.is_ok());
692        if let Ok(Address::Solana(solana_address)) = result {
693            // Verify it's a valid base58-encoded address
694            assert!(!solana_address.is_empty());
695            assert!(solana_address.len() >= 32 && solana_address.len() <= 44);
696            // Verify it matches the expected address from our test key
697            let test_keys = TestEd25519Keys::new();
698            let expected_address = bs58::encode(test_keys.public_key_raw).into_string();
699            assert_eq!(solana_address, expected_address);
700        } else {
701            panic!("Expected Solana address");
702        }
703    }
704
705    #[tokio::test]
706    async fn test_get_solana_address_fail() {
707        let (mock_client, _) = setup_mock_kms_client();
708        let kms = AwsKmsService::new_for_testing(
709            mock_client,
710            AwsKmsSignerConfig {
711                region: Some("us-east-1".to_string()),
712                key_id: "invalid-key-id".to_string(),
713            },
714        );
715
716        let result = kms.get_solana_address().await;
717        assert!(result.is_err());
718        if let Err(err) = result {
719            assert!(matches!(err, AwsKmsError::GetError(_)))
720        }
721    }
722
723    #[tokio::test]
724    async fn test_sign_solana() {
725        let (mock_client, _) = setup_mock_kms_client();
726        let kms = AwsKmsService::new_for_testing(
727            mock_client,
728            AwsKmsSignerConfig {
729                region: Some("us-east-1".to_string()),
730                key_id: "test-key-id".to_string(),
731            },
732        );
733
734        let message = b"Test Solana message";
735        let result = kms.sign_solana(message).await;
736        assert!(result.is_ok());
737        let signature = result.unwrap();
738        assert_eq!(signature.len(), 64); // Ed25519 signatures are 64 bytes
739    }
740
741    #[tokio::test]
742    async fn test_sign_solana_fail() {
743        let (mock_client, _) = setup_mock_kms_client();
744        let kms = AwsKmsService::new_for_testing(
745            mock_client,
746            AwsKmsSignerConfig {
747                region: Some("us-east-1".to_string()),
748                key_id: "invalid-key-id".to_string(),
749            },
750        );
751
752        let message = b"Test Solana message";
753        let result = kms.sign_solana(message).await;
754        assert!(result.is_err());
755        if let Err(err) = result {
756            assert!(matches!(err, AwsKmsError::SignError(_)))
757        }
758    }
759
760    #[tokio::test]
761    async fn test_get_stellar_address() {
762        let (mock_client, _) = setup_mock_kms_client();
763        let kms = AwsKmsService::new_for_testing(
764            mock_client,
765            AwsKmsSignerConfig {
766                region: Some("us-east-1".to_string()),
767                key_id: "test-key-id".to_string(),
768            },
769        );
770
771        let result = kms.get_stellar_address().await;
772        assert!(result.is_ok());
773        if let Ok(Address::Stellar(stellar_address)) = result {
774            // Stellar addresses start with 'G' for public accounts
775            assert!(stellar_address.starts_with('G'));
776            // Stellar addresses are 56 characters long
777            assert_eq!(stellar_address.len(), 56);
778        } else {
779            panic!("Expected Stellar address");
780        }
781    }
782
783    #[tokio::test]
784    async fn test_get_stellar_address_fail() {
785        let (mock_client, _) = setup_mock_kms_client();
786        let kms = AwsKmsService::new_for_testing(
787            mock_client,
788            AwsKmsSignerConfig {
789                region: Some("us-east-1".to_string()),
790                key_id: "invalid-key-id".to_string(),
791            },
792        );
793
794        let result = kms.get_stellar_address().await;
795        assert!(result.is_err());
796        if let Err(err) = result {
797            assert!(matches!(err, AwsKmsError::GetError(_)))
798        }
799    }
800
801    #[tokio::test]
802    async fn test_sign_stellar() {
803        let (mock_client, _) = setup_mock_kms_client();
804        let kms = AwsKmsService::new_for_testing(
805            mock_client,
806            AwsKmsSignerConfig {
807                region: Some("us-east-1".to_string()),
808                key_id: "test-key-id".to_string(),
809            },
810        );
811
812        let message = b"Test Stellar message";
813        let result = kms.sign_stellar(message).await;
814        assert!(result.is_ok());
815        let signature = result.unwrap();
816        assert_eq!(signature.len(), 64); // Ed25519 signatures are 64 bytes
817    }
818
819    #[tokio::test]
820    async fn test_sign_stellar_fail() {
821        let (mock_client, _) = setup_mock_kms_client();
822        let kms = AwsKmsService::new_for_testing(
823            mock_client,
824            AwsKmsSignerConfig {
825                region: Some("us-east-1".to_string()),
826                key_id: "invalid-key-id".to_string(),
827            },
828        );
829
830        let message = b"Test Stellar message";
831        let result = kms.sign_stellar(message).await;
832        assert!(result.is_err());
833        if let Err(err) = result {
834            assert!(matches!(err, AwsKmsError::SignError(_)))
835        }
836    }
837
838    // Note: Ed25519 DER parsing tests are in utils/ed25519.rs
839}