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