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.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 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 (*universe_domain).clone()
202 } else if universe_domain.starts_with("http://") {
203 #[cfg(not(test))]
205 {
206 format!("https://{}", universe_domain.trim_start_matches("http://"))
207 }
208 #[cfg(test)]
211 {
212 (*universe_domain).clone()
213 }
214 } else {
215 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 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 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 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 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 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 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 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 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 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 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 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", "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=" })).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 })).unwrap())
699 .create_async()
700 .await
701 }
702
703 #[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 #[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"); let service = GoogleCloudKmsService::new(&config).unwrap();
923
924 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 })).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}