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 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 async fn get_evm_address(&self) -> GoogleCloudKmsResult<Address>;
81 async fn sign_payload_evm(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
91
92 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 async fn get_stellar_address(&self) -> GoogleCloudKmsResult<Address>;
109 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 async fn get_pem_public_key(&self) -> GoogleCloudKmsResult<String>;
119 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,
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,
141 "auth_uri": config.service_account.auth_uri,
142 "token_uri": config.service_account.token_uri,
143 "auth_provider_x509_cert_url": config.service_account.auth_provider_x509_cert_url,
144 "client_x509_cert_url": config.service_account.client_x509_cert_url,
145 "universe_domain": config.service_account.universe_domain,
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 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 if self
198 .config
199 .service_account
200 .universe_domain
201 .starts_with("http")
202 {
203 self.config.service_account.universe_domain.clone()
204 } else {
205 format!(
206 "https://cloudkms.{}",
207 self.config.service_account.universe_domain
208 )
209 }
210 }
211
212 async fn kms_get(&self, url: &str) -> GoogleCloudKmsResult<Value> {
213 let headers = self.get_auth_headers().await?;
214 let resp = self
215 .client
216 .get(url)
217 .headers(headers)
218 .send()
219 .await
220 .map_err(|e| GoogleCloudKmsError::HttpError(e.to_string()))?;
221
222 let status = resp.status();
223 let text = resp.text().await.unwrap_or_else(|_| "".to_string());
224
225 if !status.is_success() {
226 return Err(GoogleCloudKmsError::ApiError(format!(
227 "KMS request failed ({status}): {text}"
228 )));
229 }
230
231 serde_json::from_str(&text)
232 .map_err(|e| GoogleCloudKmsError::ParseError(format!("{e}: {text}")))
233 }
234
235 async fn kms_post(&self, url: &str, body: &Value) -> GoogleCloudKmsResult<Value> {
236 let headers = self.get_auth_headers().await?;
237 let resp = self
238 .client
239 .post(url)
240 .headers(headers)
241 .json(body)
242 .send()
243 .await
244 .map_err(|e| GoogleCloudKmsError::HttpError(e.to_string()))?;
245
246 let status = resp.status();
247 let text = resp.text().await.unwrap_or_else(|_| "".to_string());
248
249 if !status.is_success() {
250 return Err(GoogleCloudKmsError::ApiError(format!(
251 "KMS request failed ({status}): {text}"
252 )));
253 }
254
255 serde_json::from_str(&text)
256 .map_err(|e| GoogleCloudKmsError::ParseError(format!("{e}: {text}")))
257 }
258
259 fn get_key_path(&self) -> String {
260 format!(
261 "projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/{}",
262 self.config.service_account.project_id,
263 self.config.key.location,
264 self.config.key.key_ring_id,
265 self.config.key.key_id,
266 self.config.key.key_version
267 )
268 }
269
270 async fn get_pem(&self) -> GoogleCloudKmsResult<String> {
272 let base_url = self.get_base_url();
273 let key_path = self.get_key_path();
274 let url = format!("{base_url}/v1/{key_path}/publicKey",);
275 debug!(url = %url, "kms public key url");
276
277 let body = self.kms_get(&url).await?;
278 let pem_str = body
279 .get("pem")
280 .and_then(|v| v.as_str())
281 .ok_or_else(|| GoogleCloudKmsError::MissingField("pem".to_string()))?;
282
283 Ok(pem_str.to_string())
284 }
285
286 async fn sign_and_recover_evm(
293 &self,
294 digest: [u8; 32],
295 original_bytes: &[u8],
296 use_prehash_recovery: bool,
297 ) -> GoogleCloudKmsResult<Vec<u8>> {
298 let der_signature = self.sign_digest(digest).await?;
299
300 let pem_str = self.get_pem().await?;
301
302 let pem_parsed =
304 pem::parse(&pem_str).map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
305 let der_pk = pem_parsed.contents();
306
307 recover_evm_signature_from_der(
309 &der_signature,
310 der_pk,
311 digest,
312 original_bytes,
313 use_prehash_recovery,
314 )
315 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))
316 }
317
318 pub async fn sign_payload_evm(&self, bytes: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
328 let digest = keccak256(bytes).0;
329 self.sign_and_recover_evm(digest, bytes, false).await
330 }
331
332 pub async fn sign_hash_evm(&self, hash: &[u8; 32]) -> GoogleCloudKmsResult<Vec<u8>> {
342 self.sign_and_recover_evm(*hash, hash, true).await
343 }
344}
345
346#[async_trait]
347impl GoogleCloudKmsK256 for GoogleCloudKmsService {
348 async fn get_pem_public_key(&self) -> GoogleCloudKmsResult<String> {
349 self.get_pem().await
350 }
351
352 async fn sign_digest(&self, digest: [u8; 32]) -> GoogleCloudKmsResult<Vec<u8>> {
353 let base_url = self.get_base_url();
354 let key_path = self.get_key_path();
355 let url = format!("{base_url}/v1/{key_path}:asymmetricSign");
356
357 let digest_b64 = base64_encode(&digest);
358
359 let body = serde_json::json!({
360 "name": key_path,
361 "digest": {
362 "sha256": digest_b64
363 }
364 });
365
366 let resp = self.kms_post(&url, &body).await?;
367 let signature_b64 = resp
368 .get("signature")
369 .and_then(|v| v.as_str())
370 .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
371
372 let signature = base64_decode(signature_b64)
373 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
374
375 Ok(signature)
376 }
377}
378
379#[async_trait]
380impl GoogleCloudKmsServiceTrait for GoogleCloudKmsService {
381 async fn get_solana_address(&self) -> GoogleCloudKmsResult<String> {
382 let pem_str = self.get_pem().await?;
383
384 debug!(pem_str = %pem_str, "pem solana");
385
386 utils::derive_solana_address_from_pem(&pem_str).map_err(GoogleCloudKmsError::from)
387 }
388
389 async fn get_evm_address(&self) -> GoogleCloudKmsResult<String> {
390 let pem_str = self.get_pem().await?;
391
392 debug!(pem_str = %pem_str, "pem evm");
393
394 let address_bytes =
395 utils::derive_ethereum_address_from_pem(&pem_str).map_err(GoogleCloudKmsError::from)?;
396 Ok(format!("0x{}", hex::encode(address_bytes)))
397 }
398
399 async fn sign_solana(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
400 let base_url = self.get_base_url();
401 let key_path = self.get_key_path();
402
403 let url = format!("{base_url}/v1/{key_path}:asymmetricSign",);
404
405 let body = serde_json::json!({
406 "name": key_path,
407 "data": base64_encode(message)
408 });
409
410 let resp = self.kms_post(&url, &body).await?;
411 let signature_b64 = resp
412 .get("signature")
413 .and_then(|v| v.as_str())
414 .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
415
416 let signature = base64_decode(signature_b64)
417 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
418
419 Ok(signature)
420 }
421
422 async fn sign_evm(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
423 let base_url = self.get_base_url();
424 let key_path = self.get_key_path();
425 let url = format!("{base_url}/v1/{key_path}:asymmetricSign",);
426
427 let hash = Sha256::digest(message);
428 let digest = base64_encode(&hash);
429
430 let body = serde_json::json!({
431 "name": key_path,
432 "digest": {
433 "sha256": digest
434 }
435 });
436
437 debug!(body = ?body, "kms asymmetric sign body");
438
439 let resp = self.kms_post(&url, &body).await?;
440 let signature = resp
441 .get("signature")
442 .and_then(|v| v.as_str())
443 .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
444
445 debug!(resp = ?resp, "kms asymmetric sign response");
446 let signature_b64 =
447 base64_decode(signature).map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
448 debug!(signature_b64 = ?signature_b64, "signature b64 decoded");
449 Ok(signature_b64)
450 }
451
452 async fn get_stellar_address(&self) -> GoogleCloudKmsResult<String> {
453 let pem_str = self.get_pem().await?;
454
455 debug!(pem_str = %pem_str, "pem stellar");
456
457 utils::derive_stellar_address_from_pem(&pem_str).map_err(GoogleCloudKmsError::from)
458 }
459
460 async fn sign_stellar(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
461 let base_url = self.get_base_url();
462 let key_path = self.get_key_path();
463
464 let url = format!("{base_url}/v1/{key_path}:asymmetricSign",);
465 debug!(url = %url, "kms asymmetric sign url for stellar");
466
467 let body = serde_json::json!({
469 "name": key_path,
470 "data": base64_encode(message)
471 });
472
473 debug!(body = ?body, "kms asymmetric sign body for stellar");
474
475 let resp = self.kms_post(&url, &body).await?;
476 let signature_b64 = resp
477 .get("signature")
478 .and_then(|v| v.as_str())
479 .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
480
481 debug!(resp = ?resp, "kms asymmetric sign response for stellar");
482
483 let signature = base64_decode(signature_b64)
484 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
485
486 Ok(signature)
487 }
488}
489
490#[async_trait]
491impl GoogleCloudKmsEvmService for GoogleCloudKmsService {
492 async fn get_evm_address(&self) -> GoogleCloudKmsResult<Address> {
493 let pem_str = self.get_pem().await?;
494 let eth_address = derive_ethereum_address_from_pem(&pem_str)
495 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
496 Ok(Address::Evm(eth_address))
497 }
498
499 async fn sign_payload_evm(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
500 let digest = keccak256(payload).0;
501 self.sign_and_recover_evm(digest, payload, false).await
502 }
503
504 async fn sign_hash_evm(&self, hash: &[u8; 32]) -> GoogleCloudKmsResult<Vec<u8>> {
505 self.sign_and_recover_evm(*hash, hash, true).await
506 }
507}
508
509#[async_trait]
510impl GoogleCloudKmsStellarService for GoogleCloudKmsService {
511 async fn get_stellar_address(&self) -> GoogleCloudKmsResult<Address> {
512 let pem_str = self.get_pem().await?;
513 let stellar_address = derive_stellar_address_from_pem(&pem_str)
514 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
515 Ok(Address::Stellar(stellar_address))
516 }
517
518 async fn sign_payload_stellar(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
519 self.sign_stellar(payload).await
521 }
522}
523
524impl From<utils::AddressDerivationError> for GoogleCloudKmsError {
525 fn from(value: utils::AddressDerivationError) -> Self {
526 match value {
527 utils::AddressDerivationError::ParseError(msg) => GoogleCloudKmsError::ParseError(msg),
528 }
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::models::{
536 GoogleCloudKmsSignerKeyConfig, GoogleCloudKmsSignerServiceAccountConfig, SecretString,
537 };
538 use alloy::primitives::utils::eip191_message;
539 use mockito::{Mock, ServerGuard};
540 use serde_json::json;
541
542 fn create_test_config(uri: &str) -> GoogleCloudKmsSignerConfig {
543 GoogleCloudKmsSignerConfig {
544 service_account: GoogleCloudKmsSignerServiceAccountConfig {
545 project_id: "test-project".to_string(),
546 private_key_id: SecretString::new("test-private-key-id"),
547 private_key: SecretString::new("-----BEGIN EXAMPLE PRIVATE KEY-----\nFAKEKEYDATA\n-----END EXAMPLE PRIVATE KEY-----\n"),
548 client_email: SecretString::new("test-service-account@example.com"),
549 client_id: "test-client-id".to_string(),
550 auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
551 token_uri: "https://oauth2.googleapis.com/token".to_string(),
552 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test-service-account%40example.com".to_string(),
553 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(),
554 universe_domain: uri.to_string(),
555 },
556 key: GoogleCloudKmsSignerKeyConfig {
557 location: "global".to_string(),
558 key_id: "test-key-id".to_string(),
559 key_ring_id: "test-key-ring-id".to_string(),
560 key_version: 1,
561 },
562 }
563 }
564
565 #[tokio::test]
566 async fn test_service_creation_success() {
567 let config = create_test_config("https://example.com");
568 let result = GoogleCloudKmsService::new(&config);
569 assert!(result.is_ok());
570 }
571
572 #[tokio::test]
573 async fn test_get_key_path_format() {
574 let config = create_test_config("https://example.com");
575 let service = GoogleCloudKmsService::new(&config).unwrap();
576
577 let key_path = service.get_key_path();
578 let expected = "projects/test-project/locations/global/keyRings/test-key-ring-id/cryptoKeys/test-key-id/cryptoKeyVersions/1";
579
580 assert_eq!(key_path, expected);
581 }
582
583 #[tokio::test]
584 async fn test_get_base_url_with_http_prefix() {
585 let config = create_test_config("http://localhost:8080");
586 let service = GoogleCloudKmsService::new(&config).unwrap();
587
588 let base_url = service.get_base_url();
589 assert_eq!(base_url, "http://localhost:8080");
590 }
591
592 #[tokio::test]
593 async fn test_get_base_url_without_http_prefix() {
594 let config = create_test_config("googleapis.com");
595 let service = GoogleCloudKmsService::new(&config).unwrap();
596
597 let base_url = service.get_base_url();
598 assert_eq!(base_url, "https://cloudkms.googleapis.com");
599 }
600
601 async fn setup_mock_solana_public_key(mock_server: &mut ServerGuard) -> Mock {
603 mock_server
604 .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
605 .match_header("Authorization", mockito::Matcher::Any)
606 .with_status(200)
607 .with_header("content-type", "application/json")
608 .with_body(serde_json::to_string(&json!({
609 "pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAVyC+iqnSu0vo6R8x0sRMhintQtoZgcLOur1VyvCrdrs=\n-----END PUBLIC KEY-----\n",
610 "algorithm": "ECDSA_P256_SHA256"
611 })).unwrap())
612 .create_async()
613 .await
614 }
615
616 async fn setup_mock_evm_public_key(mock_server: &mut ServerGuard) -> Mock {
617 mock_server
618 .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
619 .match_header("Authorization", mockito::Matcher::Any)
620 .with_status(200)
621 .with_header("content-type", "application/json")
622 .with_body(serde_json::to_string(&json!({
623 "pem": "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEjJaJh5wfZwvj8b3bQ4GYikqDTLXWUjMh\nkFs9lGj2N9B17zo37p4PSy99rDio0QHLadpso0rtTJDSISRW9MdOqA==\n-----END PUBLIC KEY-----\n", "algorithm": "ECDSA_SECP256K1_SHA256"
625 })).unwrap())
626 .create_async()
627 .await
628 }
629
630 async fn setup_mock_sign_success(mock_server: &mut ServerGuard) -> Mock {
631 mock_server
632 .mock("POST", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign".to_string()))
633 .match_header("Authorization", mockito::Matcher::Any)
634 .with_status(200)
635 .with_header("content-type", "application/json")
636 .with_body(serde_json::to_string(&json!({
637 "signature": "ZHVtbXlzaWduYXR1cmU=" })).unwrap())
639 .create_async()
640 .await
641 }
642
643 async fn setup_mock_sign_error(mock_server: &mut ServerGuard) -> Mock {
644 mock_server
645 .mock("POST", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign".to_string()))
646 .match_header("Authorization", mockito::Matcher::Any)
647 .with_status(400)
648 .with_header("content-type", "application/json")
649 .with_body(serde_json::to_string(&json!({
650 "error": {
651 "code": 400,
652 "message": "Invalid request",
653 "status": "INVALID_ARGUMENT"
654 }
655 })).unwrap())
656 .create_async()
657 .await
658 }
659
660 async fn setup_mock_get_key_error(mock_server: &mut ServerGuard) -> Mock {
661 mock_server
662 .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
663 .match_header("Authorization", mockito::Matcher::Any)
664 .with_status(404)
665 .with_header("content-type", "application/json")
666 .with_body(serde_json::to_string(&json!({
667 "error": {
668 "code": 404,
669 "message": "Key not found",
670 "status": "NOT_FOUND"
671 }
672 })).unwrap())
673 .create_async()
674 .await
675 }
676
677 async fn setup_mock_malformed_response(mock_server: &mut ServerGuard) -> Mock {
678 mock_server
679 .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
680 .match_header("Authorization", mockito::Matcher::Any)
681 .with_status(200)
682 .with_header("content-type", "application/json")
683 .with_body(serde_json::to_string(&json!({
684 "algorithm": "ED25519"
685 })).unwrap())
687 .create_async()
688 .await
689 }
690
691 #[tokio::test]
693 async fn test_get_solana_address_success() {
694 let mut mock_server = mockito::Server::new_async().await;
695 let _mock = setup_mock_solana_public_key(&mut mock_server).await;
696
697 let config = create_test_config(&mock_server.url());
698 let service = GoogleCloudKmsService::new(&config).unwrap();
699
700 let result = service.get_solana_address().await;
701 assert!(result.is_ok());
702 assert_eq!(
703 result.unwrap(),
704 "6s7RsvzcdXFJi1tXeDoGfSKZWjCDNJLiu74rd72zLy6J"
705 );
706 }
707
708 #[tokio::test]
709 async fn test_get_solana_address_api_error() {
710 let mut mock_server = mockito::Server::new_async().await;
711 let _mock = setup_mock_get_key_error(&mut mock_server).await;
712
713 let config = create_test_config(&mock_server.url());
714 let service = GoogleCloudKmsService::new(&config).unwrap();
715
716 let result = service.get_solana_address().await;
717 assert!(result.is_err());
718 assert!(matches!(
719 result.unwrap_err(),
720 GoogleCloudKmsError::ApiError(_)
721 ));
722 }
723
724 #[tokio::test]
725 async fn test_get_evm_address_success() {
726 let mut mock_server = mockito::Server::new_async().await;
727 let _mock = setup_mock_evm_public_key(&mut mock_server).await;
728
729 let config = create_test_config(&mock_server.url());
730 let service = GoogleCloudKmsService::new(&config).unwrap();
731
732 let result = GoogleCloudKmsServiceTrait::get_evm_address(&service).await;
733 assert!(result.is_ok());
734
735 let address = result.unwrap();
736 assert!(address.starts_with("0x"));
737 assert_eq!(address.len(), 42);
738 }
739
740 #[tokio::test]
741 async fn test_sign_solana_success() {
742 let mut mock_server = mockito::Server::new_async().await;
743 let _mock = setup_mock_sign_success(&mut mock_server).await;
744
745 let config = create_test_config(&mock_server.url());
746 let service = GoogleCloudKmsService::new(&config).unwrap();
747
748 let result = service.sign_solana(b"test message").await;
749 assert!(result.is_ok());
750 assert_eq!(result.unwrap(), b"dummysignature");
751 }
752
753 #[tokio::test]
754 async fn test_sign_solana_api_error() {
755 let mut mock_server = mockito::Server::new_async().await;
756 let _mock = setup_mock_sign_error(&mut mock_server).await;
757
758 let config = create_test_config(&mock_server.url());
759 let service = GoogleCloudKmsService::new(&config).unwrap();
760
761 let result = service.sign_solana(b"test message").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_sign_evm_success() {
771 let mut mock_server = mockito::Server::new_async().await;
772 let _mock = setup_mock_sign_success(&mut mock_server).await;
773
774 let config = create_test_config(&mock_server.url());
775 let service = GoogleCloudKmsService::new(&config).unwrap();
776
777 let result = service.sign_evm(b"test message").await;
778 assert!(result.is_ok());
779 assert_eq!(result.unwrap(), b"dummysignature");
780 }
781
782 #[tokio::test]
783 async fn test_sign_evm_api_error() {
784 let mut mock_server = mockito::Server::new_async().await;
785 let _mock = setup_mock_sign_error(&mut mock_server).await;
786
787 let config = create_test_config(&mock_server.url());
788 let service = GoogleCloudKmsService::new(&config).unwrap();
789
790 let result = service.sign_evm(b"test message").await;
791 assert!(result.is_err());
792 assert!(matches!(
793 result.unwrap_err(),
794 GoogleCloudKmsError::ApiError(_)
795 ));
796 }
797
798 #[tokio::test]
800 async fn test_evm_service_get_address_success() {
801 let mut mock_server = mockito::Server::new_async().await;
802 let _mock = setup_mock_evm_public_key(&mut mock_server).await;
803
804 let config = create_test_config(&mock_server.url());
805 let service = GoogleCloudKmsService::new(&config).unwrap();
806
807 let result = GoogleCloudKmsEvmService::get_evm_address(&service).await;
808 assert!(result.is_ok());
809
810 let address = result.unwrap();
811 assert!(matches!(address, Address::Evm(_)));
812 if let Address::Evm(addr) = address {
813 assert_eq!(addr.len(), 20);
814 }
815 }
816
817 #[tokio::test]
818 async fn test_evm_service_get_address_api_error() {
819 let mut mock_server = mockito::Server::new_async().await;
820 let _mock = setup_mock_get_key_error(&mut mock_server).await;
821
822 let config = create_test_config(&mock_server.url());
823 let service = GoogleCloudKmsService::new(&config).unwrap();
824
825 let result = GoogleCloudKmsEvmService::get_evm_address(&service).await;
826 assert!(result.is_err());
827 assert!(matches!(
828 result.unwrap_err(),
829 GoogleCloudKmsError::ApiError(_)
830 ));
831 }
832
833 #[tokio::test]
834 async fn test_sign_payload_evm_network_error() {
835 let config = create_test_config("http://invalid-host:9999");
836 let service = GoogleCloudKmsService::new(&config).unwrap();
837
838 let message = eip191_message(b"Hello World!");
839 let result = GoogleCloudKmsEvmService::sign_payload_evm(&service, &message).await;
840 assert!(result.is_err());
841 assert!(matches!(
842 result.unwrap_err(),
843 GoogleCloudKmsError::HttpError(_)
844 ));
845 }
846
847 #[tokio::test]
848 async fn test_get_pem_public_key_success() {
849 let mut mock_server = mockito::Server::new_async().await;
850 let _mock = setup_mock_evm_public_key(&mut mock_server).await;
851
852 let config = create_test_config(&mock_server.url());
853 let service = GoogleCloudKmsService::new(&config).unwrap();
854
855 let result = GoogleCloudKmsK256::get_pem_public_key(&service).await;
856 assert!(result.is_ok());
857 assert!(result.unwrap().contains("BEGIN PUBLIC KEY"));
858 }
859
860 #[tokio::test]
861 async fn test_get_pem_public_key_missing_field() {
862 let mut mock_server = mockito::Server::new_async().await;
863 let _mock = setup_mock_malformed_response(&mut mock_server).await;
864
865 let config = create_test_config(&mock_server.url());
866 let service = GoogleCloudKmsService::new(&config).unwrap();
867
868 let result = GoogleCloudKmsK256::get_pem_public_key(&service).await;
869 assert!(result.is_err());
870 assert!(matches!(
871 result.unwrap_err(),
872 GoogleCloudKmsError::MissingField(_)
873 ));
874 }
875
876 #[tokio::test]
877 async fn test_sign_digest_success() {
878 let mut mock_server = mockito::Server::new_async().await;
879 let _mock = setup_mock_sign_success(&mut mock_server).await;
880
881 let config = create_test_config(&mock_server.url());
882 let service = GoogleCloudKmsService::new(&config).unwrap();
883
884 let digest = [0u8; 32];
885 let result = GoogleCloudKmsK256::sign_digest(&service, digest).await;
886 assert!(result.is_ok());
887 assert_eq!(result.unwrap(), b"dummysignature");
888 }
889
890 #[tokio::test]
891 async fn test_sign_digest_api_error() {
892 let mut mock_server = mockito::Server::new_async().await;
893 let _mock = setup_mock_sign_error(&mut mock_server).await;
894
895 let config = create_test_config(&mock_server.url());
896 let service = GoogleCloudKmsService::new(&config).unwrap();
897
898 let digest = [0u8; 32];
899 let result = GoogleCloudKmsK256::sign_digest(&service, digest).await;
900 assert!(result.is_err());
901 assert!(matches!(
902 result.unwrap_err(),
903 GoogleCloudKmsError::ApiError(_)
904 ));
905 }
906
907 #[tokio::test]
908 async fn test_network_failure_handling() {
909 let config = create_test_config("http://localhost:99999"); let service = GoogleCloudKmsService::new(&config).unwrap();
911
912 let solana_addr_result = service.get_solana_address().await;
914 assert!(solana_addr_result.is_err());
915 assert!(matches!(
916 solana_addr_result.unwrap_err(),
917 GoogleCloudKmsError::HttpError(_)
918 ));
919
920 let evm_addr_result = GoogleCloudKmsServiceTrait::get_evm_address(&service).await;
921 assert!(evm_addr_result.is_err());
922 assert!(matches!(
923 evm_addr_result.unwrap_err(),
924 GoogleCloudKmsError::HttpError(_)
925 ));
926
927 let sign_solana_result = service.sign_solana(b"test").await;
928 assert!(sign_solana_result.is_err());
929 assert!(matches!(
930 sign_solana_result.unwrap_err(),
931 GoogleCloudKmsError::HttpError(_)
932 ));
933
934 let sign_evm_result = service.sign_evm(b"test").await;
935 assert!(sign_evm_result.is_err());
936 assert!(matches!(
937 sign_evm_result.unwrap_err(),
938 GoogleCloudKmsError::HttpError(_)
939 ));
940 }
941
942 #[tokio::test]
943 async fn test_config_with_different_universe_domains() {
944 let config1 = create_test_config("googleapis.com");
945 let service1 = GoogleCloudKmsService::new(&config1).unwrap();
946 assert_eq!(service1.get_base_url(), "https://cloudkms.googleapis.com");
947
948 let config2 = create_test_config("https://custom-domain.com");
949 let service2 = GoogleCloudKmsService::new(&config2).unwrap();
950 assert_eq!(service2.get_base_url(), "https://custom-domain.com");
951 }
952
953 #[tokio::test]
954 async fn test_solana_address_derivation() {
955 let valid_ed25519_pem = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAnUV+ReQWxMZ3Z2pC/5aOPPjcc8jzOo0ZgSl7+j4AMLo=\n-----END PUBLIC KEY-----\n";
956 let result = utils::derive_solana_address_from_pem(valid_ed25519_pem);
957 assert!(result.is_ok());
958 assert_eq!(
959 result.unwrap(),
960 "BavUBpkD77FABnevMkBVqV8BDHv7gX8sSoYYJY9WU9L5"
961 );
962 }
963
964 #[tokio::test]
965 async fn test_malformed_json_response() {
966 let mut mock_server = mockito::Server::new_async().await;
967
968 let _mock = mock_server
969 .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
970 .match_header("Authorization", mockito::Matcher::Any)
971 .with_status(200)
972 .with_header("content-type", "application/json")
973 .with_body("invalid json")
974 .create_async()
975 .await;
976
977 let config = create_test_config(&mock_server.url());
978 let service = GoogleCloudKmsService::new(&config).unwrap();
979
980 let result = service.get_solana_address().await;
981 assert!(result.is_err());
982 assert!(matches!(
983 result.unwrap_err(),
984 GoogleCloudKmsError::ParseError(_)
985 ));
986 }
987
988 #[tokio::test]
989 async fn test_missing_signature_field_in_response() {
990 let mut mock_server = mockito::Server::new_async().await;
991
992 let _mock = mock_server
993 .mock("POST", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign".to_string()))
994 .match_header("Authorization", mockito::Matcher::Any)
995 .with_status(200)
996 .with_header("content-type", "application/json")
997 .with_body(serde_json::to_string(&json!({
998 "name": "test-key"
999 })).unwrap())
1001 .create_async()
1002 .await;
1003
1004 let config = create_test_config(&mock_server.url());
1005 let service = GoogleCloudKmsService::new(&config).unwrap();
1006
1007 let result = service.sign_solana(b"test").await;
1008 assert!(result.is_err());
1009 assert!(matches!(
1010 result.unwrap_err(),
1011 GoogleCloudKmsError::MissingField(_)
1012 ));
1013 }
1014}