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