openzeppelin_relayer/services/google_cloud_kms/
mod.rs

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