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