openzeppelin_relayer/services/google_cloud_kms/
mod.rs1use alloy::primitives::keccak256;
22use async_trait::async_trait;
23use google_cloud_auth::credentials::{service_account::Builder as GcpCredBuilder, Credentials};
24#[cfg_attr(test, allow(unused_imports))]
25use http::{Extensions, HeaderMap};
26use log::debug;
27use reqwest::Client;
28use serde_json::Value;
29use sha2::{Digest, Sha256};
30use std::sync::Arc;
31
32#[cfg(test)]
33use mockall::automock;
34
35use crate::models::{Address, GoogleCloudKmsSignerConfig};
36use crate::utils::{
37 self, base64_decode, base64_encode, derive_ethereum_address_from_pem,
38 extract_public_key_from_der,
39};
40
41#[derive(Debug, thiserror::Error, serde::Serialize)]
42pub enum GoogleCloudKmsError {
43 #[error("KMS HTTP error: {0}")]
44 HttpError(String),
45 #[error("KMS API error: {0}")]
46 ApiError(String),
47 #[error("KMS response parse error: {0}")]
48 ParseError(String),
49 #[error("KMS missing field: {0}")]
50 MissingField(String),
51 #[error("KMS config error: {0}")]
52 ConfigError(String),
53 #[error("KMS conversion error: {0}")]
54 ConvertError(String),
55 #[error("KMS public key error: {0}")]
56 RecoveryError(#[from] utils::Secp256k1Error),
57 #[error("Other error: {0}")]
58 Other(String),
59}
60
61pub type GoogleCloudKmsResult<T> = Result<T, GoogleCloudKmsError>;
62
63#[async_trait]
64#[cfg_attr(test, automock)]
65pub trait GoogleCloudKmsServiceTrait: Send + Sync {
66 async fn get_solana_address(&self) -> GoogleCloudKmsResult<String>;
67 async fn sign_solana(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
68 async fn get_evm_address(&self) -> GoogleCloudKmsResult<String>;
69 async fn sign_evm(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
70}
71
72#[async_trait]
73#[cfg_attr(test, automock)]
74pub trait GoogleCloudKmsEvmService: Send + Sync {
75 async fn get_evm_address(&self) -> GoogleCloudKmsResult<Address>;
77 async fn sign_payload_evm(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>>;
80}
81
82#[async_trait]
83#[cfg_attr(test, automock)]
84pub trait GoogleCloudKmsK256: Send + Sync {
85 async fn get_pem_public_key(&self) -> GoogleCloudKmsResult<String>;
87 async fn sign_digest(&self, digest: [u8; 32]) -> GoogleCloudKmsResult<Vec<u8>>;
89}
90
91#[derive(Clone)]
92#[allow(dead_code)]
93pub struct GoogleCloudKmsService {
94 pub config: GoogleCloudKmsSignerConfig,
95 credentials: Arc<Credentials>,
96 client: Client,
97}
98
99impl GoogleCloudKmsService {
100 pub fn new(config: &GoogleCloudKmsSignerConfig) -> GoogleCloudKmsResult<Self> {
101 let credentials_json = serde_json::json!({
102 "type": "service_account",
103 "project_id": config.service_account.project_id,
104 "private_key_id": config.service_account.private_key_id.to_str().to_string(),
105 "private_key": config.service_account.private_key.to_str().to_string(),
106 "client_email": config.service_account.client_email.to_str().to_string(),
107 "client_id": config.service_account.client_id,
108 "auth_uri": config.service_account.auth_uri,
109 "token_uri": config.service_account.token_uri,
110 "auth_provider_x509_cert_url": config.service_account.auth_provider_x509_cert_url,
111 "client_x509_cert_url": config.service_account.client_x509_cert_url,
112 "universe_domain": config.service_account.universe_domain,
113 });
114 let credentials = GcpCredBuilder::new(credentials_json)
115 .build()
116 .map_err(|e| GoogleCloudKmsError::ConfigError(e.to_string()))?;
117
118 Ok(Self {
119 config: config.clone(),
120 credentials: Arc::new(credentials),
121 client: Client::new(),
122 })
123 }
124
125 async fn get_auth_headers(&self) -> GoogleCloudKmsResult<HeaderMap> {
126 #[cfg(test)]
128 {
129 let mut headers = HeaderMap::new();
131 headers.insert("Authorization", "Bearer test-token".parse().unwrap());
132 Ok(headers)
133 }
134
135 #[cfg(not(test))]
136 {
137 self.credentials
138 .headers(Extensions::new())
139 .await
140 .map_err(|e| GoogleCloudKmsError::ConfigError(e.to_string()))
141 }
142 }
143
144 fn get_base_url(&self) -> String {
145 if self
146 .config
147 .service_account
148 .universe_domain
149 .starts_with("http")
150 {
151 self.config.service_account.universe_domain.clone()
152 } else {
153 format!(
154 "https://cloudkms.{}",
155 self.config.service_account.universe_domain
156 )
157 }
158 }
159
160 async fn kms_get(&self, url: &str) -> GoogleCloudKmsResult<Value> {
161 let headers = self.get_auth_headers().await?;
162 let resp = self
163 .client
164 .get(url)
165 .headers(headers)
166 .send()
167 .await
168 .map_err(|e| GoogleCloudKmsError::HttpError(e.to_string()))?;
169
170 let status = resp.status();
171 let text = resp.text().await.unwrap_or_else(|_| "".to_string());
172
173 if !status.is_success() {
174 return Err(GoogleCloudKmsError::ApiError(format!(
175 "KMS request failed ({}): {}",
176 status, text
177 )));
178 }
179
180 serde_json::from_str(&text)
181 .map_err(|e| GoogleCloudKmsError::ParseError(format!("{}: {}", e, text)))
182 }
183
184 async fn kms_post(&self, url: &str, body: &Value) -> GoogleCloudKmsResult<Value> {
185 let headers = self.get_auth_headers().await?;
186 let resp = self
187 .client
188 .post(url)
189 .headers(headers)
190 .json(body)
191 .send()
192 .await
193 .map_err(|e| GoogleCloudKmsError::HttpError(e.to_string()))?;
194
195 let status = resp.status();
196 let text = resp.text().await.unwrap_or_else(|_| "".to_string());
197
198 if !status.is_success() {
199 return Err(GoogleCloudKmsError::ApiError(format!(
200 "KMS request failed ({}): {}",
201 status, text
202 )));
203 }
204
205 serde_json::from_str(&text)
206 .map_err(|e| GoogleCloudKmsError::ParseError(format!("{}: {}", e, text)))
207 }
208
209 fn get_key_path(&self) -> String {
210 format!(
211 "projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/{}",
212 self.config.service_account.project_id,
213 self.config.key.location,
214 self.config.key.key_ring_id,
215 self.config.key.key_id,
216 self.config.key.key_version
217 )
218 }
219
220 async fn get_pem(&self) -> GoogleCloudKmsResult<String> {
222 let base_url = self.get_base_url();
223 let key_path = self.get_key_path();
224 let url = format!("{}/v1/{}/publicKey", base_url, key_path,);
225 debug!("KMS publicKey URL: {}", url);
226
227 let body = self.kms_get(&url).await?;
228 let pem_str = body
229 .get("pem")
230 .and_then(|v| v.as_str())
231 .ok_or_else(|| GoogleCloudKmsError::MissingField("pem".to_string()))?;
232
233 Ok(pem_str.to_string())
234 }
235
236 pub async fn sign_bytes_evm(&self, bytes: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
240 let digest = keccak256(bytes).0;
241 let der_signature = self.sign_digest(digest).await?;
242
243 let rs = k256::ecdsa::Signature::from_der(&der_signature)
245 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
246
247 let pem_str = self.get_pem().await?;
248
249 let pem_parsed =
251 pem::parse(&pem_str).map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
252 let der_pk = pem_parsed.contents();
253
254 let pk = extract_public_key_from_der(der_pk)
255 .map_err(|e| GoogleCloudKmsError::ConvertError(e.to_string()))?;
256
257 let v = utils::recover_public_key(&pk, &rs, bytes)?;
258
259 let eth_v = 27 + v;
261
262 let mut sig_bytes = rs.to_vec();
263 sig_bytes.push(eth_v);
264
265 Ok(sig_bytes)
266 }
267}
268
269#[async_trait]
270impl GoogleCloudKmsK256 for GoogleCloudKmsService {
271 async fn get_pem_public_key(&self) -> GoogleCloudKmsResult<String> {
272 self.get_pem().await
273 }
274
275 async fn sign_digest(&self, digest: [u8; 32]) -> GoogleCloudKmsResult<Vec<u8>> {
276 let base_url = self.get_base_url();
277 let key_path = self.get_key_path();
278 let url = format!("{}/v1/{}:asymmetricSign", base_url, key_path);
279
280 let digest_b64 = base64_encode(&digest);
281
282 let body = serde_json::json!({
283 "name": key_path,
284 "digest": {
285 "sha256": digest_b64
286 }
287 });
288
289 let resp = self.kms_post(&url, &body).await?;
290 let signature_b64 = resp
291 .get("signature")
292 .and_then(|v| v.as_str())
293 .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
294
295 let signature = base64_decode(signature_b64)
296 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
297
298 Ok(signature)
299 }
300}
301
302#[async_trait]
303impl GoogleCloudKmsServiceTrait for GoogleCloudKmsService {
304 async fn get_solana_address(&self) -> GoogleCloudKmsResult<String> {
305 let pem_str = self.get_pem().await?;
306
307 println!("PEM solana: {}", pem_str);
308
309 utils::derive_solana_address_from_pem(&pem_str).map_err(GoogleCloudKmsError::from)
310 }
311
312 async fn get_evm_address(&self) -> GoogleCloudKmsResult<String> {
313 let pem_str = self.get_pem().await?;
314
315 println!("PEM evm: {}", pem_str);
316
317 let address_bytes =
318 utils::derive_ethereum_address_from_pem(&pem_str).map_err(GoogleCloudKmsError::from)?;
319 Ok(format!("0x{}", hex::encode(address_bytes)))
320 }
321
322 async fn sign_solana(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
323 let base_url = self.get_base_url();
324 let key_path = self.get_key_path();
325
326 let url = format!("{}/v1/{}:asymmetricSign", base_url, key_path,);
327 debug!("KMS asymmetricSign URL: {}", url);
328
329 let body = serde_json::json!({
330 "name": key_path,
331 "data": base64_encode(message)
332 });
333
334 print!("KMS asymmetricSign body: {}", body);
335
336 let resp = self.kms_post(&url, &body).await?;
337 let signature_b64 = resp
338 .get("signature")
339 .and_then(|v| v.as_str())
340 .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
341
342 println!("KMS asymmetricSign response: {}", resp);
343
344 let signature = base64_decode(signature_b64)
345 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
346
347 Ok(signature)
348 }
349
350 async fn sign_evm(&self, message: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
351 let base_url = self.get_base_url();
352 let key_path = self.get_key_path();
353 let url = format!("{}/v1/{}:asymmetricSign", base_url, key_path,);
354 debug!("KMS asymmetricSign URL: {}", url);
355
356 let hash = Sha256::digest(message);
357 let digest = base64_encode(&hash);
358
359 let body = serde_json::json!({
360 "name": key_path,
361 "digest": {
362 "sha256": digest
363 }
364 });
365
366 print!("KMS asymmetricSign body: {}", body);
367
368 let resp = self.kms_post(&url, &body).await?;
369 let signature = resp
370 .get("signature")
371 .and_then(|v| v.as_str())
372 .ok_or_else(|| GoogleCloudKmsError::MissingField("signature".to_string()))?;
373
374 println!("KMS asymmetricSign response: {}", resp);
375 let signature_b64 =
376 base64_decode(signature).map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
377 print!("Signature b64 decoded: {:?}", signature_b64);
378 Ok(signature_b64)
379 }
380}
381
382#[async_trait]
383impl GoogleCloudKmsEvmService for GoogleCloudKmsService {
384 async fn get_evm_address(&self) -> GoogleCloudKmsResult<Address> {
385 let pem_str = self.get_pem().await?;
386 let eth_address = derive_ethereum_address_from_pem(&pem_str)
387 .map_err(|e| GoogleCloudKmsError::ParseError(e.to_string()))?;
388 Ok(Address::Evm(eth_address))
389 }
390
391 async fn sign_payload_evm(&self, payload: &[u8]) -> GoogleCloudKmsResult<Vec<u8>> {
392 self.sign_bytes_evm(payload).await
393 }
394}
395
396impl From<utils::AddressDerivationError> for GoogleCloudKmsError {
397 fn from(value: utils::AddressDerivationError) -> Self {
398 match value {
399 utils::AddressDerivationError::ParseError(msg) => GoogleCloudKmsError::ParseError(msg),
400 }
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::models::{
408 GoogleCloudKmsSignerConfig, GoogleCloudKmsSignerKeyConfig,
409 GoogleCloudKmsSignerServiceAccountConfig, SecretString,
410 };
411 use alloy::primitives::utils::eip191_message;
412 use serde_json::json;
413 use wiremock::matchers::{header_exists, method, path_regex};
414 use wiremock::{Mock, MockServer, ResponseTemplate};
415
416 fn create_test_config(uri: &str) -> GoogleCloudKmsSignerConfig {
417 GoogleCloudKmsSignerConfig {
418 service_account: GoogleCloudKmsSignerServiceAccountConfig {
419 project_id: "test-project".to_string(),
420 private_key_id: SecretString::new("test-private-key-id"),
421 private_key: SecretString::new("-----BEGIN EXAMPLE PRIVATE KEY-----\nFAKEKEYDATA\n-----END EXAMPLE PRIVATE KEY-----\n"),
422 client_email: SecretString::new("test-service-account@example.com"),
423 client_id: "test-client-id".to_string(),
424 auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
425 token_uri: "https://oauth2.googleapis.com/token".to_string(),
426 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test-service-account%40example.com".to_string(),
427 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(),
428 universe_domain: uri.to_string(),
429 },
430 key: GoogleCloudKmsSignerKeyConfig {
431 location: "global".to_string(),
432 key_id: "test-key-id".to_string(),
433 key_ring_id: "test-key-ring-id".to_string(),
434 key_version: 1,
435 },
436 }
437 }
438
439 #[tokio::test]
440 async fn test_service_creation_success() {
441 let config = create_test_config("https://example.com");
442 let result = GoogleCloudKmsService::new(&config);
443 assert!(result.is_ok());
444 }
445
446 #[tokio::test]
447 async fn test_get_key_path_format() {
448 let config = create_test_config("https://example.com");
449 let service = GoogleCloudKmsService::new(&config).unwrap();
450
451 let key_path = service.get_key_path();
452 let expected = "projects/test-project/locations/global/keyRings/test-key-ring-id/cryptoKeys/test-key-id/cryptoKeyVersions/1";
453
454 assert_eq!(key_path, expected);
455 }
456
457 #[tokio::test]
458 async fn test_get_base_url_with_http_prefix() {
459 let config = create_test_config("http://localhost:8080");
460 let service = GoogleCloudKmsService::new(&config).unwrap();
461
462 let base_url = service.get_base_url();
463 assert_eq!(base_url, "http://localhost:8080");
464 }
465
466 #[tokio::test]
467 async fn test_get_base_url_without_http_prefix() {
468 let config = create_test_config("googleapis.com");
469 let service = GoogleCloudKmsService::new(&config).unwrap();
470
471 let base_url = service.get_base_url();
472 assert_eq!(base_url, "https://cloudkms.googleapis.com");
473 }
474
475 async fn setup_mock_solana_public_key(mock_server: &MockServer) {
477 Mock::given(method("GET"))
478 .and(path_regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey"))
479 .and(header_exists("Authorization"))
480 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
481 "pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAVyC+iqnSu0vo6R8x0sRMhintQtoZgcLOur1VyvCrdrs=\n-----END PUBLIC KEY-----\n",
482 "algorithm": "ECDSA_P256_SHA256"
483 })))
484 .mount(mock_server)
485 .await;
486 }
487
488 async fn setup_mock_evm_public_key(mock_server: &MockServer) {
489 Mock::given(method("GET"))
490 .and(path_regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey"))
491 .and(header_exists("Authorization"))
492 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
493 "pem": "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEjJaJh5wfZwvj8b3bQ4GYikqDTLXWUjMh\nkFs9lGj2N9B17zo37p4PSy99rDio0QHLadpso0rtTJDSISRW9MdOqA==\n-----END PUBLIC KEY-----\n", "algorithm": "ECDSA_SECP256K1_SHA256"
495 })))
496 .mount(mock_server)
497 .await;
498 }
499
500 async fn setup_mock_sign_success(mock_server: &MockServer) {
501 Mock::given(method("POST"))
502 .and(path_regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign"))
503 .and(header_exists("Authorization"))
504 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
505 "signature": "ZHVtbXlzaWduYXR1cmU=" })))
507 .mount(mock_server)
508 .await;
509 }
510
511 async fn setup_mock_sign_error(mock_server: &MockServer) {
512 Mock::given(method("POST"))
513 .and(path_regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign"))
514 .and(header_exists("Authorization"))
515 .respond_with(ResponseTemplate::new(400).set_body_json(json!({
516 "error": {
517 "code": 400,
518 "message": "Invalid request",
519 "status": "INVALID_ARGUMENT"
520 }
521 })))
522 .mount(mock_server)
523 .await;
524 }
525
526 async fn setup_mock_get_key_error(mock_server: &MockServer) {
527 Mock::given(method("GET"))
528 .and(path_regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey"))
529 .and(header_exists("Authorization"))
530 .respond_with(ResponseTemplate::new(404).set_body_json(json!({
531 "error": {
532 "code": 404,
533 "message": "Key not found",
534 "status": "NOT_FOUND"
535 }
536 })))
537 .mount(mock_server)
538 .await;
539 }
540
541 async fn setup_mock_malformed_response(mock_server: &MockServer) {
542 Mock::given(method("GET"))
543 .and(path_regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey"))
544 .and(header_exists("Authorization"))
545 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
546 "algorithm": "ED25519"
547 })))
549 .mount(mock_server)
550 .await;
551 }
552
553 #[tokio::test]
555 async fn test_get_solana_address_success() {
556 let mock_server = MockServer::start().await;
557 setup_mock_solana_public_key(&mock_server).await;
558
559 let config = create_test_config(&mock_server.uri());
560 let service = GoogleCloudKmsService::new(&config).unwrap();
561
562 let result = service.get_solana_address().await;
563 assert!(result.is_ok());
564 assert_eq!(
565 result.unwrap(),
566 "6s7RsvzcdXFJi1tXeDoGfSKZWjCDNJLiu74rd72zLy6J"
567 );
568 }
569
570 #[tokio::test]
571 async fn test_get_solana_address_api_error() {
572 let mock_server = MockServer::start().await;
573 setup_mock_get_key_error(&mock_server).await;
574
575 let config = create_test_config(&mock_server.uri());
576 let service = GoogleCloudKmsService::new(&config).unwrap();
577
578 let result = service.get_solana_address().await;
579 assert!(result.is_err());
580 assert!(matches!(
581 result.unwrap_err(),
582 GoogleCloudKmsError::ApiError(_)
583 ));
584 }
585
586 #[tokio::test]
587 async fn test_get_evm_address_success() {
588 let mock_server = MockServer::start().await;
589 setup_mock_evm_public_key(&mock_server).await;
590
591 let config = create_test_config(&mock_server.uri());
592 let service = GoogleCloudKmsService::new(&config).unwrap();
593
594 let result = GoogleCloudKmsServiceTrait::get_evm_address(&service).await;
595 assert!(result.is_ok());
596
597 let address = result.unwrap();
598 assert!(address.starts_with("0x"));
599 assert_eq!(address.len(), 42);
600 }
601
602 #[tokio::test]
603 async fn test_sign_solana_success() {
604 let mock_server = MockServer::start().await;
605 setup_mock_sign_success(&mock_server).await;
606
607 let config = create_test_config(&mock_server.uri());
608 let service = GoogleCloudKmsService::new(&config).unwrap();
609
610 let result = service.sign_solana(b"test message").await;
611 assert!(result.is_ok());
612 assert_eq!(result.unwrap(), b"dummysignature");
613 }
614
615 #[tokio::test]
616 async fn test_sign_solana_api_error() {
617 let mock_server = MockServer::start().await;
618 setup_mock_sign_error(&mock_server).await;
619
620 let config = create_test_config(&mock_server.uri());
621 let service = GoogleCloudKmsService::new(&config).unwrap();
622
623 let result = service.sign_solana(b"test message").await;
624 assert!(result.is_err());
625 assert!(matches!(
626 result.unwrap_err(),
627 GoogleCloudKmsError::ApiError(_)
628 ));
629 }
630
631 #[tokio::test]
632 async fn test_sign_evm_success() {
633 let mock_server = MockServer::start().await;
634 setup_mock_sign_success(&mock_server).await;
635
636 let config = create_test_config(&mock_server.uri());
637 let service = GoogleCloudKmsService::new(&config).unwrap();
638
639 let result = service.sign_evm(b"test message").await;
640 assert!(result.is_ok());
641 assert_eq!(result.unwrap(), b"dummysignature");
642 }
643
644 #[tokio::test]
645 async fn test_sign_evm_api_error() {
646 let mock_server = MockServer::start().await;
647 setup_mock_sign_error(&mock_server).await;
648
649 let config = create_test_config(&mock_server.uri());
650 let service = GoogleCloudKmsService::new(&config).unwrap();
651
652 let result = service.sign_evm(b"test message").await;
653 assert!(result.is_err());
654 assert!(matches!(
655 result.unwrap_err(),
656 GoogleCloudKmsError::ApiError(_)
657 ));
658 }
659
660 #[tokio::test]
662 async fn test_evm_service_get_address_success() {
663 let mock_server = MockServer::start().await;
664 setup_mock_evm_public_key(&mock_server).await;
665
666 let config = create_test_config(&mock_server.uri());
667 let service = GoogleCloudKmsService::new(&config).unwrap();
668
669 let result = GoogleCloudKmsEvmService::get_evm_address(&service).await;
670 assert!(result.is_ok());
671
672 let address = result.unwrap();
673 assert!(matches!(address, Address::Evm(_)));
674 if let Address::Evm(addr) = address {
675 assert_eq!(addr.len(), 20);
676 }
677 }
678
679 #[tokio::test]
680 async fn test_evm_service_get_address_api_error() {
681 let mock_server = MockServer::start().await;
682 setup_mock_get_key_error(&mock_server).await;
683
684 let config = create_test_config(&mock_server.uri());
685 let service = GoogleCloudKmsService::new(&config).unwrap();
686
687 let result = GoogleCloudKmsEvmService::get_evm_address(&service).await;
688 assert!(result.is_err());
689 assert!(matches!(
690 result.unwrap_err(),
691 GoogleCloudKmsError::ApiError(_)
692 ));
693 }
694
695 #[tokio::test]
696 async fn test_sign_payload_evm_network_error() {
697 let config = create_test_config("http://invalid-host:9999");
698 let service = GoogleCloudKmsService::new(&config).unwrap();
699
700 let message = eip191_message(b"Hello World!");
701 let result = GoogleCloudKmsEvmService::sign_payload_evm(&service, &message).await;
702 assert!(result.is_err());
703 assert!(matches!(
704 result.unwrap_err(),
705 GoogleCloudKmsError::HttpError(_)
706 ));
707 }
708
709 #[tokio::test]
710 async fn test_get_pem_public_key_success() {
711 let mock_server = MockServer::start().await;
712 setup_mock_evm_public_key(&mock_server).await;
713
714 let config = create_test_config(&mock_server.uri());
715 let service = GoogleCloudKmsService::new(&config).unwrap();
716
717 let result = GoogleCloudKmsK256::get_pem_public_key(&service).await;
718 assert!(result.is_ok());
719 assert!(result.unwrap().contains("BEGIN PUBLIC KEY"));
720 }
721
722 #[tokio::test]
723 async fn test_get_pem_public_key_missing_field() {
724 let mock_server = MockServer::start().await;
725 setup_mock_malformed_response(&mock_server).await;
726
727 let config = create_test_config(&mock_server.uri());
728 let service = GoogleCloudKmsService::new(&config).unwrap();
729
730 let result = GoogleCloudKmsK256::get_pem_public_key(&service).await;
731 assert!(result.is_err());
732 assert!(matches!(
733 result.unwrap_err(),
734 GoogleCloudKmsError::MissingField(_)
735 ));
736 }
737
738 #[tokio::test]
739 async fn test_sign_digest_success() {
740 let mock_server = MockServer::start().await;
741 setup_mock_sign_success(&mock_server).await;
742
743 let config = create_test_config(&mock_server.uri());
744 let service = GoogleCloudKmsService::new(&config).unwrap();
745
746 let digest = [0u8; 32];
747 let result = GoogleCloudKmsK256::sign_digest(&service, digest).await;
748 assert!(result.is_ok());
749 assert_eq!(result.unwrap(), b"dummysignature");
750 }
751
752 #[tokio::test]
753 async fn test_sign_digest_api_error() {
754 let mock_server = MockServer::start().await;
755 setup_mock_sign_error(&mock_server).await;
756
757 let config = create_test_config(&mock_server.uri());
758 let service = GoogleCloudKmsService::new(&config).unwrap();
759
760 let digest = [0u8; 32];
761 let result = GoogleCloudKmsK256::sign_digest(&service, digest).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_network_failure_handling() {
771 let config = create_test_config("http://localhost:99999"); let service = GoogleCloudKmsService::new(&config).unwrap();
773
774 let solana_addr_result = service.get_solana_address().await;
776 assert!(solana_addr_result.is_err());
777 assert!(matches!(
778 solana_addr_result.unwrap_err(),
779 GoogleCloudKmsError::HttpError(_)
780 ));
781
782 let evm_addr_result = GoogleCloudKmsServiceTrait::get_evm_address(&service).await;
783 assert!(evm_addr_result.is_err());
784 assert!(matches!(
785 evm_addr_result.unwrap_err(),
786 GoogleCloudKmsError::HttpError(_)
787 ));
788
789 let sign_solana_result = service.sign_solana(b"test").await;
790 assert!(sign_solana_result.is_err());
791 assert!(matches!(
792 sign_solana_result.unwrap_err(),
793 GoogleCloudKmsError::HttpError(_)
794 ));
795
796 let sign_evm_result = service.sign_evm(b"test").await;
797 assert!(sign_evm_result.is_err());
798 assert!(matches!(
799 sign_evm_result.unwrap_err(),
800 GoogleCloudKmsError::HttpError(_)
801 ));
802 }
803
804 #[tokio::test]
805 async fn test_config_with_different_universe_domains() {
806 let config1 = create_test_config("googleapis.com");
807 let service1 = GoogleCloudKmsService::new(&config1).unwrap();
808 assert_eq!(service1.get_base_url(), "https://cloudkms.googleapis.com");
809
810 let config2 = create_test_config("https://custom-domain.com");
811 let service2 = GoogleCloudKmsService::new(&config2).unwrap();
812 assert_eq!(service2.get_base_url(), "https://custom-domain.com");
813 }
814
815 #[tokio::test]
816 async fn test_solana_address_derivation() {
817 let valid_ed25519_pem = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAnUV+ReQWxMZ3Z2pC/5aOPPjcc8jzOo0ZgSl7+j4AMLo=\n-----END PUBLIC KEY-----\n";
818 let result = utils::derive_solana_address_from_pem(valid_ed25519_pem);
819 assert!(result.is_ok());
820 assert_eq!(
821 result.unwrap(),
822 "BavUBpkD77FABnevMkBVqV8BDHv7gX8sSoYYJY9WU9L5"
823 );
824 }
825
826 #[tokio::test]
827 async fn test_malformed_json_response() {
828 let mock_server = MockServer::start().await;
829
830 Mock::given(method("GET"))
831 .and(path_regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*/publicKey"))
832 .and(header_exists("Authorization"))
833 .respond_with(ResponseTemplate::new(200).set_body_string("invalid json"))
834 .mount(&mock_server)
835 .await;
836
837 let config = create_test_config(&mock_server.uri());
838 let service = GoogleCloudKmsService::new(&config).unwrap();
839
840 let result = service.get_solana_address().await;
841 assert!(result.is_err());
842 assert!(matches!(
843 result.unwrap_err(),
844 GoogleCloudKmsError::ParseError(_)
845 ));
846 }
847
848 #[tokio::test]
849 async fn test_missing_signature_field_in_response() {
850 let mock_server = MockServer::start().await;
851
852 Mock::given(method("POST"))
853 .and(path_regex(r"/v1/projects/.*/locations/global/keyRings/.*/cryptoKeys/.*/cryptoKeyVersions/.*:asymmetricSign"))
854 .and(header_exists("Authorization"))
855 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
856 "name": "test-key"
857 })))
859 .mount(&mock_server)
860 .await;
861
862 let config = create_test_config(&mock_server.uri());
863 let service = GoogleCloudKmsService::new(&config).unwrap();
864
865 let result = service.sign_solana(b"test").await;
866 assert!(result.is_err());
867 assert!(matches!(
868 result.unwrap_err(),
869 GoogleCloudKmsError::MissingField(_)
870 ));
871 }
872}