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::{
32 constants::ID_REGEX,
33 models::SecretString,
34 utils::{base64_decode, validate_safe_url},
35};
36use secrets::SecretVec;
37use serde::{Deserialize, Serialize, Serializer};
38use solana_sdk::pubkey::Pubkey;
39use std::str::FromStr;
40use utoipa::ToSchema;
41use validator::Validate;
42
43fn serialize_secret_redacted<S>(_secret: &SecretVec<u8>, serializer: S) -> Result<S::Ok, S::Error>
45where
46 S: Serializer,
47{
48 serializer.serialize_str("[REDACTED]")
49}
50
51#[derive(Debug, Clone, Serialize)]
53pub struct LocalSignerConfig {
54 #[serde(serialize_with = "serialize_secret_redacted")]
55 pub raw_key: SecretVec<u8>,
56}
57
58impl LocalSignerConfig {
59 pub fn validate(&self) -> Result<(), SignerValidationError> {
61 let key_bytes = self.raw_key.borrow();
62
63 if key_bytes.len() != 32 {
65 return Err(SignerValidationError::InvalidConfig(format!(
66 "Raw key must be exactly 32 bytes, got {} bytes",
67 key_bytes.len()
68 )));
69 }
70
71 if key_bytes.iter().all(|&b| b == 0) {
73 return Err(SignerValidationError::InvalidConfig(
74 "Raw key cannot be all zeros".to_string(),
75 ));
76 }
77
78 Ok(())
79 }
80}
81
82impl<'de> Deserialize<'de> for LocalSignerConfig {
83 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
84 where
85 D: serde::Deserializer<'de>,
86 {
87 #[derive(Deserialize)]
88 struct LocalSignerConfigHelper {
89 raw_key: String,
90 }
91
92 let helper = LocalSignerConfigHelper::deserialize(deserializer)?;
93 let raw_key = if helper.raw_key == "[REDACTED]" {
94 SecretVec::zero(32)
96 } else {
97 SecretVec::new(helper.raw_key.len(), |v| {
100 v.copy_from_slice(helper.raw_key.as_bytes())
101 })
102 };
103
104 Ok(LocalSignerConfig { raw_key })
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
125pub struct AwsKmsSignerConfig {
126 #[validate(length(min = 1, message = "Region cannot be empty"))]
127 pub region: Option<String>,
128 #[validate(length(min = 1, message = "Key ID cannot be empty"))]
129 pub key_id: String,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
134pub struct VaultSignerConfig {
135 #[validate(url(message = "Address must be a valid URL"))]
136 pub address: String,
137 pub namespace: Option<String>,
138 #[validate(custom(
139 function = "validate_secret_string",
140 message = "Role ID cannot be empty"
141 ))]
142 pub role_id: SecretString,
143 #[validate(custom(
144 function = "validate_secret_string",
145 message = "Secret ID cannot be empty"
146 ))]
147 pub secret_id: SecretString,
148 #[validate(length(min = 1, message = "Vault key name cannot be empty"))]
149 pub key_name: String,
150 pub mount_point: Option<String>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
155pub struct VaultTransitSignerConfig {
156 #[validate(length(min = 1, message = "Key name cannot be empty"))]
157 pub key_name: String,
158 #[validate(url(message = "Address must be a valid URL"))]
159 pub address: String,
160 pub namespace: Option<String>,
161 #[validate(custom(
162 function = "validate_secret_string",
163 message = "Role ID cannot be empty"
164 ))]
165 pub role_id: SecretString,
166 #[validate(custom(
167 function = "validate_secret_string",
168 message = "Secret ID cannot be empty"
169 ))]
170 pub secret_id: SecretString,
171 #[validate(length(min = 1, message = "pubkey cannot be empty"))]
172 pub pubkey: String,
173 pub mount_point: Option<String>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
178pub struct TurnkeySignerConfig {
179 #[validate(length(min = 1, message = "API public key cannot be empty"))]
180 pub api_public_key: String,
181 #[validate(custom(
182 function = "validate_secret_string",
183 message = "API private key cannot be empty"
184 ))]
185 pub api_private_key: SecretString,
186 #[validate(length(min = 1, message = "Organization ID cannot be empty"))]
187 pub organization_id: String,
188 #[validate(length(min = 1, message = "Private key ID cannot be empty"))]
189 pub private_key_id: String,
190 #[validate(length(min = 1, message = "Public key cannot be empty"))]
191 pub public_key: String,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
196#[validate(schema(function = "validate_cdp_config"))]
197pub struct CdpSignerConfig {
198 #[validate(length(min = 1, message = "API Key ID cannot be empty"))]
199 pub api_key_id: String,
200 #[validate(custom(
201 function = "validate_secret_string",
202 message = "API Key Secret cannot be empty"
203 ))]
204 pub api_key_secret: SecretString,
205 #[validate(custom(
206 function = "validate_secret_string",
207 message = "API Wallet Secret cannot be empty"
208 ))]
209 pub wallet_secret: SecretString,
210 #[validate(length(min = 1, message = "Account address cannot be empty"))]
211 pub account_address: String,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
219pub struct GoogleCloudKmsSignerServiceAccountConfig {
220 #[validate(custom(
221 function = "validate_secret_string",
222 message = "Private key cannot be empty"
223 ))]
224 pub private_key: SecretString,
225 #[validate(custom(
226 function = "validate_secret_string",
227 message = "Private key ID cannot be empty"
228 ))]
229 pub private_key_id: SecretString,
230 #[validate(custom(
231 function = "validate_secret_string",
232 message = "Project ID cannot be empty"
233 ))]
234 pub project_id: SecretString,
235 #[validate(custom(
236 function = "validate_secret_string",
237 message = "Client email cannot be empty"
238 ))]
239 pub client_email: SecretString,
240 #[validate(custom(
241 function = "validate_secret_string",
242 message = "Client ID cannot be empty"
243 ))]
244 pub client_id: SecretString,
245 #[validate(custom(
246 function = "validate_secret_url",
247 message = "Auth URI must be a valid URL"
248 ))]
249 pub auth_uri: SecretString,
250 #[validate(custom(
251 function = "validate_secret_url",
252 message = "Token URI must be a valid URL"
253 ))]
254 pub token_uri: SecretString,
255 #[validate(custom(
256 function = "validate_secret_url",
257 message = "Auth provider x509 cert URL must be a valid URL"
258 ))]
259 pub auth_provider_x509_cert_url: SecretString,
260 #[validate(custom(
261 function = "validate_secret_url",
262 message = "Client x509 cert URL must be a valid URL"
263 ))]
264 pub client_x509_cert_url: SecretString,
265 #[validate(
266 custom(
267 function = "validate_secret_string",
268 message = "Universe domain cannot be empty"
269 ),
270 custom(
271 function = "validate_universe_domain",
272 message = "Universe domain must be a valid Google Cloud KMS domain"
273 )
274 )]
275 pub universe_domain: SecretString,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
283pub struct GoogleCloudKmsSignerKeyConfig {
284 #[validate(custom(
285 function = "validate_secret_string",
286 message = "Location cannot be empty"
287 ))]
288 pub location: SecretString,
289 #[validate(custom(
290 function = "validate_secret_string",
291 message = "Key ring ID cannot be empty"
292 ))]
293 pub key_ring_id: SecretString,
294 #[validate(custom(
295 function = "validate_secret_string",
296 message = "Key ID cannot be empty"
297 ))]
298 pub key_id: SecretString,
299 pub key_version: u32,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
304pub struct GoogleCloudKmsSignerConfig {
305 #[validate(nested)]
306 pub service_account: GoogleCloudKmsSignerServiceAccountConfig,
307 #[validate(nested)]
308 pub key: GoogleCloudKmsSignerKeyConfig,
309}
310
311fn validate_secret_string(secret: &SecretString) -> Result<(), validator::ValidationError> {
313 if secret.to_str().is_empty() {
314 return Err(validator::ValidationError::new("empty_secret"));
315 }
316 Ok(())
317}
318
319fn validate_secret_url(secret: &SecretString) -> Result<(), validator::ValidationError> {
321 secret.as_str(|s| {
322 if s.is_empty() {
323 return Err(validator::ValidationError::new("empty_url"));
324 }
325 reqwest::Url::parse(s).map_err(|_| validator::ValidationError::new("invalid_url"))?;
326 Ok(())
327 })
328}
329
330const ALLOWED_KMS_DOMAINS: &[&str] = &[
334 "cloudkms.googleapis.com", ];
336
337fn validate_universe_domain(secret: &SecretString) -> Result<(), validator::ValidationError> {
340 let value = secret.to_str();
341 let url = if value.starts_with("http") {
343 value.to_string()
344 } else {
345 format!("https://cloudkms.{}", &*value)
346 };
347
348 let allowed_hosts: Vec<String> = ALLOWED_KMS_DOMAINS.iter().map(|s| s.to_string()).collect();
349
350 validate_safe_url(&url, &allowed_hosts, true).map_err(|e| {
352 let mut err = validator::ValidationError::new("universe_domain_ssrf");
353 err.message = Some(e.into());
354 err
355 })
356}
357
358fn validate_cdp_config(config: &CdpSignerConfig) -> Result<(), validator::ValidationError> {
360 let api_key_valid = config
362 .api_key_secret
363 .as_str(|secret_str| base64_decode(secret_str).is_ok());
364 if !api_key_valid {
365 let mut error = validator::ValidationError::new("invalid_base64_api_key_secret");
366 error.message = Some("API Key Secret is not valid base64".into());
367 return Err(error);
368 }
369
370 let wallet_secret_valid = config
372 .wallet_secret
373 .as_str(|secret_str| base64_decode(secret_str).is_ok());
374 if !wallet_secret_valid {
375 let mut error = validator::ValidationError::new("invalid_base64_wallet_secret");
376 error.message = Some("Wallet Secret is not valid base64".into());
377 return Err(error);
378 }
379
380 let addr = &config.account_address;
381
382 if addr.starts_with("0x") {
384 if addr.len() != 42 {
385 let mut error = validator::ValidationError::new("invalid_evm_address_format");
386 error.message = Some(
387 "EVM account address must be a valid 0x-prefixed 40-character hex string".into(),
388 );
389 return Err(error);
390 }
391
392 if let Some(end) = addr.strip_prefix("0x") {
394 if !end.chars().all(|c| c.is_ascii_hexdigit()) {
395 let mut error = validator::ValidationError::new("invalid_evm_address_hex");
396 error.message = Some("EVM account address contains invalid hex characters".into());
397 return Err(error);
398 }
399 }
400 } else {
401 if Pubkey::from_str(addr).is_err() {
403 let mut error = validator::ValidationError::new("invalid_solana_address");
404 error.message = Some("Invalid Solana account address format".into());
405 return Err(error);
406 }
407 }
408
409 Ok(())
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
414pub enum SignerConfig {
415 Local(LocalSignerConfig),
416 Vault(VaultSignerConfig),
417 VaultTransit(VaultTransitSignerConfig),
418 AwsKms(AwsKmsSignerConfig),
419 Turnkey(TurnkeySignerConfig),
420 Cdp(CdpSignerConfig),
421 GoogleCloudKms(Box<GoogleCloudKmsSignerConfig>),
422}
423
424impl SignerConfig {
425 pub fn validate(&self) -> Result<(), SignerValidationError> {
427 match self {
428 Self::Local(config) => config.validate(),
429 Self::AwsKms(config) => Validate::validate(config).map_err(|e| {
430 SignerValidationError::InvalidConfig(format!(
431 "AWS KMS validation failed: {}",
432 format_validation_errors(&e)
433 ))
434 }),
435 Self::Vault(config) => Validate::validate(config).map_err(|e| {
436 SignerValidationError::InvalidConfig(format!(
437 "Vault validation failed: {}",
438 format_validation_errors(&e)
439 ))
440 }),
441 Self::VaultTransit(config) => Validate::validate(config).map_err(|e| {
442 SignerValidationError::InvalidConfig(format!(
443 "Vault Transit validation failed: {}",
444 format_validation_errors(&e)
445 ))
446 }),
447 Self::Turnkey(config) => Validate::validate(config).map_err(|e| {
448 SignerValidationError::InvalidConfig(format!(
449 "Turnkey validation failed: {}",
450 format_validation_errors(&e)
451 ))
452 }),
453 Self::Cdp(config) => Validate::validate(config).map_err(|e| {
454 SignerValidationError::InvalidConfig(format!(
455 "CDP validation failed: {}",
456 format_validation_errors(&e)
457 ))
458 }),
459 Self::GoogleCloudKms(config) => Validate::validate(config.as_ref()).map_err(|e| {
460 SignerValidationError::InvalidConfig(format!(
461 "Google Cloud KMS validation failed: {}",
462 format_validation_errors(&e)
463 ))
464 }),
465 }
466 }
467
468 pub fn get_local(&self) -> Option<&LocalSignerConfig> {
470 match self {
471 Self::Local(config) => Some(config),
472 _ => None,
473 }
474 }
475
476 pub fn get_aws_kms(&self) -> Option<&AwsKmsSignerConfig> {
478 match self {
479 Self::AwsKms(config) => Some(config),
480 _ => None,
481 }
482 }
483
484 pub fn get_vault(&self) -> Option<&VaultSignerConfig> {
486 match self {
487 Self::Vault(config) => Some(config),
488 _ => None,
489 }
490 }
491
492 pub fn get_vault_transit(&self) -> Option<&VaultTransitSignerConfig> {
494 match self {
495 Self::VaultTransit(config) => Some(config),
496 _ => None,
497 }
498 }
499
500 pub fn get_turnkey(&self) -> Option<&TurnkeySignerConfig> {
502 match self {
503 Self::Turnkey(config) => Some(config),
504 _ => None,
505 }
506 }
507
508 pub fn get_cdp(&self) -> Option<&CdpSignerConfig> {
510 match self {
511 Self::Cdp(config) => Some(config),
512 _ => None,
513 }
514 }
515
516 pub fn get_google_cloud_kms(&self) -> Option<&GoogleCloudKmsSignerConfig> {
518 match self {
519 Self::GoogleCloudKms(config) => Some(config),
520 _ => None,
521 }
522 }
523
524 pub fn get_signer_type(&self) -> SignerType {
526 match self {
527 Self::Local(_) => SignerType::Local,
528 Self::AwsKms(_) => SignerType::AwsKms,
529 Self::Vault(_) => SignerType::Vault,
530 Self::VaultTransit(_) => SignerType::VaultTransit,
531 Self::Turnkey(_) => SignerType::Turnkey,
532 Self::Cdp(_) => SignerType::Cdp,
533 Self::GoogleCloudKms(_) => SignerType::GoogleCloudKms,
534 }
535 }
536}
537
538fn format_validation_errors(errors: &validator::ValidationErrors) -> String {
540 let mut messages = Vec::new();
541
542 for (field, field_errors) in errors.field_errors().iter() {
543 let field_msgs: Vec<String> = field_errors
544 .iter()
545 .map(|error| error.message.clone().unwrap_or_default().to_string())
546 .collect();
547 messages.push(format!("{}: {}", field, field_msgs.join(", ")));
548 }
549
550 for (struct_field, kind) in errors.errors().iter() {
551 if let validator::ValidationErrorsKind::Struct(nested) = kind {
552 let nested_msgs = format_validation_errors(nested);
553 messages.push(format!("{struct_field}.{nested_msgs}"));
554 }
555 }
556
557 messages.join("; ")
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
562pub struct Signer {
563 #[validate(
564 length(min = 1, max = 36, message = "ID must be between 1 and 36 characters"),
565 regex(
566 path = "*ID_REGEX",
567 message = "ID must contain only letters, numbers, dashes and underscores"
568 )
569 )]
570 pub id: String,
571 pub config: SignerConfig,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
576#[serde(rename_all = "lowercase")]
577pub enum SignerType {
578 Local,
579 #[serde(rename = "aws_kms")]
580 AwsKms,
581 #[serde(rename = "google_cloud_kms")]
582 GoogleCloudKms,
583 Vault,
584 #[serde(rename = "vault_transit")]
585 VaultTransit,
586 Turnkey,
587 Cdp,
588}
589
590impl Signer {
591 pub fn new(id: String, config: SignerConfig) -> Self {
593 Self { id, config }
594 }
595
596 pub fn signer_type(&self) -> SignerType {
598 self.config.get_signer_type()
599 }
600
601 pub fn validate(&self) -> Result<(), SignerValidationError> {
603 Validate::validate(self).map_err(|validation_errors| {
605 for (field, errors) in validation_errors.field_errors() {
608 if let Some(error) = errors.first() {
609 let field_str = field.as_ref();
610 return match (field_str, error.code.as_ref()) {
611 ("id", "length") => SignerValidationError::InvalidIdFormat,
612 ("id", "regex") => SignerValidationError::InvalidIdFormat,
613 _ => SignerValidationError::InvalidIdFormat, };
615 }
616 }
617 SignerValidationError::InvalidIdFormat
619 })?;
620
621 self.config.validate()?;
623
624 Ok(())
625 }
626}
627
628#[derive(Debug, thiserror::Error)]
630pub enum SignerValidationError {
631 #[error("Signer ID cannot be empty")]
632 EmptyId,
633 #[error("Signer ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long")]
634 InvalidIdFormat,
635 #[error("Invalid signer configuration: {0}")]
636 InvalidConfig(String),
637}
638
639impl From<SignerValidationError> for crate::models::ApiError {
641 fn from(error: SignerValidationError) -> Self {
642 use crate::models::ApiError;
643
644 ApiError::BadRequest(match error {
645 SignerValidationError::EmptyId => "ID cannot be empty".to_string(),
646 SignerValidationError::InvalidIdFormat => {
647 "ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long".to_string()
648 }
649 SignerValidationError::InvalidConfig(msg) => format!("Invalid signer configuration: {msg}"),
650 })
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 #[test]
659 fn test_valid_local_signer() {
660 let config = SignerConfig::Local(LocalSignerConfig {
661 raw_key: SecretVec::new(32, |v| v.fill(1)),
662 });
663
664 let signer = Signer::new("valid-id".to_string(), config);
665
666 assert!(signer.validate().is_ok());
667 assert_eq!(signer.signer_type(), SignerType::Local);
668 }
669
670 #[test]
671 fn test_valid_aws_kms_signer() {
672 let config = SignerConfig::AwsKms(AwsKmsSignerConfig {
673 region: Some("us-east-1".to_string()),
674 key_id: "test-key-id".to_string(),
675 });
676
677 let signer = Signer::new("aws-signer".to_string(), config);
678
679 assert!(signer.validate().is_ok());
680 assert_eq!(signer.signer_type(), SignerType::AwsKms);
681 }
682
683 #[test]
684 fn test_empty_id() {
685 let config = SignerConfig::Local(LocalSignerConfig {
686 raw_key: SecretVec::new(32, |v| v.fill(1)), });
688
689 let signer = Signer::new("".to_string(), config);
690
691 assert!(matches!(
692 signer.validate(),
693 Err(SignerValidationError::InvalidIdFormat)
694 ));
695 }
696
697 #[test]
698 fn test_id_too_long() {
699 let config = SignerConfig::Local(LocalSignerConfig {
700 raw_key: SecretVec::new(32, |v| v.fill(1)), });
702
703 let signer = Signer::new("a".repeat(37), config);
704
705 assert!(matches!(
706 signer.validate(),
707 Err(SignerValidationError::InvalidIdFormat)
708 ));
709 }
710
711 #[test]
712 fn test_invalid_id_format() {
713 let config = SignerConfig::Local(LocalSignerConfig {
714 raw_key: SecretVec::new(32, |v| v.fill(1)), });
716
717 let signer = Signer::new("invalid@id".to_string(), config);
718
719 assert!(matches!(
720 signer.validate(),
721 Err(SignerValidationError::InvalidIdFormat)
722 ));
723 }
724
725 #[test]
726 fn test_local_signer_invalid_key_length() {
727 let config = SignerConfig::Local(LocalSignerConfig {
728 raw_key: SecretVec::new(16, |v| v.fill(1)), });
730
731 let signer = Signer::new("valid-id".to_string(), config);
732
733 let result = signer.validate();
734 assert!(result.is_err());
735 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
736 assert!(msg.contains("Raw key must be exactly 32 bytes"));
737 assert!(msg.contains("got 16 bytes"));
738 } else {
739 panic!("Expected InvalidConfig error for invalid key length");
740 }
741 }
742
743 #[test]
744 fn test_local_signer_all_zero_key() {
745 let config = SignerConfig::Local(LocalSignerConfig {
746 raw_key: SecretVec::new(32, |v| v.fill(0)), });
748
749 let signer = Signer::new("valid-id".to_string(), config);
750
751 let result = signer.validate();
752 assert!(result.is_err());
753 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
754 assert_eq!(msg, "Raw key cannot be all zeros");
755 } else {
756 panic!("Expected InvalidConfig error for all-zero key");
757 }
758 }
759
760 #[test]
761 fn test_local_signer_valid_key() {
762 let config = SignerConfig::Local(LocalSignerConfig {
763 raw_key: SecretVec::new(32, |v| v.fill(1)), });
765
766 let signer = Signer::new("valid-id".to_string(), config);
767
768 assert!(signer.validate().is_ok());
769 }
770
771 #[test]
772 fn test_signer_type_serialization() {
773 use serde_json::{from_str, to_string};
774
775 assert_eq!(to_string(&SignerType::Local).unwrap(), "\"local\"");
776 assert_eq!(to_string(&SignerType::AwsKms).unwrap(), "\"aws_kms\"");
777 assert_eq!(
778 to_string(&SignerType::GoogleCloudKms).unwrap(),
779 "\"google_cloud_kms\""
780 );
781 assert_eq!(
782 to_string(&SignerType::VaultTransit).unwrap(),
783 "\"vault_transit\""
784 );
785
786 assert_eq!(
787 from_str::<SignerType>("\"local\"").unwrap(),
788 SignerType::Local
789 );
790 assert_eq!(
791 from_str::<SignerType>("\"aws_kms\"").unwrap(),
792 SignerType::AwsKms
793 );
794 }
795
796 #[test]
797 fn test_config_accessor_methods() {
798 let local_config = LocalSignerConfig {
800 raw_key: SecretVec::new(32, |v| v.fill(1)),
801 };
802 let config = SignerConfig::Local(local_config);
803 assert!(config.get_local().is_some());
804 assert!(config.get_aws_kms().is_none());
805
806 let aws_config = AwsKmsSignerConfig {
808 region: Some("us-east-1".to_string()),
809 key_id: "test-key".to_string(),
810 };
811 let config = SignerConfig::AwsKms(aws_config);
812 assert!(config.get_aws_kms().is_some());
813 assert!(config.get_local().is_none());
814 }
815
816 #[test]
817 fn test_error_conversion_to_api_error() {
818 let error = SignerValidationError::InvalidIdFormat;
819 let api_error: crate::models::ApiError = error.into();
820
821 if let crate::models::ApiError::BadRequest(msg) = api_error {
822 assert!(msg.contains("ID must contain only letters, numbers, dashes and underscores"));
823 } else {
824 panic!("Expected BadRequest error");
825 }
826 }
827
828 #[test]
829 fn test_valid_vault_signer() {
830 let config = SignerConfig::Vault(VaultSignerConfig {
831 address: "https://vault.example.com".to_string(),
832 namespace: Some("test".to_string()),
833 role_id: SecretString::new("role-id"),
834 secret_id: SecretString::new("secret-id"),
835 key_name: "test-key".to_string(),
836 mount_point: None,
837 });
838
839 let signer = Signer::new("vault-signer".to_string(), config);
840 assert!(signer.validate().is_ok());
841 assert_eq!(signer.signer_type(), SignerType::Vault);
842 }
843
844 #[test]
845 fn test_invalid_vault_signer_url() {
846 let config = SignerConfig::Vault(VaultSignerConfig {
847 address: "not-a-url".to_string(),
848 namespace: Some("test".to_string()),
849 role_id: SecretString::new("role-id"),
850 secret_id: SecretString::new("secret-id"),
851 key_name: "test-key".to_string(),
852 mount_point: None,
853 });
854
855 let signer = Signer::new("vault-signer".to_string(), config);
856 let result = signer.validate();
857 assert!(result.is_err());
858 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
859 assert!(msg.contains("Address must be a valid URL"));
860 } else {
861 panic!("Expected InvalidConfig error for invalid URL");
862 }
863 }
864
865 #[test]
866 fn test_valid_google_cloud_kms_signer() {
867 let config = SignerConfig::GoogleCloudKms(Box::new(GoogleCloudKmsSignerConfig {
868 service_account: GoogleCloudKmsSignerServiceAccountConfig {
869 private_key: SecretString::new("private-key"),
870 private_key_id: SecretString::new("key-id"),
871 project_id: SecretString::new("project"),
872 client_email: SecretString::new("client@example.com"),
873 client_id: SecretString::new("client-id"),
874 auth_uri: SecretString::new("https://accounts.google.com/o/oauth2/auth"),
875 token_uri: SecretString::new("https://oauth2.googleapis.com/token"),
876 auth_provider_x509_cert_url: SecretString::new(
877 "https://www.googleapis.com/oauth2/v1/certs",
878 ),
879 client_x509_cert_url: SecretString::new(
880 "https://www.googleapis.com/robot/v1/metadata/x509/test",
881 ),
882 universe_domain: SecretString::new("googleapis.com"),
883 },
884 key: GoogleCloudKmsSignerKeyConfig {
885 location: SecretString::new("us-central1"),
886 key_ring_id: SecretString::new("test-ring"),
887 key_id: SecretString::new("test-key"),
888 key_version: 1,
889 },
890 }));
891
892 let signer = Signer::new("gcp-kms-signer".to_string(), config);
893 assert!(signer.validate().is_ok());
894 assert_eq!(signer.signer_type(), SignerType::GoogleCloudKms);
895 }
896
897 #[test]
898 fn test_invalid_google_cloud_kms_urls() {
899 let config = SignerConfig::GoogleCloudKms(Box::new(GoogleCloudKmsSignerConfig {
900 service_account: GoogleCloudKmsSignerServiceAccountConfig {
901 private_key: SecretString::new("private-key"),
902 private_key_id: SecretString::new("key-id"),
903 project_id: SecretString::new("project"),
904 client_email: SecretString::new("client@example.com"),
905 client_id: SecretString::new("client-id"),
906 auth_uri: SecretString::new("not-a-url"), token_uri: SecretString::new("https://oauth2.googleapis.com/token"),
908 auth_provider_x509_cert_url: SecretString::new(
909 "https://www.googleapis.com/oauth2/v1/certs",
910 ),
911 client_x509_cert_url: SecretString::new(
912 "https://www.googleapis.com/robot/v1/metadata/x509/test",
913 ),
914 universe_domain: SecretString::new("googleapis.com"),
915 },
916 key: GoogleCloudKmsSignerKeyConfig {
917 location: SecretString::new("us-central1"),
918 key_ring_id: SecretString::new("test-ring"),
919 key_id: SecretString::new("test-key"),
920 key_version: 1,
921 },
922 }));
923
924 let signer = Signer::new("gcp-kms-signer".to_string(), config);
925 let result = signer.validate();
926 assert!(result.is_err());
927 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
928 assert!(msg.contains("Auth URI must be a valid URL"));
929 } else {
930 panic!("Expected InvalidConfig error for invalid URL");
931 }
932 }
933
934 #[test]
935 fn test_secret_string_validation() {
936 let result = validate_secret_string(&SecretString::new(""));
938 if let Err(e) = result {
939 assert_eq!(e.code, "empty_secret");
940 } else {
941 panic!("Expected validation error for empty secret");
942 }
943
944 let result = validate_secret_string(&SecretString::new("secret"));
946 assert!(result.is_ok());
947 }
948
949 #[test]
950 fn test_validation_error_formatting() {
951 let invalid_config = GoogleCloudKmsSignerConfig {
953 service_account: GoogleCloudKmsSignerServiceAccountConfig {
954 private_key: SecretString::new(""), private_key_id: SecretString::new("key-id"),
956 project_id: SecretString::new("project"),
957 client_email: SecretString::new("client@example.com"),
958 client_id: SecretString::new(""), auth_uri: SecretString::new("not-a-url"), token_uri: SecretString::new("https://oauth2.googleapis.com/token"),
961 auth_provider_x509_cert_url: SecretString::new(
962 "https://www.googleapis.com/oauth2/v1/certs",
963 ),
964 client_x509_cert_url: SecretString::new(
965 "https://www.googleapis.com/robot/v1/metadata/x509/test",
966 ),
967 universe_domain: SecretString::new("googleapis.com"),
968 },
969 key: GoogleCloudKmsSignerKeyConfig {
970 location: SecretString::new("us-central1"),
971 key_ring_id: SecretString::new(""), key_id: SecretString::new("test-key"),
973 key_version: 1,
974 },
975 };
976
977 let errors = invalid_config.validate().unwrap_err();
978
979 let formatted = format_validation_errors(&errors);
981
982 println!("formatted: {formatted}");
983
984 assert!(formatted.contains("client_id: Client ID cannot be empty"));
986 assert!(formatted.contains("private_key: Private key cannot be empty"));
987 assert!(formatted.contains("auth_uri: Auth URI must be a valid URL"));
988 assert!(formatted.contains("key_ring_id: Key ring ID cannot be empty"));
989 }
990
991 #[test]
992 fn test_config_type_getters() {
993 let vault_config = VaultSignerConfig {
995 address: "https://vault.example.com".to_string(),
996 namespace: None,
997 role_id: SecretString::new("role"),
998 secret_id: SecretString::new("secret"),
999 key_name: "key".to_string(),
1000 mount_point: None,
1001 };
1002 let config = SignerConfig::Vault(vault_config);
1003 assert!(config.get_vault().is_some());
1004
1005 let vault_transit_config = VaultTransitSignerConfig {
1007 key_name: "key".to_string(),
1008 address: "https://vault.example.com".to_string(),
1009 namespace: None,
1010 role_id: SecretString::new("role"),
1011 secret_id: SecretString::new("secret"),
1012 pubkey: "pubkey".to_string(),
1013 mount_point: None,
1014 };
1015 let config = SignerConfig::VaultTransit(vault_transit_config);
1016 assert!(config.get_vault_transit().is_some());
1017 assert!(config.get_turnkey().is_none());
1018
1019 let turnkey_config = TurnkeySignerConfig {
1021 api_public_key: "public".to_string(),
1022 api_private_key: SecretString::new("private"),
1023 organization_id: "org".to_string(),
1024 private_key_id: "key-id".to_string(),
1025 public_key: "pubkey".to_string(),
1026 };
1027 let config = SignerConfig::Turnkey(turnkey_config);
1028 assert!(config.get_turnkey().is_some());
1029 assert!(config.get_google_cloud_kms().is_none());
1030
1031 let gcp_config = GoogleCloudKmsSignerConfig {
1033 service_account: GoogleCloudKmsSignerServiceAccountConfig {
1034 private_key: SecretString::new("private-key"),
1035 private_key_id: SecretString::new("key-id"),
1036 project_id: SecretString::new("project"),
1037 client_email: SecretString::new("client@example.com"),
1038 client_id: SecretString::new("client-id"),
1039 auth_uri: SecretString::new("https://accounts.google.com/o/oauth2/auth"),
1040 token_uri: SecretString::new("https://oauth2.googleapis.com/token"),
1041 auth_provider_x509_cert_url: SecretString::new(
1042 "https://www.googleapis.com/oauth2/v1/certs",
1043 ),
1044 client_x509_cert_url: SecretString::new(
1045 "https://www.googleapis.com/robot/v1/metadata/x509/test",
1046 ),
1047 universe_domain: SecretString::new("googleapis.com"),
1048 },
1049 key: GoogleCloudKmsSignerKeyConfig {
1050 location: SecretString::new("us-central1"),
1051 key_ring_id: SecretString::new("test-ring"),
1052 key_id: SecretString::new("test-key"),
1053 key_version: 1,
1054 },
1055 };
1056 let config = SignerConfig::GoogleCloudKms(Box::new(gcp_config));
1057 assert!(config.get_google_cloud_kms().is_some());
1058 assert!(config.get_local().is_none());
1059 }
1060
1061 #[test]
1062 fn test_valid_cdp_signer_with_evm_address() {
1063 let config = CdpSignerConfig {
1064 api_key_id: "test-api-key".to_string(),
1065 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
1068 };
1069 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1070 assert!(signer.validate().is_ok());
1071 assert_eq!(signer.signer_type(), SignerType::Cdp);
1072 }
1073
1074 #[test]
1075 fn test_valid_cdp_signer_with_solana_address() {
1076 let config = CdpSignerConfig {
1077 api_key_id: "test-api-key".to_string(),
1078 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string(),
1081 };
1082 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1083 assert!(signer.validate().is_ok());
1084 assert_eq!(signer.signer_type(), SignerType::Cdp);
1085 }
1086
1087 #[test]
1088 fn test_invalid_cdp_signer_empty_address() {
1089 let config = CdpSignerConfig {
1090 api_key_id: "test-api-key".to_string(),
1091 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "".to_string(),
1094 };
1095 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1096 let result = signer.validate();
1097 assert!(result.is_err());
1098 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1099 assert!(msg.contains("Account address cannot be empty"));
1100 } else {
1101 panic!("Expected InvalidConfig error for empty address");
1102 }
1103 }
1104
1105 #[test]
1106 fn test_invalid_cdp_signer_bad_evm_address() {
1107 let config = CdpSignerConfig {
1108 api_key_id: "test-api-key".to_string(),
1109 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "0xinvalid-address".to_string(),
1112 };
1113 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1114 let result = signer.validate();
1115 assert!(result.is_err());
1116 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1117 assert!(msg.contains("EVM account address must be a valid 0x-prefixed"));
1118 } else {
1119 panic!("Expected InvalidConfig error for bad EVM address");
1120 }
1121 }
1122
1123 #[test]
1124 fn test_invalid_cdp_signer_bad_solana_address() {
1125 let config = CdpSignerConfig {
1126 api_key_id: "test-api-key".to_string(),
1127 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "invalid".to_string(),
1130 };
1131 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1132 let result = signer.validate();
1133 assert!(result.is_err());
1134 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1135 assert!(msg.contains("Invalid Solana account address format"));
1136 } else {
1137 panic!("Expected InvalidConfig error for bad Solana address");
1138 }
1139 }
1140
1141 #[test]
1142 fn test_invalid_cdp_signer_evm_address_wrong_format() {
1143 let config = CdpSignerConfig {
1144 api_key_id: "test-api-key".to_string(),
1145 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44".to_string(), };
1149 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1150 let result = signer.validate();
1151 assert!(result.is_err());
1152 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1153 assert!(msg.contains("EVM account address must be a valid 0x-prefixed"));
1154 } else {
1155 panic!("Expected InvalidConfig error for wrong EVM address format");
1156 }
1157 }
1158
1159 #[test]
1160 fn test_invalid_cdp_signer_solana_address_wrong_charset() {
1161 let config = CdpSignerConfig {
1162 api_key_id: "test-api-key".to_string(),
1163 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm0".to_string(), };
1167 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1168 let result = signer.validate();
1169 assert!(result.is_err());
1170 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1171 assert!(msg.contains("Invalid Solana account address format"));
1172 } else {
1173 panic!("Expected InvalidConfig error for wrong Solana address charset");
1174 }
1175 }
1176
1177 #[test]
1178 fn test_invalid_cdp_signer_invalid_base64_api_key_secret() {
1179 let config = CdpSignerConfig {
1180 api_key_id: "test-api-key".to_string(),
1181 api_key_secret: SecretString::new("invalid-base64!@#"), wallet_secret: SecretString::new("dGVzdC13YWxsZXQtc2VjcmV0"), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
1184 };
1185 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1186 let result = signer.validate();
1187 assert!(result.is_err());
1188 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1189 assert!(msg.contains("API Key Secret is not valid base64"));
1190 } else {
1191 panic!("Expected InvalidConfig error for invalid base64 API key secret");
1192 }
1193 }
1194
1195 #[test]
1196 fn test_invalid_cdp_signer_invalid_base64_wallet_secret() {
1197 let config = CdpSignerConfig {
1198 api_key_id: "test-api-key".to_string(),
1199 api_key_secret: SecretString::new("dGVzdC1hcGkta2V5LXNlY3JldA=="), wallet_secret: SecretString::new("invalid-base64!@#"), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
1202 };
1203 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1204 let result = signer.validate();
1205 assert!(result.is_err());
1206 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1207 assert!(msg.contains("Wallet Secret is not valid base64"));
1208 } else {
1209 panic!("Expected InvalidConfig error for invalid base64 wallet secret");
1210 }
1211 }
1212
1213 #[test]
1214 fn test_valid_cdp_signer_with_valid_base64_secrets() {
1215 let config = CdpSignerConfig {
1216 api_key_id: "test-api-key".to_string(),
1217 api_key_secret: SecretString::new("dGVzdC1hcGkta2V5LXNlY3JldA=="), wallet_secret: SecretString::new("dGVzdC13YWxsZXQtc2VjcmV0"), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
1220 };
1221 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1222 let result = signer.validate();
1223 assert!(result.is_ok());
1224 assert_eq!(signer.signer_type(), SignerType::Cdp);
1225 }
1226
1227 #[test]
1228 fn test_validate_universe_domain_valid_default() {
1229 let result = validate_universe_domain(&SecretString::new("googleapis.com"));
1231 assert!(result.is_ok());
1232 }
1233
1234 #[test]
1235 fn test_validate_universe_domain_valid_explicit_https() {
1236 let result =
1238 validate_universe_domain(&SecretString::new("https://cloudkms.googleapis.com"));
1239 assert!(result.is_ok());
1240 }
1241
1242 #[test]
1243 fn test_validate_universe_domain_invalid_aws_metadata() {
1244 let result = validate_universe_domain(&SecretString::new("http://169.254.169.254"));
1246 assert!(result.is_err());
1247 if let Err(e) = result {
1248 assert_eq!(e.code, "universe_domain_ssrf");
1249 }
1250 }
1251
1252 #[test]
1253 fn test_validate_universe_domain_invalid_gcp_metadata() {
1254 let result =
1256 validate_universe_domain(&SecretString::new("http://metadata.google.internal"));
1257 assert!(result.is_err());
1258 if let Err(e) = result {
1259 assert_eq!(e.code, "universe_domain_ssrf");
1260 }
1261 }
1262
1263 #[test]
1264 fn test_validate_universe_domain_invalid_localhost() {
1265 let result = validate_universe_domain(&SecretString::new("http://localhost:8080"));
1267 assert!(result.is_err());
1268 if let Err(e) = result {
1269 assert_eq!(e.code, "universe_domain_ssrf");
1270 }
1271 }
1272
1273 #[test]
1274 fn test_validate_universe_domain_invalid_private_ip() {
1275 let result = validate_universe_domain(&SecretString::new("http://192.168.1.1"));
1277 assert!(result.is_err());
1278 if let Err(e) = result {
1279 assert_eq!(e.code, "universe_domain_ssrf");
1280 }
1281
1282 let result = validate_universe_domain(&SecretString::new("http://10.0.0.1"));
1283 assert!(result.is_err());
1284 if let Err(e) = result {
1285 assert_eq!(e.code, "universe_domain_ssrf");
1286 }
1287 }
1288
1289 #[test]
1290 fn test_validate_universe_domain_rejects_non_allowlisted_domains() {
1291 let result = validate_universe_domain(&SecretString::new("https://evil.com"));
1294 assert!(result.is_err());
1295 if let Err(e) = result {
1296 assert_eq!(e.code, "universe_domain_ssrf");
1297 }
1298
1299 let result = validate_universe_domain(&SecretString::new(
1301 "https://cloudkms.googleapis.com.evil.com",
1302 ));
1303 assert!(result.is_err());
1304 if let Err(e) = result {
1305 assert_eq!(e.code, "universe_domain_ssrf");
1306 }
1307
1308 let result =
1310 validate_universe_domain(&SecretString::new("https://cloudkms.googleapis.org"));
1311 assert!(result.is_err());
1312 if let Err(e) = result {
1313 assert_eq!(e.code, "universe_domain_ssrf");
1314 }
1315
1316 let result = validate_universe_domain(&SecretString::new("example.com"));
1318 assert!(result.is_err());
1319 if let Err(e) = result {
1320 assert_eq!(e.code, "universe_domain_ssrf");
1321 }
1322 }
1323}