openzeppelin_relayer/services/google_cloud_kms/
mod.rs

1//! # Google Cloud KMS Service Module
2//!
3//! This module provides integration with Google Cloud KMS for secure key management
4//! and cryptographic operations such as public key retrieval and message signing.
5//!
6//! ## Features
7//!
8//! - Service account authentication using google-cloud-auth
9//! - Public key retrieval from KMS
10//! - Message signing via KMS
11//!
12//! ## Architecture
13//!
14//! ```text
15//! GoogleCloudKmsService (implements GoogleCloudKmsServiceTrait, GoogleCloudKmsEvmService)
16//!   ├── Authentication (service account)
17//!   ├── Public Key Retrieval
18//!   └── Message Signing
19//! ```
20
21use alloy::primitives::keccak256;
22use async_trait::async_trait;
23use google_cloud_auth::credentials::{service_account::Builder as GcpCredBuilder, Credentials};
24#[cfg_attr(test, allow(unused_imports))]
25use http::{Extensions, HeaderMap};
26use reqwest::Client;
27use serde_json::Value;
28use sha2::{Digest, Sha256};
29use std::sync::Arc;
30use tokio::sync::RwLock;
31use tracing::debug;
32
33#[cfg(test)]
34use mockall::automock;
35
36use crate::models::{Address, GoogleCloudKmsSignerConfig};
37use crate::services::signer::evm::utils::recover_evm_signature_from_der;
38use crate::utils::{
39    self, base64_decode, base64_encode, derive_ethereum_address_from_pem,
40    derive_stellar_address_from_pem,
41};
42
43#[derive(Debug, thiserror::Error, serde::Serialize)]
44pub enum GoogleCloudKmsError {
45    #[error("KMS HTTP error: {0}")]
46    HttpError(String),
47    #[error("KMS API error: {0}")]
48    ApiError(String),
49    #[error("KMS response parse error: {0}")]
50    ParseError(String),
51    #[error("KMS missing field: {0}")]
52    MissingField(String),
53    #[error("KMS config error: {0}")]
54    ConfigError(String),
55    #[error("KMS conversion error: {0}")]
56    ConvertError(String),
57    #[error("KMS public key error: {0}")]
58    RecoveryError(#[from] utils::Secp256k1Error),
59    #[error("Other error: {0}")]
60    Other(String),
61}
62
63pub type GoogleCloudKmsResult<T> = Result<T, GoogleCloudKmsError>;
64
65#[async_trait]
66#[cfg_attr(test, automock)]
67pub trait GoogleCloudKmsServiceTrait: Send + Sync {
68    async fn get_solana_address(&self) -> GoogleCloudKmsResult<String>;
69    async fn sign_solana(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
70    async fn get_evm_address(&self) -> GoogleCloudKmsResult<String>;
71    async fn sign_evm(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
72    async fn get_stellar_address(&self) -> GoogleCloudKmsResult<String>;
73    async fn sign_stellar(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
74}
75
76#[async_trait]
77#[cfg_attr(test, automock)]
78pub trait GoogleCloudKmsEvmService: Send + Sync {
79    /// Returns the EVM address derived from the configured public key.
80    async fn get_evm_address(&self) -> GoogleCloudKmsResult<Address>;
81    /// Signs a payload using the EVM signing scheme (hashes before signing).
82    ///
83    /// This method applies keccak256 hashing before signing.
84    ///
85    /// **Use for:**
86    /// - Raw transaction data (TxLegacy, TxEip1559)
87    /// - EIP-191 personal messages
88    ///
89    /// **Note:** For EIP-712 typed data, use `sign_hash_evm()` to avoid double-hashing.
90    async fn sign_payload_evm(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
91
92    /// Signs a pre-computed hash using the EVM signing scheme (no hashing).
93    ///
94    /// This method signs the hash directly without applying keccak256.
95    ///
96    /// **Use for:**
97    /// - EIP-712 typed data (already hashed)
98    /// - Pre-computed message digests
99    ///
100    /// **Note:** For raw data, use `sign_payload_evm()` instead.
101    async fn sign_hash_evm(&self, hash: &[u8; 32]) -> GoogleCloudKmsResult<Vec<u8>>;
102}
103
104#[async_trait]
105#[cfg_attr(test, automock)]
106pub trait GoogleCloudKmsStellarService: Send + Sync {
107    /// Returns the Stellar address derived from the configured public key.
108    async fn get_stellar_address(&self) -> GoogleCloudKmsResult<Address>;
109    /// Signs a payload using the Stellar signing scheme.
110    /// Returns the signature in Stellar format.
111    async fn sign_payload_stellar(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
112}
113
114#[async_trait]
115#[cfg_attr(test, automock)]
116pub trait GoogleCloudKmsK256: Send + Sync {
117    /// Fetches the PEM-encoded public key from Google Cloud KMS.
118    async fn get_pem_public_key(&self) -> GoogleCloudKmsResult<String>;
119    /// Signs a digest using ECDSA_SHA256. Returns DER-encoded signature.
120    async fn sign_digest(&self, digest: [u8; 32]) -> GoogleCloudKmsResult<Vec<u8>>;
121}
122
123#[derive(Clone, Debug)]
124#[allow(dead_code)]
125pub struct GoogleCloudKmsService {
126    pub config: GoogleCloudKmsSignerConfig,
127    credentials: Arc<Credentials>,
128    client: Client,
129    cached_headers: Arc<RwLock<Option<HeaderMap>>>,
130}
131
132impl GoogleCloudKmsService {
133    pub fn new(config: &GoogleCloudKmsSignerConfig) -> GoogleCloudKmsResult<Self> {
134        let credentials_json = serde_json::json!({
135            "type": "service_account",
136            "project_id": config.service_account.project_id,
137            "private_key_id": config.service_account.private_key_id.to_str().to_string(),
138            "private_key": config.service_account.private_key.to_str().to_string(),
139            "client_email": config.service_account.client_email.to_str().to_string(),
140            "client_id": config.service_account.client_id,
141            "auth_uri": config.service_account.auth_uri,
142            "token_uri": config.service_account.token_uri,
143            "auth_provider_x509_cert_url": config.service_account.auth_provider_x509_cert_url,
144            "client_x509_cert_url": config.service_account.client_x509_cert_url,
145            "universe_domain": config.service_account.universe_domain,
146        });
147        let credentials = GcpCredBuilder::new(credentials_json)
148            .build()
149            .map_err(|e| GoogleCloudKmsError::ConfigError(e.to_string()))?;
150
151        Ok(Self {
152            config: config.clone(),
153            credentials: Arc::new(credentials),
154            client: Client::new(),
155            cached_headers: Arc::new(RwLock::new(None)),
156        })
157    }
158
159    async fn get_auth_headers(&self) -> GoogleCloudKmsResult<HeaderMap> {
160        #[cfg(test)]
161        {
162            // In test mode, return empty headers or mock headers
163            let mut headers = HeaderMap::new();
164            headers.insert("Authorization", "Bearer test-token".parse().unwrap());
165            Ok(headers)
166        }
167
168        #[cfg(not(test))]
169        {
170            let cacheable_headers = self
171                .credentials
172                .headers(Extensions::new())
173                .await
174                .map_err(|e| GoogleCloudKmsError::ConfigError(e.to_string()))?;
175
176            match cacheable_headers {
177                google_cloud_auth::credentials::CacheableResource::New { data, .. } => {
178                    let mut cached = self.cached_headers.write().await;
179                    *cached = Some(data.clone());
180                    Ok(data)
181                }
182                google_cloud_auth::credentials::CacheableResource::NotModified => {
183                    let cached = self.cached_headers.read().await;
184                    if let Some(headers) = cached.as_ref() {
185                        Ok(headers.clone())
186                    } else {
187                        Err(GoogleCloudKmsError::ConfigError(
188                            "KMS auth token not modified, but not found in cache".to_string(),
189                        ))
190                    }
191                }
192            }
193        }
194    }
195
196    fn get_base_url(&self) -> String {
197        if self
198            .config
199            .service_account
200            .universe_domain
201            .starts_with("http")
202        {
203            self.config.service_account.universe_domain.clone()
204        } else {
205            format!(
206                "https://cloudkms.{}",
207                self.config.service_account.universe_domain
208            )
209        }
210    }
211
212    async fn kms_get(&self, url: &str) -> GoogleCloudKmsResult<Value> {
213        let headers = self.get_auth_headers().await?;
214        let resp = self
215            .client
216            .get(url)
217            .headers(headers)
218            .send()
219            .await
220            .map_err(|e| GoogleCloudKmsError::HttpError(e.to_string()))?;
221
222        let status = resp.status();
223        let text = resp.text().await.unwrap_or_else(|_| "".to_string());
224
225        if !status.is_success() {
226            return Err(GoogleCloudKmsError::ApiError(format!(
227                "KMS request failed ({status}): {text}"
228            )));
229        }
230
231        serde_json::from_str(&text)
232            .map_err(|e| GoogleCloudKmsError::ParseError(format!("{e}: {text}")))
233    }
234
235    async fn kms_post(&self, url: &str, body: &Value) -> GoogleCloudKmsResult<Value> {
236        let headers = self.get_auth_headers().await?;
237        let resp = self
238            .client
239            .post(url)
240            .headers(headers)
241            .json(body)
242            .send()
243            .await
244            .map_err(|e| GoogleCloudKmsError::HttpError(e.to_string()))?;
245
246        let status = resp.status();
247        let text = resp.text().await.unwrap_or_else(|_| "".to_string());
248
249        if !status.is_success() {
250            return Err(GoogleCloudKmsError::ApiError(format!(
251                "KMS request failed ({status}): {text}"
252            )));
253        }
254
255        serde_json::from_str(&text)
256            .map_err(|e| GoogleCloudKmsError::ParseError(format!("{e}: {text}")))
257    }
258
259    fn get_key_path(&self) -> String {
260        format!(
261            "projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/{}",
262            self.config.service_account.project_id,
263            self.config.key.location,
264            self.config.key.key_ring_id,
265            self.config.key.key_id,
266            self.config.key.key_version
267        )
268    }
269
270    /// Fetches the PEM-encoded public key from KMS.
271    async fn get_pem(&self) -> GoogleCloudKmsResult<String> {
272        let base_url = self.get_base_url();
273        let key_path = self.get_key_path();
274        let url = format!("{base_url}/v1/{key_path}/publicKey",);
275        debug!(url = %url, "kms public key url");
276
277        let body = self.kms_get(&url).await?;
278        let pem_str = body
279            .get("pem")
280            .and_then(|v| v.as_str())
281            .ok_or_else(|| GoogleCloudKmsError::MissingField("pem".to_string()))?;
282
283        Ok(pem_str.to_string())
284    }
285
286    /// Common signing logic for EVM signatures.
287    ///
288    /// # Parameters
289    /// * `digest` - The 32-byte hash to sign
290    /// * `original_bytes` - The original message bytes for recovery verification (if applicable)
291    /// * `use_prehash_recovery` - If true, recovers using hash directly; if false, uses original bytes
292    async fn sign_and_recover_evm(
293        &self,
294        digest: [u8; 32],
295        original_bytes: &[u8],
296        use_prehash_recovery: bool,
297    ) -> GoogleCloudKmsResult<Vec<u8>> {
298        let der_signature = self.sign_digest(digest).await?;
299
300        let pem_str = self.get_pem().await?;
301
302        // Convert PEM to DER first
303        let pem_parsed =
304            pem::parse(&pem_str).map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
305        let der_pk = pem_parsed.contents();
306
307        // Use shared signature recovery logic
308        recover_evm_signature_from_der(
309            &der_signature,
310            der_pk,
311            digest,
312            original_bytes,
313            use_prehash_recovery,
314        )
315        .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))
316    }
317
318    /// Signs a payload using the EVM signing scheme (hashes before signing).
319    ///
320    /// This method applies keccak256 hashing before signing.
321    ///
322    /// **Use for:**
323    /// - Raw transaction data (TxLegacy, TxEip1559)
324    /// - EIP-191 personal messages
325    ///
326    /// **Note:** For EIP-712 typed data, use `sign_hash_evm()` to avoid double-hashing.
327    pub async fn sign_payload_evm(&self, bytes: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
328        let digest = keccak256(bytes).0;
329        self.sign_and_recover_evm(digest, bytes, false).await
330    }
331
332    /// Signs a pre-computed hash using the EVM signing scheme (no hashing).
333    ///
334    /// This method signs the hash directly without applying keccak256.
335    ///
336    /// **Use for:**
337    /// - EIP-712 typed data (already hashed)
338    /// - Pre-computed message digests
339    ///
340    /// **Note:** For raw data, use `sign_payload_evm()` instead.
341    pub async fn sign_hash_evm(&self, hash: &[u8; 32]) -> GoogleCloudKmsResult<Vec<u8>> {
342        self.sign_and_recover_evm(*hash, hash, true).await
343    }
344}
345
346#[async_trait]
347impl GoogleCloudKmsK256 for GoogleCloudKmsService {
348    async fn get_pem_public_key(&self) -> GoogleCloudKmsResult<String> {
349        self.get_pem().await
350    }
351
352    async fn sign_digest(&self, digest: [u8; 32]) -> GoogleCloudKmsResult<Vec<u8>> {
353        let base_url = self.get_base_url();
354        let key_path = self.get_key_path();
355        let url = format!("{base_url}/v1/{key_path}:asymmetricSign");
356
357        let digest_b64 = base64_encode(&digest);
358
359        let body = serde_json::json!({
360            "name": key_path,
361            "digest": {
362                "sha256": digest_b64
363            }
364        });
365
366        let resp = self.kms_post(&url, &body).await?;
367        let signature_b64 = resp
368            .get("signature")
369            .and_then(|v| v.as_str())
370            .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
371
372        let signature = base64_decode(signature_b64)
373            .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
374
375        Ok(signature)
376    }
377}
378
379#[async_trait]
380impl GoogleCloudKmsServiceTrait for GoogleCloudKmsService {
381    async fn get_solana_address(&self) -> GoogleCloudKmsResult<String> {
382        let pem_str = self.get_pem().await?;
383
384        debug!(pem_str = %pem_str, "pem solana");
385
386        utils::derive_solana_address_from_pem(&pem_str).map_err(GoogleCloudKmsError::from)
387    }
388
389    async fn get_evm_address(&self) -> GoogleCloudKmsResult<String> {
390        let pem_str = self.get_pem().await?;
391
392        debug!(pem_str = %pem_str, "pem evm");
393
394        let address_bytes =
395            utils::derive_ethereum_address_from_pem(&pem_str).map_err(GoogleCloudKmsError::from)?;
396        Ok(format!("0x{}", hex::encode(address_bytes)))
397    }
398
399    async fn sign_solana(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
400        let base_url = self.get_base_url();
401        let key_path = self.get_key_path();
402
403        let url = format!("{base_url}/v1/{key_path}:asymmetricSign",);
404
405        let body = serde_json::json!({
406            "name": key_path,
407            "data": base64_encode(message)
408        });
409
410        let resp = self.kms_post(&url, &body).await?;
411        let signature_b64 = resp
412            .get("signature")
413            .and_then(|v| v.as_str())
414            .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
415
416        let signature = base64_decode(signature_b64)
417            .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
418
419        Ok(signature)
420    }
421
422    async fn sign_evm(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
423        let base_url = self.get_base_url();
424        let key_path = self.get_key_path();
425        let url = format!("{base_url}/v1/{key_path}:asymmetricSign",);
426
427        let hash = Sha256::digest(message);
428        let digest = base64_encode(&hash);
429
430        let body = serde_json::json!({
431            "name": key_path,
432            "digest": {
433                "sha256": digest
434            }
435        });
436
437        debug!(body = ?body, "kms asymmetric sign body");
438
439        let resp = self.kms_post(&url, &body).await?;
440        let signature = resp
441            .get("signature")
442            .and_then(|v| v.as_str())
443            .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
444
445        debug!(resp = ?resp, "kms asymmetric sign response");
446        let signature_b64 =
447            base64_decode(signature).map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
448        debug!(signature_b64 = ?signature_b64, "signature b64 decoded");
449        Ok(signature_b64)
450    }
451
452    async fn get_stellar_address(&self) -> GoogleCloudKmsResult<String> {
453        let pem_str = self.get_pem().await?;
454
455        debug!(pem_str = %pem_str, "pem stellar");
456
457        utils::derive_stellar_address_from_pem(&pem_str).map_err(GoogleCloudKmsError::from)
458    }
459
460    async fn sign_stellar(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
461        let base_url = self.get_base_url();
462        let key_path = self.get_key_path();
463
464        let url = format!("{base_url}/v1/{key_path}:asymmetricSign",);
465        debug!(url = %url, "kms asymmetric sign url for stellar");
466
467        // For Ed25519, we can sign the message directly without pre-hashing
468        let body = serde_json::json!({
469            "name": key_path,
470            "data": base64_encode(message)
471        });
472
473        debug!(body = ?body, "kms asymmetric sign body for stellar");
474
475        let resp = self.kms_post(&url, &body).await?;
476        let signature_b64 = resp
477            .get("signature")
478            .and_then(|v| v.as_str())
479            .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
480
481        debug!(resp = ?resp, "kms asymmetric sign response for stellar");
482
483        let signature = base64_decode(signature_b64)
484            .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
485
486        Ok(signature)
487    }
488}
489
490#[async_trait]
491impl GoogleCloudKmsEvmService for GoogleCloudKmsService {
492    async fn get_evm_address(&self) -> GoogleCloudKmsResult<Address> {
493        let pem_str = self.get_pem().await?;
494        let eth_address = derive_ethereum_address_from_pem(&pem_str)
495            .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
496        Ok(Address::Evm(eth_address))
497    }
498
499    async fn sign_payload_evm(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
500        let digest = keccak256(payload).0;
501        self.sign_and_recover_evm(digest, payload, false).await
502    }
503
504    async fn sign_hash_evm(&self, hash: &[u8; 32]) -> GoogleCloudKmsResult<Vec<u8>> {
505        self.sign_and_recover_evm(*hash, hash, true).await
506    }
507}
508
509#[async_trait]
510impl GoogleCloudKmsStellarService for GoogleCloudKmsService {
511    async fn get_stellar_address(&self) -> GoogleCloudKmsResult<Address> {
512        let pem_str = self.get_pem().await?;
513        let stellar_address = derive_stellar_address_from_pem(&pem_str)
514            .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
515        Ok(Address::Stellar(stellar_address))
516    }
517
518    async fn sign_payload_stellar(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
519        // For Stellar/Ed25519, we can sign directly without pre-hashing
520        self.sign_stellar(payload).await
521    }
522}
523
524impl From<utils::AddressDerivationError> for GoogleCloudKmsError {
525    fn from(value: utils::AddressDerivationError) -> Self {
526        match value {
527            utils::AddressDerivationError::ParseError(msg) => GoogleCloudKmsError::ParseError(msg),
528        }
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use crate::models::{
536        GoogleCloudKmsSignerKeyConfig, GoogleCloudKmsSignerServiceAccountConfig, SecretString,
537    };
538    use alloy::primitives::utils::eip191_message;
539    use mockito::{Mock, ServerGuard};
540    use serde_json::json;
541
542    fn create_test_config(uri: &str) -> GoogleCloudKmsSignerConfig {
543        GoogleCloudKmsSignerConfig {
544            service_account: GoogleCloudKmsSignerServiceAccountConfig {
545                project_id: "test-project".to_string(),
546                private_key_id: SecretString::new("test-private-key-id"),
547                private_key: SecretString::new("-----BEGIN EXAMPLE PRIVATE KEY-----\nFAKEKEYDATA\n-----END EXAMPLE PRIVATE KEY-----\n"),
548                client_email: SecretString::new("test-service-account@example.com"),
549                client_id: "test-client-id".to_string(),
550                auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
551                token_uri: "https://oauth2.googleapis.com/token".to_string(),
552                client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test-service-account%40example.com".to_string(),
553                auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(),
554                universe_domain: uri.to_string(),
555            },
556            key: GoogleCloudKmsSignerKeyConfig {
557                location: "global".to_string(),
558                key_id: "test-key-id".to_string(),
559                key_ring_id: "test-key-ring-id".to_string(),
560                key_version: 1,
561            },
562        }
563    }
564
565    #[tokio::test]
566    async fn test_service_creation_success() {
567        let config = create_test_config("https://example.com");
568        let result = GoogleCloudKmsService::new(&config);
569        assert!(result.is_ok());
570    }
571
572    #[tokio::test]
573    async fn test_get_key_path_format() {
574        let config = create_test_config("https://example.com");
575        let service = GoogleCloudKmsService::new(&config).unwrap();
576
577        let key_path = service.get_key_path();
578        let expected = "projects/test-project/locations/global/keyRings/test-key-ring-id/cryptoKeys/test-key-id/cryptoKeyVersions/1";
579
580        assert_eq!(key_path, expected);
581    }
582
583    #[tokio::test]
584    async fn test_get_base_url_with_http_prefix() {
585        let config = create_test_config("http://localhost:8080");
586        let service = GoogleCloudKmsService::new(&config).unwrap();
587
588        let base_url = service.get_base_url();
589        assert_eq!(base_url, "http://localhost:8080");
590    }
591
592    #[tokio::test]
593    async fn test_get_base_url_without_http_prefix() {
594        let config = create_test_config("googleapis.com");
595        let service = GoogleCloudKmsService::new(&config).unwrap();
596
597        let base_url = service.get_base_url();
598        assert_eq!(base_url, "https://cloudkms.googleapis.com");
599    }
600
601    // Mock setup helpers
602    async fn setup_mock_solana_public_key(mock_server: &mut ServerGuard) -> Mock {
603        mock_server
604            .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
605            .match_header("Authorization", mockito::Matcher::Any)
606            .with_status(200)
607            .with_header("content-type", "application/json")
608            .with_body(serde_json::to_string(&json!({
609                "pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAVyC+iqnSu0vo6R8x0sRMhintQtoZgcLOur1VyvCrdrs=\n-----END PUBLIC KEY-----\n",
610                "algorithm": "ECDSA_P256_SHA256"
611            })).unwrap())
612            .create_async()
613            .await
614    }
615
616    async fn setup_mock_evm_public_key(mock_server: &mut ServerGuard) -> Mock {
617        mock_server
618            .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
619            .match_header("Authorization", mockito::Matcher::Any)
620            .with_status(200)
621            .with_header("content-type", "application/json")
622            .with_body(serde_json::to_string(&json!({
623                "pem": "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEjJaJh5wfZwvj8b3bQ4GYikqDTLXWUjMh\nkFs9lGj2N9B17zo37p4PSy99rDio0QHLadpso0rtTJDSISRW9MdOqA==\n-----END PUBLIC KEY-----\n", // noboost
624                "algorithm": "ECDSA_SECP256K1_SHA256"
625            })).unwrap())
626            .create_async()
627            .await
628    }
629
630    async fn setup_mock_sign_success(mock_server: &mut ServerGuard) -> Mock {
631        mock_server
632            .mock("POST", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign".to_string()))
633            .match_header("Authorization", mockito::Matcher::Any)
634            .with_status(200)
635            .with_header("content-type", "application/json")
636            .with_body(serde_json::to_string(&json!({
637                "signature": "ZHVtbXlzaWduYXR1cmU="  // Base64 encoded "dummysignature"
638            })).unwrap())
639            .create_async()
640            .await
641    }
642
643    async fn setup_mock_sign_error(mock_server: &mut ServerGuard) -> Mock {
644        mock_server
645            .mock("POST", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign".to_string()))
646            .match_header("Authorization", mockito::Matcher::Any)
647            .with_status(400)
648            .with_header("content-type", "application/json")
649            .with_body(serde_json::to_string(&json!({
650                "error": {
651                    "code": 400,
652                    "message": "Invalid request",
653                    "status": "INVALID_ARGUMENT"
654                }
655            })).unwrap())
656            .create_async()
657            .await
658    }
659
660    async fn setup_mock_get_key_error(mock_server: &mut ServerGuard) -> Mock {
661        mock_server
662            .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
663            .match_header("Authorization", mockito::Matcher::Any)
664            .with_status(404)
665            .with_header("content-type", "application/json")
666            .with_body(serde_json::to_string(&json!({
667                "error": {
668                    "code": 404,
669                    "message": "Key not found",
670                    "status": "NOT_FOUND"
671                }
672            })).unwrap())
673            .create_async()
674            .await
675    }
676
677    async fn setup_mock_malformed_response(mock_server: &mut ServerGuard) -> Mock {
678        mock_server
679            .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
680            .match_header("Authorization", mockito::Matcher::Any)
681            .with_status(200)
682            .with_header("content-type", "application/json")
683            .with_body(serde_json::to_string(&json!({
684                "algorithm": "ED25519"
685                // Missing "pem" field
686            })).unwrap())
687            .create_async()
688            .await
689    }
690
691    // GoogleCloudKmsServiceTrait tests
692    #[tokio::test]
693    async fn test_get_solana_address_success() {
694        let mut mock_server = mockito::Server::new_async().await;
695        let _mock = setup_mock_solana_public_key(&mut mock_server).await;
696
697        let config = create_test_config(&mock_server.url());
698        let service = GoogleCloudKmsService::new(&config).unwrap();
699
700        let result = service.get_solana_address().await;
701        assert!(result.is_ok());
702        assert_eq!(
703            result.unwrap(),
704            "6s7RsvzcdXFJi1tXeDoGfSKZWjCDNJLiu74rd72zLy6J"
705        );
706    }
707
708    #[tokio::test]
709    async fn test_get_solana_address_api_error() {
710        let mut mock_server = mockito::Server::new_async().await;
711        let _mock = setup_mock_get_key_error(&mut mock_server).await;
712
713        let config = create_test_config(&mock_server.url());
714        let service = GoogleCloudKmsService::new(&config).unwrap();
715
716        let result = service.get_solana_address().await;
717        assert!(result.is_err());
718        assert!(matches!(
719            result.unwrap_err(),
720            GoogleCloudKmsError::ApiError(_)
721        ));
722    }
723
724    #[tokio::test]
725    async fn test_get_evm_address_success() {
726        let mut mock_server = mockito::Server::new_async().await;
727        let _mock = setup_mock_evm_public_key(&mut mock_server).await;
728
729        let config = create_test_config(&mock_server.url());
730        let service = GoogleCloudKmsService::new(&config).unwrap();
731
732        let result = GoogleCloudKmsServiceTrait::get_evm_address(&service).await;
733        assert!(result.is_ok());
734
735        let address = result.unwrap();
736        assert!(address.starts_with("0x"));
737        assert_eq!(address.len(), 42);
738    }
739
740    #[tokio::test]
741    async fn test_sign_solana_success() {
742        let mut mock_server = mockito::Server::new_async().await;
743        let _mock = setup_mock_sign_success(&mut mock_server).await;
744
745        let config = create_test_config(&mock_server.url());
746        let service = GoogleCloudKmsService::new(&config).unwrap();
747
748        let result = service.sign_solana(b"test message").await;
749        assert!(result.is_ok());
750        assert_eq!(result.unwrap(), b"dummysignature");
751    }
752
753    #[tokio::test]
754    async fn test_sign_solana_api_error() {
755        let mut mock_server = mockito::Server::new_async().await;
756        let _mock = setup_mock_sign_error(&mut mock_server).await;
757
758        let config = create_test_config(&mock_server.url());
759        let service = GoogleCloudKmsService::new(&config).unwrap();
760
761        let result = service.sign_solana(b"test message").await;
762        assert!(result.is_err());
763        assert!(matches!(
764            result.unwrap_err(),
765            GoogleCloudKmsError::ApiError(_)
766        ));
767    }
768
769    #[tokio::test]
770    async fn test_sign_evm_success() {
771        let mut mock_server = mockito::Server::new_async().await;
772        let _mock = setup_mock_sign_success(&mut mock_server).await;
773
774        let config = create_test_config(&mock_server.url());
775        let service = GoogleCloudKmsService::new(&config).unwrap();
776
777        let result = service.sign_evm(b"test message").await;
778        assert!(result.is_ok());
779        assert_eq!(result.unwrap(), b"dummysignature");
780    }
781
782    #[tokio::test]
783    async fn test_sign_evm_api_error() {
784        let mut mock_server = mockito::Server::new_async().await;
785        let _mock = setup_mock_sign_error(&mut mock_server).await;
786
787        let config = create_test_config(&mock_server.url());
788        let service = GoogleCloudKmsService::new(&config).unwrap();
789
790        let result = service.sign_evm(b"test message").await;
791        assert!(result.is_err());
792        assert!(matches!(
793            result.unwrap_err(),
794            GoogleCloudKmsError::ApiError(_)
795        ));
796    }
797
798    // GoogleCloudKmsEvmService tests
799    #[tokio::test]
800    async fn test_evm_service_get_address_success() {
801        let mut mock_server = mockito::Server::new_async().await;
802        let _mock = setup_mock_evm_public_key(&mut mock_server).await;
803
804        let config = create_test_config(&mock_server.url());
805        let service = GoogleCloudKmsService::new(&config).unwrap();
806
807        let result = GoogleCloudKmsEvmService::get_evm_address(&service).await;
808        assert!(result.is_ok());
809
810        let address = result.unwrap();
811        assert!(matches!(address, Address::Evm(_)));
812        if let Address::Evm(addr) = address {
813            assert_eq!(addr.len(), 20);
814        }
815    }
816
817    #[tokio::test]
818    async fn test_evm_service_get_address_api_error() {
819        let mut mock_server = mockito::Server::new_async().await;
820        let _mock = setup_mock_get_key_error(&mut mock_server).await;
821
822        let config = create_test_config(&mock_server.url());
823        let service = GoogleCloudKmsService::new(&config).unwrap();
824
825        let result = GoogleCloudKmsEvmService::get_evm_address(&service).await;
826        assert!(result.is_err());
827        assert!(matches!(
828            result.unwrap_err(),
829            GoogleCloudKmsError::ApiError(_)
830        ));
831    }
832
833    #[tokio::test]
834    async fn test_sign_payload_evm_network_error() {
835        let config = create_test_config("http://invalid-host:9999");
836        let service = GoogleCloudKmsService::new(&config).unwrap();
837
838        let message = eip191_message(b"Hello World!");
839        let result = GoogleCloudKmsEvmService::sign_payload_evm(&service, &message).await;
840        assert!(result.is_err());
841        assert!(matches!(
842            result.unwrap_err(),
843            GoogleCloudKmsError::HttpError(_)
844        ));
845    }
846
847    #[tokio::test]
848    async fn test_get_pem_public_key_success() {
849        let mut mock_server = mockito::Server::new_async().await;
850        let _mock = setup_mock_evm_public_key(&mut mock_server).await;
851
852        let config = create_test_config(&mock_server.url());
853        let service = GoogleCloudKmsService::new(&config).unwrap();
854
855        let result = GoogleCloudKmsK256::get_pem_public_key(&service).await;
856        assert!(result.is_ok());
857        assert!(result.unwrap().contains("BEGIN PUBLIC KEY"));
858    }
859
860    #[tokio::test]
861    async fn test_get_pem_public_key_missing_field() {
862        let mut mock_server = mockito::Server::new_async().await;
863        let _mock = setup_mock_malformed_response(&mut mock_server).await;
864
865        let config = create_test_config(&mock_server.url());
866        let service = GoogleCloudKmsService::new(&config).unwrap();
867
868        let result = GoogleCloudKmsK256::get_pem_public_key(&service).await;
869        assert!(result.is_err());
870        assert!(matches!(
871            result.unwrap_err(),
872            GoogleCloudKmsError::MissingField(_)
873        ));
874    }
875
876    #[tokio::test]
877    async fn test_sign_digest_success() {
878        let mut mock_server = mockito::Server::new_async().await;
879        let _mock = setup_mock_sign_success(&mut mock_server).await;
880
881        let config = create_test_config(&mock_server.url());
882        let service = GoogleCloudKmsService::new(&config).unwrap();
883
884        let digest = [0u8; 32];
885        let result = GoogleCloudKmsK256::sign_digest(&service, digest).await;
886        assert!(result.is_ok());
887        assert_eq!(result.unwrap(), b"dummysignature");
888    }
889
890    #[tokio::test]
891    async fn test_sign_digest_api_error() {
892        let mut mock_server = mockito::Server::new_async().await;
893        let _mock = setup_mock_sign_error(&mut mock_server).await;
894
895        let config = create_test_config(&mock_server.url());
896        let service = GoogleCloudKmsService::new(&config).unwrap();
897
898        let digest = [0u8; 32];
899        let result = GoogleCloudKmsK256::sign_digest(&service, digest).await;
900        assert!(result.is_err());
901        assert!(matches!(
902            result.unwrap_err(),
903            GoogleCloudKmsError::ApiError(_)
904        ));
905    }
906
907    #[tokio::test]
908    async fn test_network_failure_handling() {
909        let config = create_test_config("http://localhost:99999"); // Invalid port
910        let service = GoogleCloudKmsService::new(&config).unwrap();
911
912        // Test all methods fail gracefully with network errors
913        let solana_addr_result = service.get_solana_address().await;
914        assert!(solana_addr_result.is_err());
915        assert!(matches!(
916            solana_addr_result.unwrap_err(),
917            GoogleCloudKmsError::HttpError(_)
918        ));
919
920        let evm_addr_result = GoogleCloudKmsServiceTrait::get_evm_address(&service).await;
921        assert!(evm_addr_result.is_err());
922        assert!(matches!(
923            evm_addr_result.unwrap_err(),
924            GoogleCloudKmsError::HttpError(_)
925        ));
926
927        let sign_solana_result = service.sign_solana(b"test").await;
928        assert!(sign_solana_result.is_err());
929        assert!(matches!(
930            sign_solana_result.unwrap_err(),
931            GoogleCloudKmsError::HttpError(_)
932        ));
933
934        let sign_evm_result = service.sign_evm(b"test").await;
935        assert!(sign_evm_result.is_err());
936        assert!(matches!(
937            sign_evm_result.unwrap_err(),
938            GoogleCloudKmsError::HttpError(_)
939        ));
940    }
941
942    #[tokio::test]
943    async fn test_config_with_different_universe_domains() {
944        let config1 = create_test_config("googleapis.com");
945        let service1 = GoogleCloudKmsService::new(&config1).unwrap();
946        assert_eq!(service1.get_base_url(), "https://cloudkms.googleapis.com");
947
948        let config2 = create_test_config("https://custom-domain.com");
949        let service2 = GoogleCloudKmsService::new(&config2).unwrap();
950        assert_eq!(service2.get_base_url(), "https://custom-domain.com");
951    }
952
953    #[tokio::test]
954    async fn test_solana_address_derivation() {
955        let valid_ed25519_pem = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAnUV+ReQWxMZ3Z2pC/5aOPPjcc8jzOo0ZgSl7+j4AMLo=\n-----END PUBLIC KEY-----\n";
956        let result = utils::derive_solana_address_from_pem(valid_ed25519_pem);
957        assert!(result.is_ok());
958        assert_eq!(
959            result.unwrap(),
960            "BavUBpkD77FABnevMkBVqV8BDHv7gX8sSoYYJY9WU9L5"
961        );
962    }
963
964    #[tokio::test]
965    async fn test_malformed_json_response() {
966        let mut mock_server = mockito::Server::new_async().await;
967
968        let _mock = mock_server
969            .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
970            .match_header("Authorization", mockito::Matcher::Any)
971            .with_status(200)
972            .with_header("content-type", "application/json")
973            .with_body("invalid json")
974            .create_async()
975            .await;
976
977        let config = create_test_config(&mock_server.url());
978        let service = GoogleCloudKmsService::new(&config).unwrap();
979
980        let result = service.get_solana_address().await;
981        assert!(result.is_err());
982        assert!(matches!(
983            result.unwrap_err(),
984            GoogleCloudKmsError::ParseError(_)
985        ));
986    }
987
988    #[tokio::test]
989    async fn test_missing_signature_field_in_response() {
990        let mut mock_server = mockito::Server::new_async().await;
991
992        let _mock = mock_server
993            .mock("POST", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign".to_string()))
994            .match_header("Authorization", mockito::Matcher::Any)
995            .with_status(200)
996            .with_header("content-type", "application/json")
997            .with_body(serde_json::to_string(&json!({
998                "name": "test-key"
999                // Missing "signature" field
1000            })).unwrap())
1001            .create_async()
1002            .await;
1003
1004        let config = create_test_config(&mock_server.url());
1005        let service = GoogleCloudKmsService::new(&config).unwrap();
1006
1007        let result = service.sign_solana(b"test").await;
1008        assert!(result.is_err());
1009        assert!(matches!(
1010            result.unwrap_err(),
1011            GoogleCloudKmsError::MissingField(_)
1012        ));
1013    }
1014}