1use 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 async fn get_evm_address(&self) -> GoogleCloudKmsResult<Address>;
78 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 async fn get_pem_public_key(&self) -> GoogleCloudKmsResult<String>;
88 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 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 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 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 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 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 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 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", "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=" })))
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 })))
569 .mount(mock_server)
570 .await;
571 }
572
573 #[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 #[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"); let service = GoogleCloudKmsService::new(&config).unwrap();
793
794 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 })))
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}