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::utils::{
38 self, base64_decode, base64_encode, derive_ethereum_address_from_pem,
39 derive_stellar_address_from_pem, 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 async fn get_stellar_address(&self) -> GoogleCloudKmsResult<String>;
72 async fn sign_stellar(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
73}
74
75#[async_trait]
76#[cfg_attr(test, automock)]
77pub trait GoogleCloudKmsEvmService: Send + Sync {
78 async fn get_evm_address(&self) -> GoogleCloudKmsResult<Address>;
80 async fn sign_payload_evm(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
83}
84
85#[async_trait]
86#[cfg_attr(test, automock)]
87pub trait GoogleCloudKmsStellarService: Send + Sync {
88 async fn get_stellar_address(&self) -> GoogleCloudKmsResult<Address>;
90 async fn sign_payload_stellar(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
93}
94
95#[async_trait]
96#[cfg_attr(test, automock)]
97pub trait GoogleCloudKmsK256: Send + Sync {
98 async fn get_pem_public_key(&self) -> GoogleCloudKmsResult<String>;
100 async fn sign_digest(&self, digest: [u8; 32]) -> GoogleCloudKmsResult<Vec<u8>>;
102}
103
104#[derive(Clone)]
105#[allow(dead_code)]
106pub struct GoogleCloudKmsService {
107 pub config: GoogleCloudKmsSignerConfig,
108 credentials: Arc<Credentials>,
109 client: Client,
110 cached_headers: Arc<RwLock<Option<HeaderMap>>>,
111}
112
113impl GoogleCloudKmsService {
114 pub fn new(config: &GoogleCloudKmsSignerConfig) -> GoogleCloudKmsResult<Self> {
115 let credentials_json = serde_json::json!({
116 "type": "service_account",
117 "project_id": config.service_account.project_id,
118 "private_key_id": config.service_account.private_key_id.to_str().to_string(),
119 "private_key": config.service_account.private_key.to_str().to_string(),
120 "client_email": config.service_account.client_email.to_str().to_string(),
121 "client_id": config.service_account.client_id,
122 "auth_uri": config.service_account.auth_uri,
123 "token_uri": config.service_account.token_uri,
124 "auth_provider_x509_cert_url": config.service_account.auth_provider_x509_cert_url,
125 "client_x509_cert_url": config.service_account.client_x509_cert_url,
126 "universe_domain": config.service_account.universe_domain,
127 });
128 let credentials = GcpCredBuilder::new(credentials_json)
129 .build()
130 .map_err(|e| GoogleCloudKmsError::ConfigError(e.to_string()))?;
131
132 Ok(Self {
133 config: config.clone(),
134 credentials: Arc::new(credentials),
135 client: Client::new(),
136 cached_headers: Arc::new(RwLock::new(None)),
137 })
138 }
139
140 async fn get_auth_headers(&self) -> GoogleCloudKmsResult<HeaderMap> {
141 #[cfg(test)]
142 {
143 let mut headers = HeaderMap::new();
145 headers.insert("Authorization", "Bearer test-token".parse().unwrap());
146 Ok(headers)
147 }
148
149 #[cfg(not(test))]
150 {
151 let cacheable_headers = self
152 .credentials
153 .headers(Extensions::new())
154 .await
155 .map_err(|e| GoogleCloudKmsError::ConfigError(e.to_string()))?;
156
157 match cacheable_headers {
158 google_cloud_auth::credentials::CacheableResource::New { data, .. } => {
159 let mut cached = self.cached_headers.write().await;
160 *cached = Some(data.clone());
161 Ok(data)
162 }
163 google_cloud_auth::credentials::CacheableResource::NotModified => {
164 let cached = self.cached_headers.read().await;
165 if let Some(headers) = cached.as_ref() {
166 Ok(headers.clone())
167 } else {
168 Err(GoogleCloudKmsError::ConfigError(
169 "KMS auth token not modified, but not found in cache".to_string(),
170 ))
171 }
172 }
173 }
174 }
175 }
176
177 fn get_base_url(&self) -> String {
178 if self
179 .config
180 .service_account
181 .universe_domain
182 .starts_with("http")
183 {
184 self.config.service_account.universe_domain.clone()
185 } else {
186 format!(
187 "https://cloudkms.{}",
188 self.config.service_account.universe_domain
189 )
190 }
191 }
192
193 async fn kms_get(&self, url: &str) -> GoogleCloudKmsResult<Value> {
194 let headers = self.get_auth_headers().await?;
195 let resp = self
196 .client
197 .get(url)
198 .headers(headers)
199 .send()
200 .await
201 .map_err(|e| GoogleCloudKmsError::HttpError(e.to_string()))?;
202
203 let status = resp.status();
204 let text = resp.text().await.unwrap_or_else(|_| "".to_string());
205
206 if !status.is_success() {
207 return Err(GoogleCloudKmsError::ApiError(format!(
208 "KMS request failed ({}): {}",
209 status, text
210 )));
211 }
212
213 serde_json::from_str(&text)
214 .map_err(|e| GoogleCloudKmsError::ParseError(format!("{}: {}", e, text)))
215 }
216
217 async fn kms_post(&self, url: &str, body: &Value) -> GoogleCloudKmsResult<Value> {
218 let headers = self.get_auth_headers().await?;
219 let resp = self
220 .client
221 .post(url)
222 .headers(headers)
223 .json(body)
224 .send()
225 .await
226 .map_err(|e| GoogleCloudKmsError::HttpError(e.to_string()))?;
227
228 let status = resp.status();
229 let text = resp.text().await.unwrap_or_else(|_| "".to_string());
230
231 if !status.is_success() {
232 return Err(GoogleCloudKmsError::ApiError(format!(
233 "KMS request failed ({}): {}",
234 status, text
235 )));
236 }
237
238 serde_json::from_str(&text)
239 .map_err(|e| GoogleCloudKmsError::ParseError(format!("{}: {}", e, text)))
240 }
241
242 fn get_key_path(&self) -> String {
243 format!(
244 "projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/{}",
245 self.config.service_account.project_id,
246 self.config.key.location,
247 self.config.key.key_ring_id,
248 self.config.key.key_id,
249 self.config.key.key_version
250 )
251 }
252
253 async fn get_pem(&self) -> GoogleCloudKmsResult<String> {
255 let base_url = self.get_base_url();
256 let key_path = self.get_key_path();
257 let url = format!("{}/v1/{}/publicKey", base_url, key_path,);
258 debug!(url = %url, "kms public key url");
259
260 let body = self.kms_get(&url).await?;
261 let pem_str = body
262 .get("pem")
263 .and_then(|v| v.as_str())
264 .ok_or_else(|| GoogleCloudKmsError::MissingField("pem".to_string()))?;
265
266 Ok(pem_str.to_string())
267 }
268
269 pub async fn sign_bytes_evm(&self, bytes: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
273 let digest = keccak256(bytes).0;
274 let der_signature = self.sign_digest(digest).await?;
275
276 let rs = k256::ecdsa::Signature::from_der(&der_signature)
278 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
279
280 let pem_str = self.get_pem().await?;
281
282 let pem_parsed =
284 pem::parse(&pem_str).map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
285 let der_pk = pem_parsed.contents();
286
287 let pk = extract_public_key_from_der(der_pk)
288 .map_err(|e| GoogleCloudKmsError::ConvertError(e.to_string()))?;
289
290 let v = utils::recover_public_key(&pk, &rs, bytes)?;
291
292 let eth_v = 27 + v;
294
295 let mut sig_bytes = rs.to_vec();
296 sig_bytes.push(eth_v);
297
298 Ok(sig_bytes)
299 }
300}
301
302#[async_trait]
303impl GoogleCloudKmsK256 for GoogleCloudKmsService {
304 async fn get_pem_public_key(&self) -> GoogleCloudKmsResult<String> {
305 self.get_pem().await
306 }
307
308 async fn sign_digest(&self, digest: [u8; 32]) -> GoogleCloudKmsResult<Vec<u8>> {
309 let base_url = self.get_base_url();
310 let key_path = self.get_key_path();
311 let url = format!("{}/v1/{}:asymmetricSign", base_url, key_path);
312
313 let digest_b64 = base64_encode(&digest);
314
315 let body = serde_json::json!({
316 "name": key_path,
317 "digest": {
318 "sha256": digest_b64
319 }
320 });
321
322 let resp = self.kms_post(&url, &body).await?;
323 let signature_b64 = resp
324 .get("signature")
325 .and_then(|v| v.as_str())
326 .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
327
328 let signature = base64_decode(signature_b64)
329 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
330
331 Ok(signature)
332 }
333}
334
335#[async_trait]
336impl GoogleCloudKmsServiceTrait for GoogleCloudKmsService {
337 async fn get_solana_address(&self) -> GoogleCloudKmsResult<String> {
338 let pem_str = self.get_pem().await?;
339
340 debug!(pem_str = %pem_str, "pem solana");
341
342 utils::derive_solana_address_from_pem(&pem_str).map_err(GoogleCloudKmsError::from)
343 }
344
345 async fn get_evm_address(&self) -> GoogleCloudKmsResult<String> {
346 let pem_str = self.get_pem().await?;
347
348 debug!(pem_str = %pem_str, "pem evm");
349
350 let address_bytes =
351 utils::derive_ethereum_address_from_pem(&pem_str).map_err(GoogleCloudKmsError::from)?;
352 Ok(format!("0x{}", hex::encode(address_bytes)))
353 }
354
355 async fn sign_solana(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
356 let base_url = self.get_base_url();
357 let key_path = self.get_key_path();
358
359 let url = format!("{}/v1/{}:asymmetricSign", base_url, key_path,);
360
361 let body = serde_json::json!({
362 "name": key_path,
363 "data": base64_encode(message)
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 async fn sign_evm(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
379 let base_url = self.get_base_url();
380 let key_path = self.get_key_path();
381 let url = format!("{}/v1/{}:asymmetricSign", base_url, key_path,);
382
383 let hash = Sha256::digest(message);
384 let digest = base64_encode(&hash);
385
386 let body = serde_json::json!({
387 "name": key_path,
388 "digest": {
389 "sha256": digest
390 }
391 });
392
393 debug!(body = ?body, "kms asymmetric sign body");
394
395 let resp = self.kms_post(&url, &body).await?;
396 let signature = resp
397 .get("signature")
398 .and_then(|v| v.as_str())
399 .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
400
401 debug!(resp = ?resp, "kms asymmetric sign response");
402 let signature_b64 =
403 base64_decode(signature).map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
404 debug!(signature_b64 = ?signature_b64, "signature b64 decoded");
405 Ok(signature_b64)
406 }
407
408 async fn get_stellar_address(&self) -> GoogleCloudKmsResult<String> {
409 let pem_str = self.get_pem().await?;
410
411 debug!(pem_str = %pem_str, "pem stellar");
412
413 utils::derive_stellar_address_from_pem(&pem_str).map_err(GoogleCloudKmsError::from)
414 }
415
416 async fn sign_stellar(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
417 let base_url = self.get_base_url();
418 let key_path = self.get_key_path();
419
420 let url = format!("{}/v1/{}:asymmetricSign", base_url, key_path);
421 debug!(url = %url, "kms asymmetric sign url for stellar");
422
423 let body = serde_json::json!({
425 "name": key_path,
426 "data": base64_encode(message)
427 });
428
429 debug!(body = ?body, "kms asymmetric sign body for stellar");
430
431 let resp = self.kms_post(&url, &body).await?;
432 let signature_b64 = resp
433 .get("signature")
434 .and_then(|v| v.as_str())
435 .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
436
437 debug!(resp = ?resp, "kms asymmetric sign response for stellar");
438
439 let signature = base64_decode(signature_b64)
440 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
441
442 Ok(signature)
443 }
444}
445
446#[async_trait]
447impl GoogleCloudKmsEvmService for GoogleCloudKmsService {
448 async fn get_evm_address(&self) -> GoogleCloudKmsResult<Address> {
449 let pem_str = self.get_pem().await?;
450 let eth_address = derive_ethereum_address_from_pem(&pem_str)
451 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
452 Ok(Address::Evm(eth_address))
453 }
454
455 async fn sign_payload_evm(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
456 self.sign_bytes_evm(payload).await
457 }
458}
459
460#[async_trait]
461impl GoogleCloudKmsStellarService for GoogleCloudKmsService {
462 async fn get_stellar_address(&self) -> GoogleCloudKmsResult<Address> {
463 let pem_str = self.get_pem().await?;
464 let stellar_address = derive_stellar_address_from_pem(&pem_str)
465 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
466 Ok(Address::Stellar(stellar_address))
467 }
468
469 async fn sign_payload_stellar(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
470 self.sign_stellar(payload).await
472 }
473}
474
475impl From<utils::AddressDerivationError> for GoogleCloudKmsError {
476 fn from(value: utils::AddressDerivationError) -> Self {
477 match value {
478 utils::AddressDerivationError::ParseError(msg) => GoogleCloudKmsError::ParseError(msg),
479 }
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486 use crate::models::{
487 GoogleCloudKmsSignerKeyConfig, GoogleCloudKmsSignerServiceAccountConfig, SecretString,
488 };
489 use alloy::primitives::utils::eip191_message;
490 use mockito::{Mock, ServerGuard};
491 use serde_json::json;
492
493 fn create_test_config(uri: &str) -> GoogleCloudKmsSignerConfig {
494 GoogleCloudKmsSignerConfig {
495 service_account: GoogleCloudKmsSignerServiceAccountConfig {
496 project_id: "test-project".to_string(),
497 private_key_id: SecretString::new("test-private-key-id"),
498 private_key: SecretString::new("-----BEGIN EXAMPLE PRIVATE KEY-----\nFAKEKEYDATA\n-----END EXAMPLE PRIVATE KEY-----\n"),
499 client_email: SecretString::new("test-service-account@example.com"),
500 client_id: "test-client-id".to_string(),
501 auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
502 token_uri: "https://oauth2.googleapis.com/token".to_string(),
503 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test-service-account%40example.com".to_string(),
504 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(),
505 universe_domain: uri.to_string(),
506 },
507 key: GoogleCloudKmsSignerKeyConfig {
508 location: "global".to_string(),
509 key_id: "test-key-id".to_string(),
510 key_ring_id: "test-key-ring-id".to_string(),
511 key_version: 1,
512 },
513 }
514 }
515
516 #[tokio::test]
517 async fn test_service_creation_success() {
518 let config = create_test_config("https://example.com");
519 let result = GoogleCloudKmsService::new(&config);
520 assert!(result.is_ok());
521 }
522
523 #[tokio::test]
524 async fn test_get_key_path_format() {
525 let config = create_test_config("https://example.com");
526 let service = GoogleCloudKmsService::new(&config).unwrap();
527
528 let key_path = service.get_key_path();
529 let expected = "projects/test-project/locations/global/keyRings/test-key-ring-id/cryptoKeys/test-key-id/cryptoKeyVersions/1";
530
531 assert_eq!(key_path, expected);
532 }
533
534 #[tokio::test]
535 async fn test_get_base_url_with_http_prefix() {
536 let config = create_test_config("http://localhost:8080");
537 let service = GoogleCloudKmsService::new(&config).unwrap();
538
539 let base_url = service.get_base_url();
540 assert_eq!(base_url, "http://localhost:8080");
541 }
542
543 #[tokio::test]
544 async fn test_get_base_url_without_http_prefix() {
545 let config = create_test_config("googleapis.com");
546 let service = GoogleCloudKmsService::new(&config).unwrap();
547
548 let base_url = service.get_base_url();
549 assert_eq!(base_url, "https://cloudkms.googleapis.com");
550 }
551
552 async fn setup_mock_solana_public_key(mock_server: &mut ServerGuard) -> Mock {
554 mock_server
555 .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
556 .match_header("Authorization", mockito::Matcher::Any)
557 .with_status(200)
558 .with_header("content-type", "application/json")
559 .with_body(serde_json::to_string(&json!({
560 "pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAVyC+iqnSu0vo6R8x0sRMhintQtoZgcLOur1VyvCrdrs=\n-----END PUBLIC KEY-----\n",
561 "algorithm": "ECDSA_P256_SHA256"
562 })).unwrap())
563 .create_async()
564 .await
565 }
566
567 async fn setup_mock_evm_public_key(mock_server: &mut ServerGuard) -> Mock {
568 mock_server
569 .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
570 .match_header("Authorization", mockito::Matcher::Any)
571 .with_status(200)
572 .with_header("content-type", "application/json")
573 .with_body(serde_json::to_string(&json!({
574 "pem": "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEjJaJh5wfZwvj8b3bQ4GYikqDTLXWUjMh\nkFs9lGj2N9B17zo37p4PSy99rDio0QHLadpso0rtTJDSISRW9MdOqA==\n-----END PUBLIC KEY-----\n", "algorithm": "ECDSA_SECP256K1_SHA256"
576 })).unwrap())
577 .create_async()
578 .await
579 }
580
581 async fn setup_mock_sign_success(mock_server: &mut ServerGuard) -> Mock {
582 mock_server
583 .mock("POST", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign".to_string()))
584 .match_header("Authorization", mockito::Matcher::Any)
585 .with_status(200)
586 .with_header("content-type", "application/json")
587 .with_body(serde_json::to_string(&json!({
588 "signature": "ZHVtbXlzaWduYXR1cmU=" })).unwrap())
590 .create_async()
591 .await
592 }
593
594 async fn setup_mock_sign_error(mock_server: &mut ServerGuard) -> Mock {
595 mock_server
596 .mock("POST", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign".to_string()))
597 .match_header("Authorization", mockito::Matcher::Any)
598 .with_status(400)
599 .with_header("content-type", "application/json")
600 .with_body(serde_json::to_string(&json!({
601 "error": {
602 "code": 400,
603 "message": "Invalid request",
604 "status": "INVALID_ARGUMENT"
605 }
606 })).unwrap())
607 .create_async()
608 .await
609 }
610
611 async fn setup_mock_get_key_error(mock_server: &mut ServerGuard) -> Mock {
612 mock_server
613 .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
614 .match_header("Authorization", mockito::Matcher::Any)
615 .with_status(404)
616 .with_header("content-type", "application/json")
617 .with_body(serde_json::to_string(&json!({
618 "error": {
619 "code": 404,
620 "message": "Key not found",
621 "status": "NOT_FOUND"
622 }
623 })).unwrap())
624 .create_async()
625 .await
626 }
627
628 async fn setup_mock_malformed_response(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 "algorithm": "ED25519"
636 })).unwrap())
638 .create_async()
639 .await
640 }
641
642 #[tokio::test]
644 async fn test_get_solana_address_success() {
645 let mut mock_server = mockito::Server::new_async().await;
646 let _mock = setup_mock_solana_public_key(&mut mock_server).await;
647
648 let config = create_test_config(&mock_server.url());
649 let service = GoogleCloudKmsService::new(&config).unwrap();
650
651 let result = service.get_solana_address().await;
652 assert!(result.is_ok());
653 assert_eq!(
654 result.unwrap(),
655 "6s7RsvzcdXFJi1tXeDoGfSKZWjCDNJLiu74rd72zLy6J"
656 );
657 }
658
659 #[tokio::test]
660 async fn test_get_solana_address_api_error() {
661 let mut mock_server = mockito::Server::new_async().await;
662 let _mock = setup_mock_get_key_error(&mut mock_server).await;
663
664 let config = create_test_config(&mock_server.url());
665 let service = GoogleCloudKmsService::new(&config).unwrap();
666
667 let result = service.get_solana_address().await;
668 assert!(result.is_err());
669 assert!(matches!(
670 result.unwrap_err(),
671 GoogleCloudKmsError::ApiError(_)
672 ));
673 }
674
675 #[tokio::test]
676 async fn test_get_evm_address_success() {
677 let mut mock_server = mockito::Server::new_async().await;
678 let _mock = setup_mock_evm_public_key(&mut mock_server).await;
679
680 let config = create_test_config(&mock_server.url());
681 let service = GoogleCloudKmsService::new(&config).unwrap();
682
683 let result = GoogleCloudKmsServiceTrait::get_evm_address(&service).await;
684 assert!(result.is_ok());
685
686 let address = result.unwrap();
687 assert!(address.starts_with("0x"));
688 assert_eq!(address.len(), 42);
689 }
690
691 #[tokio::test]
692 async fn test_sign_solana_success() {
693 let mut mock_server = mockito::Server::new_async().await;
694 let _mock = setup_mock_sign_success(&mut mock_server).await;
695
696 let config = create_test_config(&mock_server.url());
697 let service = GoogleCloudKmsService::new(&config).unwrap();
698
699 let result = service.sign_solana(b"test message").await;
700 assert!(result.is_ok());
701 assert_eq!(result.unwrap(), b"dummysignature");
702 }
703
704 #[tokio::test]
705 async fn test_sign_solana_api_error() {
706 let mut mock_server = mockito::Server::new_async().await;
707 let _mock = setup_mock_sign_error(&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.sign_solana(b"test message").await;
713 assert!(result.is_err());
714 assert!(matches!(
715 result.unwrap_err(),
716 GoogleCloudKmsError::ApiError(_)
717 ));
718 }
719
720 #[tokio::test]
721 async fn test_sign_evm_success() {
722 let mut mock_server = mockito::Server::new_async().await;
723 let _mock = setup_mock_sign_success(&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.sign_evm(b"test message").await;
729 assert!(result.is_ok());
730 assert_eq!(result.unwrap(), b"dummysignature");
731 }
732
733 #[tokio::test]
734 async fn test_sign_evm_api_error() {
735 let mut mock_server = mockito::Server::new_async().await;
736 let _mock = setup_mock_sign_error(&mut mock_server).await;
737
738 let config = create_test_config(&mock_server.url());
739 let service = GoogleCloudKmsService::new(&config).unwrap();
740
741 let result = service.sign_evm(b"test message").await;
742 assert!(result.is_err());
743 assert!(matches!(
744 result.unwrap_err(),
745 GoogleCloudKmsError::ApiError(_)
746 ));
747 }
748
749 #[tokio::test]
751 async fn test_evm_service_get_address_success() {
752 let mut mock_server = mockito::Server::new_async().await;
753 let _mock = setup_mock_evm_public_key(&mut mock_server).await;
754
755 let config = create_test_config(&mock_server.url());
756 let service = GoogleCloudKmsService::new(&config).unwrap();
757
758 let result = GoogleCloudKmsEvmService::get_evm_address(&service).await;
759 assert!(result.is_ok());
760
761 let address = result.unwrap();
762 assert!(matches!(address, Address::Evm(_)));
763 if let Address::Evm(addr) = address {
764 assert_eq!(addr.len(), 20);
765 }
766 }
767
768 #[tokio::test]
769 async fn test_evm_service_get_address_api_error() {
770 let mut mock_server = mockito::Server::new_async().await;
771 let _mock = setup_mock_get_key_error(&mut mock_server).await;
772
773 let config = create_test_config(&mock_server.url());
774 let service = GoogleCloudKmsService::new(&config).unwrap();
775
776 let result = GoogleCloudKmsEvmService::get_evm_address(&service).await;
777 assert!(result.is_err());
778 assert!(matches!(
779 result.unwrap_err(),
780 GoogleCloudKmsError::ApiError(_)
781 ));
782 }
783
784 #[tokio::test]
785 async fn test_sign_payload_evm_network_error() {
786 let config = create_test_config("http://invalid-host:9999");
787 let service = GoogleCloudKmsService::new(&config).unwrap();
788
789 let message = eip191_message(b"Hello World!");
790 let result = GoogleCloudKmsEvmService::sign_payload_evm(&service, &message).await;
791 assert!(result.is_err());
792 assert!(matches!(
793 result.unwrap_err(),
794 GoogleCloudKmsError::HttpError(_)
795 ));
796 }
797
798 #[tokio::test]
799 async fn test_get_pem_public_key_success() {
800 let mut mock_server = mockito::Server::new_async().await;
801 let _mock = setup_mock_evm_public_key(&mut mock_server).await;
802
803 let config = create_test_config(&mock_server.url());
804 let service = GoogleCloudKmsService::new(&config).unwrap();
805
806 let result = GoogleCloudKmsK256::get_pem_public_key(&service).await;
807 assert!(result.is_ok());
808 assert!(result.unwrap().contains("BEGIN PUBLIC KEY"));
809 }
810
811 #[tokio::test]
812 async fn test_get_pem_public_key_missing_field() {
813 let mut mock_server = mockito::Server::new_async().await;
814 let _mock = setup_mock_malformed_response(&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 = GoogleCloudKmsK256::get_pem_public_key(&service).await;
820 assert!(result.is_err());
821 assert!(matches!(
822 result.unwrap_err(),
823 GoogleCloudKmsError::MissingField(_)
824 ));
825 }
826
827 #[tokio::test]
828 async fn test_sign_digest_success() {
829 let mut mock_server = mockito::Server::new_async().await;
830 let _mock = setup_mock_sign_success(&mut mock_server).await;
831
832 let config = create_test_config(&mock_server.url());
833 let service = GoogleCloudKmsService::new(&config).unwrap();
834
835 let digest = [0u8; 32];
836 let result = GoogleCloudKmsK256::sign_digest(&service, digest).await;
837 assert!(result.is_ok());
838 assert_eq!(result.unwrap(), b"dummysignature");
839 }
840
841 #[tokio::test]
842 async fn test_sign_digest_api_error() {
843 let mut mock_server = mockito::Server::new_async().await;
844 let _mock = setup_mock_sign_error(&mut mock_server).await;
845
846 let config = create_test_config(&mock_server.url());
847 let service = GoogleCloudKmsService::new(&config).unwrap();
848
849 let digest = [0u8; 32];
850 let result = GoogleCloudKmsK256::sign_digest(&service, digest).await;
851 assert!(result.is_err());
852 assert!(matches!(
853 result.unwrap_err(),
854 GoogleCloudKmsError::ApiError(_)
855 ));
856 }
857
858 #[tokio::test]
859 async fn test_network_failure_handling() {
860 let config = create_test_config("http://localhost:99999"); let service = GoogleCloudKmsService::new(&config).unwrap();
862
863 let solana_addr_result = service.get_solana_address().await;
865 assert!(solana_addr_result.is_err());
866 assert!(matches!(
867 solana_addr_result.unwrap_err(),
868 GoogleCloudKmsError::HttpError(_)
869 ));
870
871 let evm_addr_result = GoogleCloudKmsServiceTrait::get_evm_address(&service).await;
872 assert!(evm_addr_result.is_err());
873 assert!(matches!(
874 evm_addr_result.unwrap_err(),
875 GoogleCloudKmsError::HttpError(_)
876 ));
877
878 let sign_solana_result = service.sign_solana(b"test").await;
879 assert!(sign_solana_result.is_err());
880 assert!(matches!(
881 sign_solana_result.unwrap_err(),
882 GoogleCloudKmsError::HttpError(_)
883 ));
884
885 let sign_evm_result = service.sign_evm(b"test").await;
886 assert!(sign_evm_result.is_err());
887 assert!(matches!(
888 sign_evm_result.unwrap_err(),
889 GoogleCloudKmsError::HttpError(_)
890 ));
891 }
892
893 #[tokio::test]
894 async fn test_config_with_different_universe_domains() {
895 let config1 = create_test_config("googleapis.com");
896 let service1 = GoogleCloudKmsService::new(&config1).unwrap();
897 assert_eq!(service1.get_base_url(), "https://cloudkms.googleapis.com");
898
899 let config2 = create_test_config("https://custom-domain.com");
900 let service2 = GoogleCloudKmsService::new(&config2).unwrap();
901 assert_eq!(service2.get_base_url(), "https://custom-domain.com");
902 }
903
904 #[tokio::test]
905 async fn test_solana_address_derivation() {
906 let valid_ed25519_pem = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAnUV+ReQWxMZ3Z2pC/5aOPPjcc8jzOo0ZgSl7+j4AMLo=\n-----END PUBLIC KEY-----\n";
907 let result = utils::derive_solana_address_from_pem(valid_ed25519_pem);
908 assert!(result.is_ok());
909 assert_eq!(
910 result.unwrap(),
911 "BavUBpkD77FABnevMkBVqV8BDHv7gX8sSoYYJY9WU9L5"
912 );
913 }
914
915 #[tokio::test]
916 async fn test_malformed_json_response() {
917 let mut mock_server = mockito::Server::new_async().await;
918
919 let _mock = mock_server
920 .mock("GET", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey".to_string()))
921 .match_header("Authorization", mockito::Matcher::Any)
922 .with_status(200)
923 .with_header("content-type", "application/json")
924 .with_body("invalid json")
925 .create_async()
926 .await;
927
928 let config = create_test_config(&mock_server.url());
929 let service = GoogleCloudKmsService::new(&config).unwrap();
930
931 let result = service.get_solana_address().await;
932 assert!(result.is_err());
933 assert!(matches!(
934 result.unwrap_err(),
935 GoogleCloudKmsError::ParseError(_)
936 ));
937 }
938
939 #[tokio::test]
940 async fn test_missing_signature_field_in_response() {
941 let mut mock_server = mockito::Server::new_async().await;
942
943 let _mock = mock_server
944 .mock("POST", mockito::Matcher::Regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign".to_string()))
945 .match_header("Authorization", mockito::Matcher::Any)
946 .with_status(200)
947 .with_header("content-type", "application/json")
948 .with_body(serde_json::to_string(&json!({
949 "name": "test-key"
950 })).unwrap())
952 .create_async()
953 .await;
954
955 let config = create_test_config(&mock_server.url());
956 let service = GoogleCloudKmsService::new(&config).unwrap();
957
958 let result = service.sign_solana(b"test").await;
959 assert!(result.is_err());
960 assert!(matches!(
961 result.unwrap_err(),
962 GoogleCloudKmsError::MissingField(_)
963 ));
964 }
965}