1mod repository;
15pub use repository::{
16 AwsKmsSignerConfigStorage, GoogleCloudKmsSignerConfigStorage,
17 GoogleCloudKmsSignerKeyConfigStorage, GoogleCloudKmsSignerServiceAccountConfigStorage,
18 LocalSignerConfigStorage, SignerConfigStorage, SignerRepoModel, TurnkeySignerConfigStorage,
19 VaultSignerConfigStorage, VaultTransitSignerConfigStorage,
20};
21
22mod config;
23pub use config::*;
24
25mod request;
26pub use request::*;
27
28mod response;
29pub use response::*;
30
31use crate::{constants::ID_REGEX, models::SecretString, utils::base64_decode};
32use secrets::SecretVec;
33use serde::{Deserialize, Serialize, Serializer};
34use solana_sdk::pubkey::Pubkey;
35use std::str::FromStr;
36use utoipa::ToSchema;
37use validator::Validate;
38
39fn serialize_secret_redacted<S>(_secret: &SecretVec<u8>, serializer: S) -> Result<S::Ok, S::Error>
41where
42 S: Serializer,
43{
44 serializer.serialize_str("[REDACTED]")
45}
46
47#[derive(Debug, Clone, Serialize)]
49pub struct LocalSignerConfig {
50 #[serde(serialize_with = "serialize_secret_redacted")]
51 pub raw_key: SecretVec<u8>,
52}
53
54impl LocalSignerConfig {
55 pub fn validate(&self) -> Result<(), SignerValidationError> {
57 let key_bytes = self.raw_key.borrow();
58
59 if key_bytes.len() != 32 {
61 return Err(SignerValidationError::InvalidConfig(format!(
62 "Raw key must be exactly 32 bytes, got {} bytes",
63 key_bytes.len()
64 )));
65 }
66
67 if key_bytes.iter().all(|&b| b == 0) {
69 return Err(SignerValidationError::InvalidConfig(
70 "Raw key cannot be all zeros".to_string(),
71 ));
72 }
73
74 Ok(())
75 }
76}
77
78impl<'de> Deserialize<'de> for LocalSignerConfig {
79 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80 where
81 D: serde::Deserializer<'de>,
82 {
83 #[derive(Deserialize)]
84 struct LocalSignerConfigHelper {
85 raw_key: String,
86 }
87
88 let helper = LocalSignerConfigHelper::deserialize(deserializer)?;
89 let raw_key = if helper.raw_key == "[REDACTED]" {
90 SecretVec::zero(32)
92 } else {
93 SecretVec::new(helper.raw_key.len(), |v| {
96 v.copy_from_slice(helper.raw_key.as_bytes())
97 })
98 };
99
100 Ok(LocalSignerConfig { raw_key })
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
121pub struct AwsKmsSignerConfig {
122 #[validate(length(min = 1, message = "Region cannot be empty"))]
123 pub region: Option<String>,
124 #[validate(length(min = 1, message = "Key ID cannot be empty"))]
125 pub key_id: String,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
130pub struct VaultSignerConfig {
131 #[validate(url(message = "Address must be a valid URL"))]
132 pub address: String,
133 pub namespace: Option<String>,
134 #[validate(custom(
135 function = "validate_secret_string",
136 message = "Role ID cannot be empty"
137 ))]
138 pub role_id: SecretString,
139 #[validate(custom(
140 function = "validate_secret_string",
141 message = "Secret ID cannot be empty"
142 ))]
143 pub secret_id: SecretString,
144 #[validate(length(min = 1, message = "Vault key name cannot be empty"))]
145 pub key_name: String,
146 pub mount_point: Option<String>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
151pub struct VaultTransitSignerConfig {
152 #[validate(length(min = 1, message = "Key name cannot be empty"))]
153 pub key_name: String,
154 #[validate(url(message = "Address must be a valid URL"))]
155 pub address: String,
156 pub namespace: Option<String>,
157 #[validate(custom(
158 function = "validate_secret_string",
159 message = "Role ID cannot be empty"
160 ))]
161 pub role_id: SecretString,
162 #[validate(custom(
163 function = "validate_secret_string",
164 message = "Secret ID cannot be empty"
165 ))]
166 pub secret_id: SecretString,
167 #[validate(length(min = 1, message = "pubkey cannot be empty"))]
168 pub pubkey: String,
169 pub mount_point: Option<String>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
174pub struct TurnkeySignerConfig {
175 #[validate(length(min = 1, message = "API public key cannot be empty"))]
176 pub api_public_key: String,
177 #[validate(custom(
178 function = "validate_secret_string",
179 message = "API private key cannot be empty"
180 ))]
181 pub api_private_key: SecretString,
182 #[validate(length(min = 1, message = "Organization ID cannot be empty"))]
183 pub organization_id: String,
184 #[validate(length(min = 1, message = "Private key ID cannot be empty"))]
185 pub private_key_id: String,
186 #[validate(length(min = 1, message = "Public key cannot be empty"))]
187 pub public_key: String,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
192#[validate(schema(function = "validate_cdp_config"))]
193pub struct CdpSignerConfig {
194 #[validate(length(min = 1, message = "API Key ID cannot be empty"))]
195 pub api_key_id: String,
196 #[validate(custom(
197 function = "validate_secret_string",
198 message = "API Key Secret cannot be empty"
199 ))]
200 pub api_key_secret: SecretString,
201 #[validate(custom(
202 function = "validate_secret_string",
203 message = "API Wallet Secret cannot be empty"
204 ))]
205 pub wallet_secret: SecretString,
206 #[validate(length(min = 1, message = "Account address cannot be empty"))]
207 pub account_address: String,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
212pub struct GoogleCloudKmsSignerServiceAccountConfig {
213 #[validate(custom(
214 function = "validate_secret_string",
215 message = "Private key cannot be empty"
216 ))]
217 pub private_key: SecretString,
218 #[validate(custom(
219 function = "validate_secret_string",
220 message = "Private key ID cannot be empty"
221 ))]
222 pub private_key_id: SecretString,
223 #[validate(length(min = 1, message = "Project ID cannot be empty"))]
224 pub project_id: String,
225 #[validate(custom(
226 function = "validate_secret_string",
227 message = "Client email cannot be empty"
228 ))]
229 pub client_email: SecretString,
230 #[validate(length(min = 1, message = "Client ID cannot be empty"))]
231 pub client_id: String,
232 #[validate(url(message = "Auth URI must be a valid URL"))]
233 pub auth_uri: String,
234 #[validate(url(message = "Token URI must be a valid URL"))]
235 pub token_uri: String,
236 #[validate(url(message = "Auth provider x509 cert URL must be a valid URL"))]
237 pub auth_provider_x509_cert_url: String,
238 #[validate(url(message = "Client x509 cert URL must be a valid URL"))]
239 pub client_x509_cert_url: String,
240 pub universe_domain: String,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
245pub struct GoogleCloudKmsSignerKeyConfig {
246 pub location: String,
247 #[validate(length(min = 1, message = "Key ring ID cannot be empty"))]
248 pub key_ring_id: String,
249 #[validate(length(min = 1, message = "Key ID cannot be empty"))]
250 pub key_id: String,
251 pub key_version: u32,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
256pub struct GoogleCloudKmsSignerConfig {
257 #[validate(nested)]
258 pub service_account: GoogleCloudKmsSignerServiceAccountConfig,
259 #[validate(nested)]
260 pub key: GoogleCloudKmsSignerKeyConfig,
261}
262
263fn validate_secret_string(secret: &SecretString) -> Result<(), validator::ValidationError> {
265 if secret.to_str().is_empty() {
266 return Err(validator::ValidationError::new("empty_secret"));
267 }
268 Ok(())
269}
270
271fn validate_cdp_config(config: &CdpSignerConfig) -> Result<(), validator::ValidationError> {
273 let api_key_valid = config
275 .api_key_secret
276 .as_str(|secret_str| base64_decode(secret_str).is_ok());
277 if !api_key_valid {
278 let mut error = validator::ValidationError::new("invalid_base64_api_key_secret");
279 error.message = Some("API Key Secret is not valid base64".into());
280 return Err(error);
281 }
282
283 let wallet_secret_valid = config
285 .wallet_secret
286 .as_str(|secret_str| base64_decode(secret_str).is_ok());
287 if !wallet_secret_valid {
288 let mut error = validator::ValidationError::new("invalid_base64_wallet_secret");
289 error.message = Some("Wallet Secret is not valid base64".into());
290 return Err(error);
291 }
292
293 let addr = &config.account_address;
294
295 if addr.starts_with("0x") {
297 if addr.len() != 42 {
298 let mut error = validator::ValidationError::new("invalid_evm_address_format");
299 error.message = Some(
300 "EVM account address must be a valid 0x-prefixed 40-character hex string".into(),
301 );
302 return Err(error);
303 }
304
305 if let Some(end) = addr.strip_prefix("0x") {
307 if !end.chars().all(|c| c.is_ascii_hexdigit()) {
308 let mut error = validator::ValidationError::new("invalid_evm_address_hex");
309 error.message = Some("EVM account address contains invalid hex characters".into());
310 return Err(error);
311 }
312 }
313 } else {
314 if Pubkey::from_str(addr).is_err() {
316 let mut error = validator::ValidationError::new("invalid_solana_address");
317 error.message = Some("Invalid Solana account address format".into());
318 return Err(error);
319 }
320 }
321
322 Ok(())
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327pub enum SignerConfig {
328 Local(LocalSignerConfig),
329 Vault(VaultSignerConfig),
330 VaultTransit(VaultTransitSignerConfig),
331 AwsKms(AwsKmsSignerConfig),
332 Turnkey(TurnkeySignerConfig),
333 Cdp(CdpSignerConfig),
334 GoogleCloudKms(GoogleCloudKmsSignerConfig),
335}
336
337impl SignerConfig {
338 pub fn validate(&self) -> Result<(), SignerValidationError> {
340 match self {
341 Self::Local(config) => config.validate(),
342 Self::AwsKms(config) => Validate::validate(config).map_err(|e| {
343 SignerValidationError::InvalidConfig(format!(
344 "AWS KMS validation failed: {}",
345 format_validation_errors(&e)
346 ))
347 }),
348 Self::Vault(config) => Validate::validate(config).map_err(|e| {
349 SignerValidationError::InvalidConfig(format!(
350 "Vault validation failed: {}",
351 format_validation_errors(&e)
352 ))
353 }),
354 Self::VaultTransit(config) => Validate::validate(config).map_err(|e| {
355 SignerValidationError::InvalidConfig(format!(
356 "Vault Transit validation failed: {}",
357 format_validation_errors(&e)
358 ))
359 }),
360 Self::Turnkey(config) => Validate::validate(config).map_err(|e| {
361 SignerValidationError::InvalidConfig(format!(
362 "Turnkey validation failed: {}",
363 format_validation_errors(&e)
364 ))
365 }),
366 Self::Cdp(config) => Validate::validate(config).map_err(|e| {
367 SignerValidationError::InvalidConfig(format!(
368 "CDP validation failed: {}",
369 format_validation_errors(&e)
370 ))
371 }),
372 Self::GoogleCloudKms(config) => Validate::validate(config).map_err(|e| {
373 SignerValidationError::InvalidConfig(format!(
374 "Google Cloud KMS validation failed: {}",
375 format_validation_errors(&e)
376 ))
377 }),
378 }
379 }
380
381 pub fn get_local(&self) -> Option<&LocalSignerConfig> {
383 match self {
384 Self::Local(config) => Some(config),
385 _ => None,
386 }
387 }
388
389 pub fn get_aws_kms(&self) -> Option<&AwsKmsSignerConfig> {
391 match self {
392 Self::AwsKms(config) => Some(config),
393 _ => None,
394 }
395 }
396
397 pub fn get_vault(&self) -> Option<&VaultSignerConfig> {
399 match self {
400 Self::Vault(config) => Some(config),
401 _ => None,
402 }
403 }
404
405 pub fn get_vault_transit(&self) -> Option<&VaultTransitSignerConfig> {
407 match self {
408 Self::VaultTransit(config) => Some(config),
409 _ => None,
410 }
411 }
412
413 pub fn get_turnkey(&self) -> Option<&TurnkeySignerConfig> {
415 match self {
416 Self::Turnkey(config) => Some(config),
417 _ => None,
418 }
419 }
420
421 pub fn get_cdp(&self) -> Option<&CdpSignerConfig> {
423 match self {
424 Self::Cdp(config) => Some(config),
425 _ => None,
426 }
427 }
428
429 pub fn get_google_cloud_kms(&self) -> Option<&GoogleCloudKmsSignerConfig> {
431 match self {
432 Self::GoogleCloudKms(config) => Some(config),
433 _ => None,
434 }
435 }
436
437 pub fn get_signer_type(&self) -> SignerType {
439 match self {
440 Self::Local(_) => SignerType::Local,
441 Self::AwsKms(_) => SignerType::AwsKms,
442 Self::Vault(_) => SignerType::Vault,
443 Self::VaultTransit(_) => SignerType::VaultTransit,
444 Self::Turnkey(_) => SignerType::Turnkey,
445 Self::Cdp(_) => SignerType::Cdp,
446 Self::GoogleCloudKms(_) => SignerType::GoogleCloudKms,
447 }
448 }
449}
450
451fn format_validation_errors(errors: &validator::ValidationErrors) -> String {
453 let mut messages = Vec::new();
454
455 for (field, field_errors) in errors.field_errors().iter() {
456 let field_msgs: Vec<String> = field_errors
457 .iter()
458 .map(|error| error.message.clone().unwrap_or_default().to_string())
459 .collect();
460 messages.push(format!("{}: {}", field, field_msgs.join(", ")));
461 }
462
463 for (struct_field, kind) in errors.errors().iter() {
464 if let validator::ValidationErrorsKind::Struct(nested) = kind {
465 let nested_msgs = format_validation_errors(nested);
466 messages.push(format!("{struct_field}.{nested_msgs}"));
467 }
468 }
469
470 messages.join("; ")
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
475pub struct Signer {
476 #[validate(
477 length(min = 1, max = 36, message = "ID must be between 1 and 36 characters"),
478 regex(
479 path = "*ID_REGEX",
480 message = "ID must contain only letters, numbers, dashes and underscores"
481 )
482 )]
483 pub id: String,
484 pub config: SignerConfig,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
489#[serde(rename_all = "lowercase")]
490pub enum SignerType {
491 Local,
492 #[serde(rename = "aws_kms")]
493 AwsKms,
494 #[serde(rename = "google_cloud_kms")]
495 GoogleCloudKms,
496 Vault,
497 #[serde(rename = "vault_transit")]
498 VaultTransit,
499 Turnkey,
500 Cdp,
501}
502
503impl Signer {
504 pub fn new(id: String, config: SignerConfig) -> Self {
506 Self { id, config }
507 }
508
509 pub fn signer_type(&self) -> SignerType {
511 self.config.get_signer_type()
512 }
513
514 pub fn validate(&self) -> Result<(), SignerValidationError> {
516 Validate::validate(self).map_err(|validation_errors| {
518 for (field, errors) in validation_errors.field_errors() {
521 if let Some(error) = errors.first() {
522 let field_str = field.as_ref();
523 return match (field_str, error.code.as_ref()) {
524 ("id", "length") => SignerValidationError::InvalidIdFormat,
525 ("id", "regex") => SignerValidationError::InvalidIdFormat,
526 _ => SignerValidationError::InvalidIdFormat, };
528 }
529 }
530 SignerValidationError::InvalidIdFormat
532 })?;
533
534 self.config.validate()?;
536
537 Ok(())
538 }
539}
540
541#[derive(Debug, thiserror::Error)]
543pub enum SignerValidationError {
544 #[error("Signer ID cannot be empty")]
545 EmptyId,
546 #[error("Signer ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long")]
547 InvalidIdFormat,
548 #[error("Invalid signer configuration: {0}")]
549 InvalidConfig(String),
550}
551
552impl From<SignerValidationError> for crate::models::ApiError {
554 fn from(error: SignerValidationError) -> Self {
555 use crate::models::ApiError;
556
557 ApiError::BadRequest(match error {
558 SignerValidationError::EmptyId => "ID cannot be empty".to_string(),
559 SignerValidationError::InvalidIdFormat => {
560 "ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long".to_string()
561 }
562 SignerValidationError::InvalidConfig(msg) => format!("Invalid signer configuration: {msg}"),
563 })
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 #[test]
572 fn test_valid_local_signer() {
573 let config = SignerConfig::Local(LocalSignerConfig {
574 raw_key: SecretVec::new(32, |v| v.fill(1)),
575 });
576
577 let signer = Signer::new("valid-id".to_string(), config);
578
579 assert!(signer.validate().is_ok());
580 assert_eq!(signer.signer_type(), SignerType::Local);
581 }
582
583 #[test]
584 fn test_valid_aws_kms_signer() {
585 let config = SignerConfig::AwsKms(AwsKmsSignerConfig {
586 region: Some("us-east-1".to_string()),
587 key_id: "test-key-id".to_string(),
588 });
589
590 let signer = Signer::new("aws-signer".to_string(), config);
591
592 assert!(signer.validate().is_ok());
593 assert_eq!(signer.signer_type(), SignerType::AwsKms);
594 }
595
596 #[test]
597 fn test_empty_id() {
598 let config = SignerConfig::Local(LocalSignerConfig {
599 raw_key: SecretVec::new(32, |v| v.fill(1)), });
601
602 let signer = Signer::new("".to_string(), config);
603
604 assert!(matches!(
605 signer.validate(),
606 Err(SignerValidationError::InvalidIdFormat)
607 ));
608 }
609
610 #[test]
611 fn test_id_too_long() {
612 let config = SignerConfig::Local(LocalSignerConfig {
613 raw_key: SecretVec::new(32, |v| v.fill(1)), });
615
616 let signer = Signer::new("a".repeat(37), config);
617
618 assert!(matches!(
619 signer.validate(),
620 Err(SignerValidationError::InvalidIdFormat)
621 ));
622 }
623
624 #[test]
625 fn test_invalid_id_format() {
626 let config = SignerConfig::Local(LocalSignerConfig {
627 raw_key: SecretVec::new(32, |v| v.fill(1)), });
629
630 let signer = Signer::new("invalid@id".to_string(), config);
631
632 assert!(matches!(
633 signer.validate(),
634 Err(SignerValidationError::InvalidIdFormat)
635 ));
636 }
637
638 #[test]
639 fn test_local_signer_invalid_key_length() {
640 let config = SignerConfig::Local(LocalSignerConfig {
641 raw_key: SecretVec::new(16, |v| v.fill(1)), });
643
644 let signer = Signer::new("valid-id".to_string(), config);
645
646 let result = signer.validate();
647 assert!(result.is_err());
648 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
649 assert!(msg.contains("Raw key must be exactly 32 bytes"));
650 assert!(msg.contains("got 16 bytes"));
651 } else {
652 panic!("Expected InvalidConfig error for invalid key length");
653 }
654 }
655
656 #[test]
657 fn test_local_signer_all_zero_key() {
658 let config = SignerConfig::Local(LocalSignerConfig {
659 raw_key: SecretVec::new(32, |v| v.fill(0)), });
661
662 let signer = Signer::new("valid-id".to_string(), config);
663
664 let result = signer.validate();
665 assert!(result.is_err());
666 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
667 assert_eq!(msg, "Raw key cannot be all zeros");
668 } else {
669 panic!("Expected InvalidConfig error for all-zero key");
670 }
671 }
672
673 #[test]
674 fn test_local_signer_valid_key() {
675 let config = SignerConfig::Local(LocalSignerConfig {
676 raw_key: SecretVec::new(32, |v| v.fill(1)), });
678
679 let signer = Signer::new("valid-id".to_string(), config);
680
681 assert!(signer.validate().is_ok());
682 }
683
684 #[test]
685 fn test_signer_type_serialization() {
686 use serde_json::{from_str, to_string};
687
688 assert_eq!(to_string(&SignerType::Local).unwrap(), "\"local\"");
689 assert_eq!(to_string(&SignerType::AwsKms).unwrap(), "\"aws_kms\"");
690 assert_eq!(
691 to_string(&SignerType::GoogleCloudKms).unwrap(),
692 "\"google_cloud_kms\""
693 );
694 assert_eq!(
695 to_string(&SignerType::VaultTransit).unwrap(),
696 "\"vault_transit\""
697 );
698
699 assert_eq!(
700 from_str::<SignerType>("\"local\"").unwrap(),
701 SignerType::Local
702 );
703 assert_eq!(
704 from_str::<SignerType>("\"aws_kms\"").unwrap(),
705 SignerType::AwsKms
706 );
707 }
708
709 #[test]
710 fn test_config_accessor_methods() {
711 let local_config = LocalSignerConfig {
713 raw_key: SecretVec::new(32, |v| v.fill(1)),
714 };
715 let config = SignerConfig::Local(local_config);
716 assert!(config.get_local().is_some());
717 assert!(config.get_aws_kms().is_none());
718
719 let aws_config = AwsKmsSignerConfig {
721 region: Some("us-east-1".to_string()),
722 key_id: "test-key".to_string(),
723 };
724 let config = SignerConfig::AwsKms(aws_config);
725 assert!(config.get_aws_kms().is_some());
726 assert!(config.get_local().is_none());
727 }
728
729 #[test]
730 fn test_error_conversion_to_api_error() {
731 let error = SignerValidationError::InvalidIdFormat;
732 let api_error: crate::models::ApiError = error.into();
733
734 if let crate::models::ApiError::BadRequest(msg) = api_error {
735 assert!(msg.contains("ID must contain only letters, numbers, dashes and underscores"));
736 } else {
737 panic!("Expected BadRequest error");
738 }
739 }
740
741 #[test]
742 fn test_valid_vault_signer() {
743 let config = SignerConfig::Vault(VaultSignerConfig {
744 address: "https://vault.example.com".to_string(),
745 namespace: Some("test".to_string()),
746 role_id: SecretString::new("role-id"),
747 secret_id: SecretString::new("secret-id"),
748 key_name: "test-key".to_string(),
749 mount_point: None,
750 });
751
752 let signer = Signer::new("vault-signer".to_string(), config);
753 assert!(signer.validate().is_ok());
754 assert_eq!(signer.signer_type(), SignerType::Vault);
755 }
756
757 #[test]
758 fn test_invalid_vault_signer_url() {
759 let config = SignerConfig::Vault(VaultSignerConfig {
760 address: "not-a-url".to_string(),
761 namespace: Some("test".to_string()),
762 role_id: SecretString::new("role-id"),
763 secret_id: SecretString::new("secret-id"),
764 key_name: "test-key".to_string(),
765 mount_point: None,
766 });
767
768 let signer = Signer::new("vault-signer".to_string(), config);
769 let result = signer.validate();
770 assert!(result.is_err());
771 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
772 assert!(msg.contains("Address must be a valid URL"));
773 } else {
774 panic!("Expected InvalidConfig error for invalid URL");
775 }
776 }
777
778 #[test]
779 fn test_valid_google_cloud_kms_signer() {
780 let config = SignerConfig::GoogleCloudKms(GoogleCloudKmsSignerConfig {
781 service_account: GoogleCloudKmsSignerServiceAccountConfig {
782 private_key: SecretString::new("private-key"),
783 private_key_id: SecretString::new("key-id"),
784 project_id: "project".to_string(),
785 client_email: SecretString::new("client@example.com"),
786 client_id: "client-id".to_string(),
787 auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
788 token_uri: "https://oauth2.googleapis.com/token".to_string(),
789 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
790 .to_string(),
791 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test"
792 .to_string(),
793 universe_domain: "googleapis.com".to_string(),
794 },
795 key: GoogleCloudKmsSignerKeyConfig {
796 location: "us-central1".to_string(),
797 key_ring_id: "test-ring".to_string(),
798 key_id: "test-key".to_string(),
799 key_version: 1,
800 },
801 });
802
803 let signer = Signer::new("gcp-kms-signer".to_string(), config);
804 assert!(signer.validate().is_ok());
805 assert_eq!(signer.signer_type(), SignerType::GoogleCloudKms);
806 }
807
808 #[test]
809 fn test_invalid_google_cloud_kms_urls() {
810 let config = SignerConfig::GoogleCloudKms(GoogleCloudKmsSignerConfig {
811 service_account: GoogleCloudKmsSignerServiceAccountConfig {
812 private_key: SecretString::new("private-key"),
813 private_key_id: SecretString::new("key-id"),
814 project_id: "project".to_string(),
815 client_email: SecretString::new("client@example.com"),
816 client_id: "client-id".to_string(),
817 auth_uri: "not-a-url".to_string(), token_uri: "https://oauth2.googleapis.com/token".to_string(),
819 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
820 .to_string(),
821 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test"
822 .to_string(),
823 universe_domain: "googleapis.com".to_string(),
824 },
825 key: GoogleCloudKmsSignerKeyConfig {
826 location: "us-central1".to_string(),
827 key_ring_id: "test-ring".to_string(),
828 key_id: "test-key".to_string(),
829 key_version: 1,
830 },
831 });
832
833 let signer = Signer::new("gcp-kms-signer".to_string(), config);
834 let result = signer.validate();
835 assert!(result.is_err());
836 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
837 assert!(msg.contains("Auth URI must be a valid URL"));
838 } else {
839 panic!("Expected InvalidConfig error for invalid URL");
840 }
841 }
842
843 #[test]
844 fn test_secret_string_validation() {
845 let result = validate_secret_string(&SecretString::new(""));
847 if let Err(e) = result {
848 assert_eq!(e.code, "empty_secret");
849 } else {
850 panic!("Expected validation error for empty secret");
851 }
852
853 let result = validate_secret_string(&SecretString::new("secret"));
855 assert!(result.is_ok());
856 }
857
858 #[test]
859 fn test_validation_error_formatting() {
860 let invalid_config = GoogleCloudKmsSignerConfig {
862 service_account: GoogleCloudKmsSignerServiceAccountConfig {
863 private_key: SecretString::new(""), private_key_id: SecretString::new("key-id"),
865 project_id: "project".to_string(),
866 client_email: SecretString::new("client@example.com"),
867 client_id: "".to_string(), auth_uri: "not-a-url".to_string(), token_uri: "https://oauth2.googleapis.com/token".to_string(),
870 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
871 .to_string(),
872 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test"
873 .to_string(),
874 universe_domain: "googleapis.com".to_string(),
875 },
876 key: GoogleCloudKmsSignerKeyConfig {
877 location: "us-central1".to_string(),
878 key_ring_id: "".to_string(), key_id: "test-key".to_string(),
880 key_version: 1,
881 },
882 };
883
884 let errors = invalid_config.validate().unwrap_err();
885
886 let formatted = format_validation_errors(&errors);
888
889 println!("formatted: {}", formatted);
890
891 assert!(formatted.contains("client_id: Client ID cannot be empty"));
893 assert!(formatted.contains("private_key: Private key cannot be empty"));
894 assert!(formatted.contains("auth_uri: Auth URI must be a valid URL"));
895 assert!(formatted.contains("key_ring_id: Key ring ID cannot be empty"));
896 }
897
898 #[test]
899 fn test_config_type_getters() {
900 let vault_config = VaultSignerConfig {
902 address: "https://vault.example.com".to_string(),
903 namespace: None,
904 role_id: SecretString::new("role"),
905 secret_id: SecretString::new("secret"),
906 key_name: "key".to_string(),
907 mount_point: None,
908 };
909 let config = SignerConfig::Vault(vault_config);
910 assert!(config.get_vault().is_some());
911
912 let vault_transit_config = VaultTransitSignerConfig {
914 key_name: "key".to_string(),
915 address: "https://vault.example.com".to_string(),
916 namespace: None,
917 role_id: SecretString::new("role"),
918 secret_id: SecretString::new("secret"),
919 pubkey: "pubkey".to_string(),
920 mount_point: None,
921 };
922 let config = SignerConfig::VaultTransit(vault_transit_config);
923 assert!(config.get_vault_transit().is_some());
924 assert!(config.get_turnkey().is_none());
925
926 let turnkey_config = TurnkeySignerConfig {
928 api_public_key: "public".to_string(),
929 api_private_key: SecretString::new("private"),
930 organization_id: "org".to_string(),
931 private_key_id: "key-id".to_string(),
932 public_key: "pubkey".to_string(),
933 };
934 let config = SignerConfig::Turnkey(turnkey_config);
935 assert!(config.get_turnkey().is_some());
936 assert!(config.get_google_cloud_kms().is_none());
937
938 let gcp_config = GoogleCloudKmsSignerConfig {
940 service_account: GoogleCloudKmsSignerServiceAccountConfig {
941 private_key: SecretString::new("private-key"),
942 private_key_id: SecretString::new("key-id"),
943 project_id: "project".to_string(),
944 client_email: SecretString::new("client@example.com"),
945 client_id: "client-id".to_string(),
946 auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
947 token_uri: "https://oauth2.googleapis.com/token".to_string(),
948 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
949 .to_string(),
950 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test"
951 .to_string(),
952 universe_domain: "googleapis.com".to_string(),
953 },
954 key: GoogleCloudKmsSignerKeyConfig {
955 location: "us-central1".to_string(),
956 key_ring_id: "test-ring".to_string(),
957 key_id: "test-key".to_string(),
958 key_version: 1,
959 },
960 };
961 let config = SignerConfig::GoogleCloudKms(gcp_config);
962 assert!(config.get_google_cloud_kms().is_some());
963 assert!(config.get_local().is_none());
964 }
965
966 #[test]
967 fn test_valid_cdp_signer_with_evm_address() {
968 let config = CdpSignerConfig {
969 api_key_id: "test-api-key".to_string(),
970 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
973 };
974 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
975 assert!(signer.validate().is_ok());
976 assert_eq!(signer.signer_type(), SignerType::Cdp);
977 }
978
979 #[test]
980 fn test_valid_cdp_signer_with_solana_address() {
981 let config = CdpSignerConfig {
982 api_key_id: "test-api-key".to_string(),
983 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string(),
986 };
987 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
988 assert!(signer.validate().is_ok());
989 assert_eq!(signer.signer_type(), SignerType::Cdp);
990 }
991
992 #[test]
993 fn test_invalid_cdp_signer_empty_address() {
994 let config = CdpSignerConfig {
995 api_key_id: "test-api-key".to_string(),
996 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "".to_string(),
999 };
1000 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1001 let result = signer.validate();
1002 assert!(result.is_err());
1003 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1004 assert!(msg.contains("Account address cannot be empty"));
1005 } else {
1006 panic!("Expected InvalidConfig error for empty address");
1007 }
1008 }
1009
1010 #[test]
1011 fn test_invalid_cdp_signer_bad_evm_address() {
1012 let config = CdpSignerConfig {
1013 api_key_id: "test-api-key".to_string(),
1014 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "0xinvalid-address".to_string(),
1017 };
1018 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1019 let result = signer.validate();
1020 assert!(result.is_err());
1021 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1022 assert!(msg.contains("EVM account address must be a valid 0x-prefixed"));
1023 } else {
1024 panic!("Expected InvalidConfig error for bad EVM address");
1025 }
1026 }
1027
1028 #[test]
1029 fn test_invalid_cdp_signer_bad_solana_address() {
1030 let config = CdpSignerConfig {
1031 api_key_id: "test-api-key".to_string(),
1032 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "invalid".to_string(),
1035 };
1036 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1037 let result = signer.validate();
1038 assert!(result.is_err());
1039 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1040 assert!(msg.contains("Invalid Solana account address format"));
1041 } else {
1042 panic!("Expected InvalidConfig error for bad Solana address");
1043 }
1044 }
1045
1046 #[test]
1047 fn test_invalid_cdp_signer_evm_address_wrong_format() {
1048 let config = CdpSignerConfig {
1049 api_key_id: "test-api-key".to_string(),
1050 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44".to_string(), };
1054 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1055 let result = signer.validate();
1056 assert!(result.is_err());
1057 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1058 assert!(msg.contains("EVM account address must be a valid 0x-prefixed"));
1059 } else {
1060 panic!("Expected InvalidConfig error for wrong EVM address format");
1061 }
1062 }
1063
1064 #[test]
1065 fn test_invalid_cdp_signer_solana_address_wrong_charset() {
1066 let config = CdpSignerConfig {
1067 api_key_id: "test-api-key".to_string(),
1068 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm0".to_string(), };
1072 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1073 let result = signer.validate();
1074 assert!(result.is_err());
1075 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1076 assert!(msg.contains("Invalid Solana account address format"));
1077 } else {
1078 panic!("Expected InvalidConfig error for wrong Solana address charset");
1079 }
1080 }
1081
1082 #[test]
1083 fn test_invalid_cdp_signer_invalid_base64_api_key_secret() {
1084 let config = CdpSignerConfig {
1085 api_key_id: "test-api-key".to_string(),
1086 api_key_secret: SecretString::new("invalid-base64!@#"), wallet_secret: SecretString::new("dGVzdC13YWxsZXQtc2VjcmV0"), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
1089 };
1090 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1091 let result = signer.validate();
1092 assert!(result.is_err());
1093 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1094 assert!(msg.contains("API Key Secret is not valid base64"));
1095 } else {
1096 panic!("Expected InvalidConfig error for invalid base64 API key secret");
1097 }
1098 }
1099
1100 #[test]
1101 fn test_invalid_cdp_signer_invalid_base64_wallet_secret() {
1102 let config = CdpSignerConfig {
1103 api_key_id: "test-api-key".to_string(),
1104 api_key_secret: SecretString::new("dGVzdC1hcGkta2V5LXNlY3JldA=="), wallet_secret: SecretString::new("invalid-base64!@#"), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
1107 };
1108 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1109 let result = signer.validate();
1110 assert!(result.is_err());
1111 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1112 assert!(msg.contains("Wallet Secret is not valid base64"));
1113 } else {
1114 panic!("Expected InvalidConfig error for invalid base64 wallet secret");
1115 }
1116 }
1117
1118 #[test]
1119 fn test_valid_cdp_signer_with_valid_base64_secrets() {
1120 let config = CdpSignerConfig {
1121 api_key_id: "test-api-key".to_string(),
1122 api_key_secret: SecretString::new("dGVzdC1hcGkta2V5LXNlY3JldA=="), wallet_secret: SecretString::new("dGVzdC13YWxsZXQtc2VjcmV0"), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
1125 };
1126 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1127 let result = signer.validate();
1128 assert!(result.is_ok());
1129 assert_eq!(signer.signer_type(), SignerType::Cdp);
1130 }
1131}