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