1mod repository;
15pub use repository::{
16 AwsKmsSignerConfigStorage, AzureKeyVaultSignerConfigStorage, 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, Copy, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
134#[serde(rename_all = "snake_case")]
135pub enum AzureKeyVaultAuthType {
136 ClientSecret,
137 ManagedIdentity,
138 WorkloadIdentity,
139}
140
141impl zeroize::Zeroize for AzureKeyVaultAuthType {
142 fn zeroize(&mut self) {
143 *self = Self::ClientSecret;
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
149#[validate(schema(function = "validate_azure_key_vault_config"))]
150pub struct AzureKeyVaultSignerConfig {
151 pub auth_type: Option<AzureKeyVaultAuthType>,
152 pub tenant_id: Option<SecretString>,
153 pub client_id: Option<SecretString>,
154 pub client_secret: Option<SecretString>,
155 pub federated_token_file: Option<SecretString>,
156 #[validate(custom(
157 function = "validate_secret_url",
158 message = "Vault URL must be a valid URL"
159 ))]
160 pub vault_url: SecretString,
161 #[validate(custom(
162 function = "validate_secret_string",
163 message = "Key name cannot be empty"
164 ))]
165 pub key_name: SecretString,
166 pub key_version: Option<String>,
167}
168
169impl AzureKeyVaultSignerConfig {
170 pub fn auth_type(&self) -> AzureKeyVaultAuthType {
171 self.auth_type
172 .unwrap_or(AzureKeyVaultAuthType::ClientSecret)
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
178pub struct VaultSignerConfig {
179 #[validate(url(message = "Address must be a valid URL"))]
180 pub address: String,
181 pub namespace: Option<String>,
182 #[validate(custom(
183 function = "validate_secret_string",
184 message = "Role ID cannot be empty"
185 ))]
186 pub role_id: SecretString,
187 #[validate(custom(
188 function = "validate_secret_string",
189 message = "Secret ID cannot be empty"
190 ))]
191 pub secret_id: SecretString,
192 #[validate(length(min = 1, message = "Vault key name cannot be empty"))]
193 pub key_name: String,
194 pub mount_point: Option<String>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
199pub struct VaultTransitSignerConfig {
200 #[validate(length(min = 1, message = "Key name cannot be empty"))]
201 pub key_name: String,
202 #[validate(url(message = "Address must be a valid URL"))]
203 pub address: String,
204 pub namespace: Option<String>,
205 #[validate(custom(
206 function = "validate_secret_string",
207 message = "Role ID cannot be empty"
208 ))]
209 pub role_id: SecretString,
210 #[validate(custom(
211 function = "validate_secret_string",
212 message = "Secret ID cannot be empty"
213 ))]
214 pub secret_id: SecretString,
215 #[validate(length(min = 1, message = "pubkey cannot be empty"))]
216 pub pubkey: String,
217 pub mount_point: Option<String>,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
222pub struct TurnkeySignerConfig {
223 #[validate(length(min = 1, message = "API public key cannot be empty"))]
224 pub api_public_key: String,
225 #[validate(custom(
226 function = "validate_secret_string",
227 message = "API private key cannot be empty"
228 ))]
229 pub api_private_key: SecretString,
230 #[validate(length(min = 1, message = "Organization ID cannot be empty"))]
231 pub organization_id: String,
232 #[validate(length(min = 1, message = "Private key ID cannot be empty"))]
233 pub private_key_id: String,
234 #[validate(length(min = 1, message = "Public key cannot be empty"))]
235 pub public_key: String,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
240#[validate(schema(function = "validate_cdp_config"))]
241pub struct CdpSignerConfig {
242 #[validate(length(min = 1, message = "API Key ID cannot be empty"))]
243 pub api_key_id: String,
244 #[validate(custom(
245 function = "validate_secret_string",
246 message = "API Key Secret cannot be empty"
247 ))]
248 pub api_key_secret: SecretString,
249 #[validate(custom(
250 function = "validate_secret_string",
251 message = "API Wallet Secret cannot be empty"
252 ))]
253 pub wallet_secret: SecretString,
254 #[validate(length(min = 1, message = "Account address cannot be empty"))]
255 pub account_address: String,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
263pub struct GoogleCloudKmsSignerServiceAccountConfig {
264 #[validate(custom(
265 function = "validate_secret_string",
266 message = "Private key cannot be empty"
267 ))]
268 pub private_key: SecretString,
269 #[validate(custom(
270 function = "validate_secret_string",
271 message = "Private key ID cannot be empty"
272 ))]
273 pub private_key_id: SecretString,
274 #[validate(custom(
275 function = "validate_secret_string",
276 message = "Project ID cannot be empty"
277 ))]
278 pub project_id: SecretString,
279 #[validate(custom(
280 function = "validate_secret_string",
281 message = "Client email cannot be empty"
282 ))]
283 pub client_email: SecretString,
284 #[validate(custom(
285 function = "validate_secret_string",
286 message = "Client ID cannot be empty"
287 ))]
288 pub client_id: SecretString,
289 #[validate(custom(
290 function = "validate_secret_url",
291 message = "Auth URI must be a valid URL"
292 ))]
293 pub auth_uri: SecretString,
294 #[validate(custom(
295 function = "validate_secret_url",
296 message = "Token URI must be a valid URL"
297 ))]
298 pub token_uri: SecretString,
299 #[validate(custom(
300 function = "validate_secret_url",
301 message = "Auth provider x509 cert URL must be a valid URL"
302 ))]
303 pub auth_provider_x509_cert_url: SecretString,
304 #[validate(custom(
305 function = "validate_secret_url",
306 message = "Client x509 cert URL must be a valid URL"
307 ))]
308 pub client_x509_cert_url: SecretString,
309 #[validate(
310 custom(
311 function = "validate_secret_string",
312 message = "Universe domain cannot be empty"
313 ),
314 custom(
315 function = "validate_universe_domain",
316 message = "Universe domain must be a valid Google Cloud KMS domain"
317 )
318 )]
319 pub universe_domain: SecretString,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
327pub struct GoogleCloudKmsSignerKeyConfig {
328 #[validate(custom(
329 function = "validate_secret_string",
330 message = "Location cannot be empty"
331 ))]
332 pub location: SecretString,
333 #[validate(custom(
334 function = "validate_secret_string",
335 message = "Key ring ID cannot be empty"
336 ))]
337 pub key_ring_id: SecretString,
338 #[validate(custom(
339 function = "validate_secret_string",
340 message = "Key ID cannot be empty"
341 ))]
342 pub key_id: SecretString,
343 pub key_version: u32,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
348pub struct GoogleCloudKmsSignerConfig {
349 #[validate(nested)]
350 pub service_account: GoogleCloudKmsSignerServiceAccountConfig,
351 #[validate(nested)]
352 pub key: GoogleCloudKmsSignerKeyConfig,
353}
354
355fn validate_secret_string(secret: &SecretString) -> Result<(), validator::ValidationError> {
357 if secret.to_str().is_empty() {
358 return Err(validator::ValidationError::new("empty_secret"));
359 }
360 Ok(())
361}
362
363fn validate_secret_url(secret: &SecretString) -> Result<(), validator::ValidationError> {
365 secret.as_str(|s| {
366 if s.is_empty() {
367 return Err(validator::ValidationError::new("empty_url"));
368 }
369 reqwest::Url::parse(s).map_err(|_| validator::ValidationError::new("invalid_url"))?;
370 Ok(())
371 })
372}
373
374fn validate_required_optional_secret(
375 value: &Option<SecretString>,
376 field: &str,
377 message: &'static str,
378) -> Result<(), validator::ValidationError> {
379 match value {
380 Some(secret) if !secret.to_str().is_empty() => Ok(()),
381 _ => {
382 let mut error = validator::ValidationError::new("missing_or_empty");
383 error.add_param(std::borrow::Cow::Borrowed("field"), &field);
384 error.message = Some(message.into());
385 Err(error)
386 }
387 }
388}
389
390fn validate_optional_secret_if_present(
391 value: &Option<SecretString>,
392 field: &str,
393 message: &'static str,
394) -> Result<(), validator::ValidationError> {
395 if let Some(secret) = value {
396 if secret.to_str().is_empty() {
397 let mut error = validator::ValidationError::new("empty_secret");
398 error.add_param(std::borrow::Cow::Borrowed("field"), &field);
399 error.message = Some(message.into());
400 return Err(error);
401 }
402 }
403
404 Ok(())
405}
406
407fn validate_azure_key_vault_config(
408 config: &AzureKeyVaultSignerConfig,
409) -> Result<(), validator::ValidationError> {
410 match config.auth_type() {
411 AzureKeyVaultAuthType::ClientSecret => {
412 validate_required_optional_secret(
413 &config.tenant_id,
414 "tenant_id",
415 "Tenant ID cannot be empty",
416 )?;
417 validate_required_optional_secret(
418 &config.client_id,
419 "client_id",
420 "Client ID cannot be empty",
421 )?;
422 validate_required_optional_secret(
423 &config.client_secret,
424 "client_secret",
425 "Client secret cannot be empty",
426 )?;
427 }
428 AzureKeyVaultAuthType::ManagedIdentity => {
429 validate_optional_secret_if_present(
430 &config.client_id,
431 "client_id",
432 "Client ID cannot be empty",
433 )?;
434 }
435 AzureKeyVaultAuthType::WorkloadIdentity => {
436 validate_required_optional_secret(
437 &config.tenant_id,
438 "tenant_id",
439 "Tenant ID cannot be empty",
440 )?;
441 validate_required_optional_secret(
442 &config.client_id,
443 "client_id",
444 "Client ID cannot be empty",
445 )?;
446 validate_optional_secret_if_present(
447 &config.federated_token_file,
448 "federated_token_file",
449 "Federated token file cannot be empty",
450 )?;
451 }
452 }
453
454 Ok(())
455}
456
457const ALLOWED_KMS_DOMAINS: &[&str] = &[
461 "cloudkms.googleapis.com", ];
463
464fn validate_universe_domain(secret: &SecretString) -> Result<(), validator::ValidationError> {
467 let value = secret.to_str();
468 let url = if value.starts_with("http") {
470 value.to_string()
471 } else {
472 format!("https://cloudkms.{}", &*value)
473 };
474
475 let allowed_hosts: Vec<String> = ALLOWED_KMS_DOMAINS.iter().map(|s| s.to_string()).collect();
476
477 validate_safe_url(&url, &allowed_hosts, true).map_err(|e| {
479 let mut err = validator::ValidationError::new("universe_domain_ssrf");
480 err.message = Some(e.into());
481 err
482 })
483}
484
485fn validate_cdp_config(config: &CdpSignerConfig) -> Result<(), validator::ValidationError> {
487 let api_key_valid = config
489 .api_key_secret
490 .as_str(|secret_str| base64_decode(secret_str).is_ok());
491 if !api_key_valid {
492 let mut error = validator::ValidationError::new("invalid_base64_api_key_secret");
493 error.message = Some("API Key Secret is not valid base64".into());
494 return Err(error);
495 }
496
497 let wallet_secret_valid = config
499 .wallet_secret
500 .as_str(|secret_str| base64_decode(secret_str).is_ok());
501 if !wallet_secret_valid {
502 let mut error = validator::ValidationError::new("invalid_base64_wallet_secret");
503 error.message = Some("Wallet Secret is not valid base64".into());
504 return Err(error);
505 }
506
507 let addr = &config.account_address;
508
509 if addr.starts_with("0x") {
511 if addr.len() != 42 {
512 let mut error = validator::ValidationError::new("invalid_evm_address_format");
513 error.message = Some(
514 "EVM account address must be a valid 0x-prefixed 40-character hex string".into(),
515 );
516 return Err(error);
517 }
518
519 if let Some(end) = addr.strip_prefix("0x") {
521 if !end.chars().all(|c| c.is_ascii_hexdigit()) {
522 let mut error = validator::ValidationError::new("invalid_evm_address_hex");
523 error.message = Some("EVM account address contains invalid hex characters".into());
524 return Err(error);
525 }
526 }
527 } else {
528 if Pubkey::from_str(addr).is_err() {
530 let mut error = validator::ValidationError::new("invalid_solana_address");
531 error.message = Some("Invalid Solana account address format".into());
532 return Err(error);
533 }
534 }
535
536 Ok(())
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
541pub enum SignerConfig {
542 Local(LocalSignerConfig),
543 Vault(VaultSignerConfig),
544 VaultTransit(VaultTransitSignerConfig),
545 AwsKms(AwsKmsSignerConfig),
546 AzureKeyVault(AzureKeyVaultSignerConfig),
547 Turnkey(TurnkeySignerConfig),
548 Cdp(CdpSignerConfig),
549 GoogleCloudKms(Box<GoogleCloudKmsSignerConfig>),
550}
551
552impl SignerConfig {
553 pub fn validate(&self) -> Result<(), SignerValidationError> {
555 match self {
556 Self::Local(config) => config.validate(),
557 Self::AwsKms(config) => Validate::validate(config).map_err(|e| {
558 SignerValidationError::InvalidConfig(format!(
559 "AWS KMS validation failed: {}",
560 format_validation_errors(&e)
561 ))
562 }),
563 Self::AzureKeyVault(config) => Validate::validate(config).map_err(|e| {
564 SignerValidationError::InvalidConfig(format!(
565 "Azure Key Vault validation failed: {}",
566 format_validation_errors(&e)
567 ))
568 }),
569 Self::Vault(config) => Validate::validate(config).map_err(|e| {
570 SignerValidationError::InvalidConfig(format!(
571 "Vault validation failed: {}",
572 format_validation_errors(&e)
573 ))
574 }),
575 Self::VaultTransit(config) => Validate::validate(config).map_err(|e| {
576 SignerValidationError::InvalidConfig(format!(
577 "Vault Transit validation failed: {}",
578 format_validation_errors(&e)
579 ))
580 }),
581 Self::Turnkey(config) => Validate::validate(config).map_err(|e| {
582 SignerValidationError::InvalidConfig(format!(
583 "Turnkey validation failed: {}",
584 format_validation_errors(&e)
585 ))
586 }),
587 Self::Cdp(config) => Validate::validate(config).map_err(|e| {
588 SignerValidationError::InvalidConfig(format!(
589 "CDP validation failed: {}",
590 format_validation_errors(&e)
591 ))
592 }),
593 Self::GoogleCloudKms(config) => Validate::validate(config.as_ref()).map_err(|e| {
594 SignerValidationError::InvalidConfig(format!(
595 "Google Cloud KMS validation failed: {}",
596 format_validation_errors(&e)
597 ))
598 }),
599 }
600 }
601
602 pub fn get_local(&self) -> Option<&LocalSignerConfig> {
604 match self {
605 Self::Local(config) => Some(config),
606 _ => None,
607 }
608 }
609
610 pub fn get_aws_kms(&self) -> Option<&AwsKmsSignerConfig> {
612 match self {
613 Self::AwsKms(config) => Some(config),
614 _ => None,
615 }
616 }
617
618 pub fn get_azure_key_vault(&self) -> Option<&AzureKeyVaultSignerConfig> {
620 match self {
621 Self::AzureKeyVault(config) => Some(config),
622 _ => None,
623 }
624 }
625
626 pub fn get_vault(&self) -> Option<&VaultSignerConfig> {
628 match self {
629 Self::Vault(config) => Some(config),
630 _ => None,
631 }
632 }
633
634 pub fn get_vault_transit(&self) -> Option<&VaultTransitSignerConfig> {
636 match self {
637 Self::VaultTransit(config) => Some(config),
638 _ => None,
639 }
640 }
641
642 pub fn get_turnkey(&self) -> Option<&TurnkeySignerConfig> {
644 match self {
645 Self::Turnkey(config) => Some(config),
646 _ => None,
647 }
648 }
649
650 pub fn get_cdp(&self) -> Option<&CdpSignerConfig> {
652 match self {
653 Self::Cdp(config) => Some(config),
654 _ => None,
655 }
656 }
657
658 pub fn get_google_cloud_kms(&self) -> Option<&GoogleCloudKmsSignerConfig> {
660 match self {
661 Self::GoogleCloudKms(config) => Some(config),
662 _ => None,
663 }
664 }
665
666 pub fn get_signer_type(&self) -> SignerType {
668 match self {
669 Self::Local(_) => SignerType::Local,
670 Self::AwsKms(_) => SignerType::AwsKms,
671 Self::AzureKeyVault(_) => SignerType::AzureKeyVault,
672 Self::Vault(_) => SignerType::Vault,
673 Self::VaultTransit(_) => SignerType::VaultTransit,
674 Self::Turnkey(_) => SignerType::Turnkey,
675 Self::Cdp(_) => SignerType::Cdp,
676 Self::GoogleCloudKms(_) => SignerType::GoogleCloudKms,
677 }
678 }
679}
680
681fn format_validation_errors(errors: &validator::ValidationErrors) -> String {
683 let mut messages = Vec::new();
684
685 for (field, field_errors) in errors.field_errors().iter() {
686 let field_msgs: Vec<String> = field_errors
687 .iter()
688 .map(|error| error.message.clone().unwrap_or_default().to_string())
689 .collect();
690 messages.push(format!("{}: {}", field, field_msgs.join(", ")));
691 }
692
693 for (struct_field, kind) in errors.errors().iter() {
694 if let validator::ValidationErrorsKind::Struct(nested) = kind {
695 let nested_msgs = format_validation_errors(nested);
696 messages.push(format!("{struct_field}.{nested_msgs}"));
697 }
698 }
699
700 messages.join("; ")
701}
702
703#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
705pub struct Signer {
706 #[validate(
707 length(min = 1, max = 36, message = "ID must be between 1 and 36 characters"),
708 regex(
709 path = "*ID_REGEX",
710 message = "ID must contain only letters, numbers, dashes and underscores"
711 )
712 )]
713 pub id: String,
714 pub config: SignerConfig,
715}
716
717#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
719#[serde(rename_all = "lowercase")]
720pub enum SignerType {
721 Local,
722 #[serde(rename = "aws_kms")]
723 AwsKms,
724 #[serde(rename = "azure_key_vault")]
725 AzureKeyVault,
726 #[serde(rename = "google_cloud_kms")]
727 GoogleCloudKms,
728 Vault,
729 #[serde(rename = "vault_transit")]
730 VaultTransit,
731 Turnkey,
732 Cdp,
733}
734
735impl Signer {
736 pub fn new(id: String, config: SignerConfig) -> Self {
738 Self { id, config }
739 }
740
741 pub fn signer_type(&self) -> SignerType {
743 self.config.get_signer_type()
744 }
745
746 pub fn validate(&self) -> Result<(), SignerValidationError> {
748 Validate::validate(self).map_err(|validation_errors| {
750 for (field, errors) in validation_errors.field_errors() {
753 if let Some(error) = errors.first() {
754 let field_str = field.as_ref();
755 return match (field_str, error.code.as_ref()) {
756 ("id", "length") => SignerValidationError::InvalidIdFormat,
757 ("id", "regex") => SignerValidationError::InvalidIdFormat,
758 _ => SignerValidationError::InvalidIdFormat, };
760 }
761 }
762 SignerValidationError::InvalidIdFormat
764 })?;
765
766 self.config.validate()?;
768
769 Ok(())
770 }
771}
772
773#[derive(Debug, thiserror::Error)]
775pub enum SignerValidationError {
776 #[error("Signer ID cannot be empty")]
777 EmptyId,
778 #[error("Signer ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long")]
779 InvalidIdFormat,
780 #[error("Invalid signer configuration: {0}")]
781 InvalidConfig(String),
782}
783
784impl From<SignerValidationError> for crate::models::ApiError {
786 fn from(error: SignerValidationError) -> Self {
787 use crate::models::ApiError;
788
789 ApiError::BadRequest(match error {
790 SignerValidationError::EmptyId => "ID cannot be empty".to_string(),
791 SignerValidationError::InvalidIdFormat => {
792 "ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long".to_string()
793 }
794 SignerValidationError::InvalidConfig(msg) => format!("Invalid signer configuration: {msg}"),
795 })
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802
803 #[test]
804 fn test_valid_local_signer() {
805 let config = SignerConfig::Local(LocalSignerConfig {
806 raw_key: SecretVec::new(32, |v| v.fill(1)),
807 });
808
809 let signer = Signer::new("valid-id".to_string(), config);
810
811 assert!(signer.validate().is_ok());
812 assert_eq!(signer.signer_type(), SignerType::Local);
813 }
814
815 #[test]
816 fn test_valid_aws_kms_signer() {
817 let config = SignerConfig::AwsKms(AwsKmsSignerConfig {
818 region: Some("us-east-1".to_string()),
819 key_id: "test-key-id".to_string(),
820 });
821
822 let signer = Signer::new("aws-signer".to_string(), config);
823
824 assert!(signer.validate().is_ok());
825 assert_eq!(signer.signer_type(), SignerType::AwsKms);
826 }
827
828 #[test]
829 fn test_valid_azure_key_vault_client_secret_signer() {
830 let config = SignerConfig::AzureKeyVault(AzureKeyVaultSignerConfig {
831 auth_type: Some(AzureKeyVaultAuthType::ClientSecret),
832 tenant_id: Some(SecretString::new("tenant-id")),
833 client_id: Some(SecretString::new("client-id")),
834 client_secret: Some(SecretString::new("client-secret")),
835 federated_token_file: None,
836 vault_url: SecretString::new("https://example.vault.azure.net"),
837 key_name: SecretString::new("test-key"),
838 key_version: None,
839 });
840
841 let signer = Signer::new("azure-client-secret".to_string(), config);
842 assert!(signer.validate().is_ok());
843 }
844
845 #[test]
846 fn test_valid_azure_key_vault_managed_identity_signer() {
847 let config = SignerConfig::AzureKeyVault(AzureKeyVaultSignerConfig {
848 auth_type: Some(AzureKeyVaultAuthType::ManagedIdentity),
849 tenant_id: None,
850 client_id: Some(SecretString::new("managed-client-id")),
851 client_secret: None,
852 federated_token_file: None,
853 vault_url: SecretString::new("https://example.vault.azure.net"),
854 key_name: SecretString::new("test-key"),
855 key_version: None,
856 });
857
858 let signer = Signer::new("azure-managed-identity".to_string(), config);
859 assert!(signer.validate().is_ok());
860 }
861
862 #[test]
863 fn test_invalid_azure_key_vault_workload_identity_without_tenant() {
864 let config = SignerConfig::AzureKeyVault(AzureKeyVaultSignerConfig {
865 auth_type: Some(AzureKeyVaultAuthType::WorkloadIdentity),
866 tenant_id: None,
867 client_id: Some(SecretString::new("workload-client-id")),
868 client_secret: None,
869 federated_token_file: None,
870 vault_url: SecretString::new("https://example.vault.azure.net"),
871 key_name: SecretString::new("test-key"),
872 key_version: None,
873 });
874
875 let signer = Signer::new("azure-workload-identity".to_string(), config);
876 let result = signer.validate();
877 assert!(result.is_err());
878 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
879 assert!(msg.contains("Tenant ID cannot be empty"));
880 } else {
881 panic!("Expected InvalidConfig error for missing tenant ID");
882 }
883 }
884
885 #[test]
886 fn test_empty_id() {
887 let config = SignerConfig::Local(LocalSignerConfig {
888 raw_key: SecretVec::new(32, |v| v.fill(1)), });
890
891 let signer = Signer::new("".to_string(), config);
892
893 assert!(matches!(
894 signer.validate(),
895 Err(SignerValidationError::InvalidIdFormat)
896 ));
897 }
898
899 #[test]
900 fn test_id_too_long() {
901 let config = SignerConfig::Local(LocalSignerConfig {
902 raw_key: SecretVec::new(32, |v| v.fill(1)), });
904
905 let signer = Signer::new("a".repeat(37), config);
906
907 assert!(matches!(
908 signer.validate(),
909 Err(SignerValidationError::InvalidIdFormat)
910 ));
911 }
912
913 #[test]
914 fn test_invalid_id_format() {
915 let config = SignerConfig::Local(LocalSignerConfig {
916 raw_key: SecretVec::new(32, |v| v.fill(1)), });
918
919 let signer = Signer::new("invalid@id".to_string(), config);
920
921 assert!(matches!(
922 signer.validate(),
923 Err(SignerValidationError::InvalidIdFormat)
924 ));
925 }
926
927 #[test]
928 fn test_local_signer_invalid_key_length() {
929 let config = SignerConfig::Local(LocalSignerConfig {
930 raw_key: SecretVec::new(16, |v| v.fill(1)), });
932
933 let signer = Signer::new("valid-id".to_string(), config);
934
935 let result = signer.validate();
936 assert!(result.is_err());
937 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
938 assert!(msg.contains("Raw key must be exactly 32 bytes"));
939 assert!(msg.contains("got 16 bytes"));
940 } else {
941 panic!("Expected InvalidConfig error for invalid key length");
942 }
943 }
944
945 #[test]
946 fn test_local_signer_all_zero_key() {
947 let config = SignerConfig::Local(LocalSignerConfig {
948 raw_key: SecretVec::new(32, |v| v.fill(0)), });
950
951 let signer = Signer::new("valid-id".to_string(), config);
952
953 let result = signer.validate();
954 assert!(result.is_err());
955 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
956 assert_eq!(msg, "Raw key cannot be all zeros");
957 } else {
958 panic!("Expected InvalidConfig error for all-zero key");
959 }
960 }
961
962 #[test]
963 fn test_local_signer_valid_key() {
964 let config = SignerConfig::Local(LocalSignerConfig {
965 raw_key: SecretVec::new(32, |v| v.fill(1)), });
967
968 let signer = Signer::new("valid-id".to_string(), config);
969
970 assert!(signer.validate().is_ok());
971 }
972
973 #[test]
974 fn test_signer_type_serialization() {
975 use serde_json::{from_str, to_string};
976
977 assert_eq!(to_string(&SignerType::Local).unwrap(), "\"local\"");
978 assert_eq!(to_string(&SignerType::AwsKms).unwrap(), "\"aws_kms\"");
979 assert_eq!(
980 to_string(&SignerType::GoogleCloudKms).unwrap(),
981 "\"google_cloud_kms\""
982 );
983 assert_eq!(
984 to_string(&SignerType::VaultTransit).unwrap(),
985 "\"vault_transit\""
986 );
987
988 assert_eq!(
989 from_str::<SignerType>("\"local\"").unwrap(),
990 SignerType::Local
991 );
992 assert_eq!(
993 from_str::<SignerType>("\"aws_kms\"").unwrap(),
994 SignerType::AwsKms
995 );
996 }
997
998 #[test]
999 fn test_config_accessor_methods() {
1000 let local_config = LocalSignerConfig {
1002 raw_key: SecretVec::new(32, |v| v.fill(1)),
1003 };
1004 let config = SignerConfig::Local(local_config);
1005 assert!(config.get_local().is_some());
1006 assert!(config.get_aws_kms().is_none());
1007
1008 let aws_config = AwsKmsSignerConfig {
1010 region: Some("us-east-1".to_string()),
1011 key_id: "test-key".to_string(),
1012 };
1013 let config = SignerConfig::AwsKms(aws_config);
1014 assert!(config.get_aws_kms().is_some());
1015 assert!(config.get_local().is_none());
1016 }
1017
1018 #[test]
1019 fn test_error_conversion_to_api_error() {
1020 let error = SignerValidationError::InvalidIdFormat;
1021 let api_error: crate::models::ApiError = error.into();
1022
1023 if let crate::models::ApiError::BadRequest(msg) = api_error {
1024 assert!(msg.contains("ID must contain only letters, numbers, dashes and underscores"));
1025 } else {
1026 panic!("Expected BadRequest error");
1027 }
1028 }
1029
1030 #[test]
1031 fn test_valid_vault_signer() {
1032 let config = SignerConfig::Vault(VaultSignerConfig {
1033 address: "https://vault.example.com".to_string(),
1034 namespace: Some("test".to_string()),
1035 role_id: SecretString::new("role-id"),
1036 secret_id: SecretString::new("secret-id"),
1037 key_name: "test-key".to_string(),
1038 mount_point: None,
1039 });
1040
1041 let signer = Signer::new("vault-signer".to_string(), config);
1042 assert!(signer.validate().is_ok());
1043 assert_eq!(signer.signer_type(), SignerType::Vault);
1044 }
1045
1046 #[test]
1047 fn test_invalid_vault_signer_url() {
1048 let config = SignerConfig::Vault(VaultSignerConfig {
1049 address: "not-a-url".to_string(),
1050 namespace: Some("test".to_string()),
1051 role_id: SecretString::new("role-id"),
1052 secret_id: SecretString::new("secret-id"),
1053 key_name: "test-key".to_string(),
1054 mount_point: None,
1055 });
1056
1057 let signer = Signer::new("vault-signer".to_string(), config);
1058 let result = signer.validate();
1059 assert!(result.is_err());
1060 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1061 assert!(msg.contains("Address must be a valid URL"));
1062 } else {
1063 panic!("Expected InvalidConfig error for invalid URL");
1064 }
1065 }
1066
1067 #[test]
1068 fn test_valid_google_cloud_kms_signer() {
1069 let config = SignerConfig::GoogleCloudKms(Box::new(GoogleCloudKmsSignerConfig {
1070 service_account: GoogleCloudKmsSignerServiceAccountConfig {
1071 private_key: SecretString::new("private-key"),
1072 private_key_id: SecretString::new("key-id"),
1073 project_id: SecretString::new("project"),
1074 client_email: SecretString::new("client@example.com"),
1075 client_id: SecretString::new("client-id"),
1076 auth_uri: SecretString::new("https://accounts.google.com/o/oauth2/auth"),
1077 token_uri: SecretString::new("https://oauth2.googleapis.com/token"),
1078 auth_provider_x509_cert_url: SecretString::new(
1079 "https://www.googleapis.com/oauth2/v1/certs",
1080 ),
1081 client_x509_cert_url: SecretString::new(
1082 "https://www.googleapis.com/robot/v1/metadata/x509/test",
1083 ),
1084 universe_domain: SecretString::new("googleapis.com"),
1085 },
1086 key: GoogleCloudKmsSignerKeyConfig {
1087 location: SecretString::new("us-central1"),
1088 key_ring_id: SecretString::new("test-ring"),
1089 key_id: SecretString::new("test-key"),
1090 key_version: 1,
1091 },
1092 }));
1093
1094 let signer = Signer::new("gcp-kms-signer".to_string(), config);
1095 assert!(signer.validate().is_ok());
1096 assert_eq!(signer.signer_type(), SignerType::GoogleCloudKms);
1097 }
1098
1099 #[test]
1100 fn test_invalid_google_cloud_kms_urls() {
1101 let config = SignerConfig::GoogleCloudKms(Box::new(GoogleCloudKmsSignerConfig {
1102 service_account: GoogleCloudKmsSignerServiceAccountConfig {
1103 private_key: SecretString::new("private-key"),
1104 private_key_id: SecretString::new("key-id"),
1105 project_id: SecretString::new("project"),
1106 client_email: SecretString::new("client@example.com"),
1107 client_id: SecretString::new("client-id"),
1108 auth_uri: SecretString::new("not-a-url"), token_uri: SecretString::new("https://oauth2.googleapis.com/token"),
1110 auth_provider_x509_cert_url: SecretString::new(
1111 "https://www.googleapis.com/oauth2/v1/certs",
1112 ),
1113 client_x509_cert_url: SecretString::new(
1114 "https://www.googleapis.com/robot/v1/metadata/x509/test",
1115 ),
1116 universe_domain: SecretString::new("googleapis.com"),
1117 },
1118 key: GoogleCloudKmsSignerKeyConfig {
1119 location: SecretString::new("us-central1"),
1120 key_ring_id: SecretString::new("test-ring"),
1121 key_id: SecretString::new("test-key"),
1122 key_version: 1,
1123 },
1124 }));
1125
1126 let signer = Signer::new("gcp-kms-signer".to_string(), config);
1127 let result = signer.validate();
1128 assert!(result.is_err());
1129 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1130 assert!(msg.contains("Auth URI must be a valid URL"));
1131 } else {
1132 panic!("Expected InvalidConfig error for invalid URL");
1133 }
1134 }
1135
1136 #[test]
1137 fn test_secret_string_validation() {
1138 let result = validate_secret_string(&SecretString::new(""));
1140 if let Err(e) = result {
1141 assert_eq!(e.code, "empty_secret");
1142 } else {
1143 panic!("Expected validation error for empty secret");
1144 }
1145
1146 let result = validate_secret_string(&SecretString::new("secret"));
1148 assert!(result.is_ok());
1149 }
1150
1151 #[test]
1152 fn test_validation_error_formatting() {
1153 let invalid_config = GoogleCloudKmsSignerConfig {
1155 service_account: GoogleCloudKmsSignerServiceAccountConfig {
1156 private_key: SecretString::new(""), private_key_id: SecretString::new("key-id"),
1158 project_id: SecretString::new("project"),
1159 client_email: SecretString::new("client@example.com"),
1160 client_id: SecretString::new(""), auth_uri: SecretString::new("not-a-url"), token_uri: SecretString::new("https://oauth2.googleapis.com/token"),
1163 auth_provider_x509_cert_url: SecretString::new(
1164 "https://www.googleapis.com/oauth2/v1/certs",
1165 ),
1166 client_x509_cert_url: SecretString::new(
1167 "https://www.googleapis.com/robot/v1/metadata/x509/test",
1168 ),
1169 universe_domain: SecretString::new("googleapis.com"),
1170 },
1171 key: GoogleCloudKmsSignerKeyConfig {
1172 location: SecretString::new("us-central1"),
1173 key_ring_id: SecretString::new(""), key_id: SecretString::new("test-key"),
1175 key_version: 1,
1176 },
1177 };
1178
1179 let errors = invalid_config.validate().unwrap_err();
1180
1181 let formatted = format_validation_errors(&errors);
1183
1184 println!("formatted: {formatted}");
1185
1186 assert!(formatted.contains("client_id: Client ID cannot be empty"));
1188 assert!(formatted.contains("private_key: Private key cannot be empty"));
1189 assert!(formatted.contains("auth_uri: Auth URI must be a valid URL"));
1190 assert!(formatted.contains("key_ring_id: Key ring ID cannot be empty"));
1191 }
1192
1193 #[test]
1194 fn test_config_type_getters() {
1195 let vault_config = VaultSignerConfig {
1197 address: "https://vault.example.com".to_string(),
1198 namespace: None,
1199 role_id: SecretString::new("role"),
1200 secret_id: SecretString::new("secret"),
1201 key_name: "key".to_string(),
1202 mount_point: None,
1203 };
1204 let config = SignerConfig::Vault(vault_config);
1205 assert!(config.get_vault().is_some());
1206
1207 let vault_transit_config = VaultTransitSignerConfig {
1209 key_name: "key".to_string(),
1210 address: "https://vault.example.com".to_string(),
1211 namespace: None,
1212 role_id: SecretString::new("role"),
1213 secret_id: SecretString::new("secret"),
1214 pubkey: "pubkey".to_string(),
1215 mount_point: None,
1216 };
1217 let config = SignerConfig::VaultTransit(vault_transit_config);
1218 assert!(config.get_vault_transit().is_some());
1219 assert!(config.get_turnkey().is_none());
1220
1221 let turnkey_config = TurnkeySignerConfig {
1223 api_public_key: "public".to_string(),
1224 api_private_key: SecretString::new("private"),
1225 organization_id: "org".to_string(),
1226 private_key_id: "key-id".to_string(),
1227 public_key: "pubkey".to_string(),
1228 };
1229 let config = SignerConfig::Turnkey(turnkey_config);
1230 assert!(config.get_turnkey().is_some());
1231 assert!(config.get_google_cloud_kms().is_none());
1232
1233 let gcp_config = GoogleCloudKmsSignerConfig {
1235 service_account: GoogleCloudKmsSignerServiceAccountConfig {
1236 private_key: SecretString::new("private-key"),
1237 private_key_id: SecretString::new("key-id"),
1238 project_id: SecretString::new("project"),
1239 client_email: SecretString::new("client@example.com"),
1240 client_id: SecretString::new("client-id"),
1241 auth_uri: SecretString::new("https://accounts.google.com/o/oauth2/auth"),
1242 token_uri: SecretString::new("https://oauth2.googleapis.com/token"),
1243 auth_provider_x509_cert_url: SecretString::new(
1244 "https://www.googleapis.com/oauth2/v1/certs",
1245 ),
1246 client_x509_cert_url: SecretString::new(
1247 "https://www.googleapis.com/robot/v1/metadata/x509/test",
1248 ),
1249 universe_domain: SecretString::new("googleapis.com"),
1250 },
1251 key: GoogleCloudKmsSignerKeyConfig {
1252 location: SecretString::new("us-central1"),
1253 key_ring_id: SecretString::new("test-ring"),
1254 key_id: SecretString::new("test-key"),
1255 key_version: 1,
1256 },
1257 };
1258 let config = SignerConfig::GoogleCloudKms(Box::new(gcp_config));
1259 assert!(config.get_google_cloud_kms().is_some());
1260 assert!(config.get_local().is_none());
1261 }
1262
1263 #[test]
1264 fn test_valid_cdp_signer_with_evm_address() {
1265 let config = CdpSignerConfig {
1266 api_key_id: "test-api-key".to_string(),
1267 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
1270 };
1271 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1272 assert!(signer.validate().is_ok());
1273 assert_eq!(signer.signer_type(), SignerType::Cdp);
1274 }
1275
1276 #[test]
1277 fn test_valid_cdp_signer_with_solana_address() {
1278 let config = CdpSignerConfig {
1279 api_key_id: "test-api-key".to_string(),
1280 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string(),
1283 };
1284 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1285 assert!(signer.validate().is_ok());
1286 assert_eq!(signer.signer_type(), SignerType::Cdp);
1287 }
1288
1289 #[test]
1290 fn test_invalid_cdp_signer_empty_address() {
1291 let config = CdpSignerConfig {
1292 api_key_id: "test-api-key".to_string(),
1293 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "".to_string(),
1296 };
1297 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1298 let result = signer.validate();
1299 assert!(result.is_err());
1300 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1301 assert!(msg.contains("Account address cannot be empty"));
1302 } else {
1303 panic!("Expected InvalidConfig error for empty address");
1304 }
1305 }
1306
1307 #[test]
1308 fn test_invalid_cdp_signer_bad_evm_address() {
1309 let config = CdpSignerConfig {
1310 api_key_id: "test-api-key".to_string(),
1311 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "0xinvalid-address".to_string(),
1314 };
1315 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1316 let result = signer.validate();
1317 assert!(result.is_err());
1318 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1319 assert!(msg.contains("EVM account address must be a valid 0x-prefixed"));
1320 } else {
1321 panic!("Expected InvalidConfig error for bad EVM address");
1322 }
1323 }
1324
1325 #[test]
1326 fn test_invalid_cdp_signer_bad_solana_address() {
1327 let config = CdpSignerConfig {
1328 api_key_id: "test-api-key".to_string(),
1329 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "invalid".to_string(),
1332 };
1333 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1334 let result = signer.validate();
1335 assert!(result.is_err());
1336 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1337 assert!(msg.contains("Invalid Solana account address format"));
1338 } else {
1339 panic!("Expected InvalidConfig error for bad Solana address");
1340 }
1341 }
1342
1343 #[test]
1344 fn test_invalid_cdp_signer_evm_address_wrong_format() {
1345 let config = CdpSignerConfig {
1346 api_key_id: "test-api-key".to_string(),
1347 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44".to_string(), };
1351 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1352 let result = signer.validate();
1353 assert!(result.is_err());
1354 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1355 assert!(msg.contains("EVM account address must be a valid 0x-prefixed"));
1356 } else {
1357 panic!("Expected InvalidConfig error for wrong EVM address format");
1358 }
1359 }
1360
1361 #[test]
1362 fn test_invalid_cdp_signer_solana_address_wrong_charset() {
1363 let config = CdpSignerConfig {
1364 api_key_id: "test-api-key".to_string(),
1365 api_key_secret: SecretString::new("c2VjcmV0"), wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm0".to_string(), };
1369 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1370 let result = signer.validate();
1371 assert!(result.is_err());
1372 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1373 assert!(msg.contains("Invalid Solana account address format"));
1374 } else {
1375 panic!("Expected InvalidConfig error for wrong Solana address charset");
1376 }
1377 }
1378
1379 #[test]
1380 fn test_invalid_cdp_signer_invalid_base64_api_key_secret() {
1381 let config = CdpSignerConfig {
1382 api_key_id: "test-api-key".to_string(),
1383 api_key_secret: SecretString::new("invalid-base64!@#"), wallet_secret: SecretString::new("dGVzdC13YWxsZXQtc2VjcmV0"), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
1386 };
1387 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1388 let result = signer.validate();
1389 assert!(result.is_err());
1390 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1391 assert!(msg.contains("API Key Secret is not valid base64"));
1392 } else {
1393 panic!("Expected InvalidConfig error for invalid base64 API key secret");
1394 }
1395 }
1396
1397 #[test]
1398 fn test_invalid_cdp_signer_invalid_base64_wallet_secret() {
1399 let config = CdpSignerConfig {
1400 api_key_id: "test-api-key".to_string(),
1401 api_key_secret: SecretString::new("dGVzdC1hcGkta2V5LXNlY3JldA=="), wallet_secret: SecretString::new("invalid-base64!@#"), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
1404 };
1405 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1406 let result = signer.validate();
1407 assert!(result.is_err());
1408 if let Err(SignerValidationError::InvalidConfig(msg)) = result {
1409 assert!(msg.contains("Wallet Secret is not valid base64"));
1410 } else {
1411 panic!("Expected InvalidConfig error for invalid base64 wallet secret");
1412 }
1413 }
1414
1415 #[test]
1416 fn test_valid_cdp_signer_with_valid_base64_secrets() {
1417 let config = CdpSignerConfig {
1418 api_key_id: "test-api-key".to_string(),
1419 api_key_secret: SecretString::new("dGVzdC1hcGkta2V5LXNlY3JldA=="), wallet_secret: SecretString::new("dGVzdC13YWxsZXQtc2VjcmV0"), account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
1422 };
1423 let signer = Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(config));
1424 let result = signer.validate();
1425 assert!(result.is_ok());
1426 assert_eq!(signer.signer_type(), SignerType::Cdp);
1427 }
1428
1429 #[test]
1430 fn test_validate_universe_domain_valid_default() {
1431 let result = validate_universe_domain(&SecretString::new("googleapis.com"));
1433 assert!(result.is_ok());
1434 }
1435
1436 #[test]
1437 fn test_validate_universe_domain_valid_explicit_https() {
1438 let result =
1440 validate_universe_domain(&SecretString::new("https://cloudkms.googleapis.com"));
1441 assert!(result.is_ok());
1442 }
1443
1444 #[test]
1445 fn test_validate_universe_domain_invalid_aws_metadata() {
1446 let result = validate_universe_domain(&SecretString::new("http://169.254.169.254"));
1448 assert!(result.is_err());
1449 if let Err(e) = result {
1450 assert_eq!(e.code, "universe_domain_ssrf");
1451 }
1452 }
1453
1454 #[test]
1455 fn test_validate_universe_domain_invalid_gcp_metadata() {
1456 let result =
1458 validate_universe_domain(&SecretString::new("http://metadata.google.internal"));
1459 assert!(result.is_err());
1460 if let Err(e) = result {
1461 assert_eq!(e.code, "universe_domain_ssrf");
1462 }
1463 }
1464
1465 #[test]
1466 fn test_validate_universe_domain_invalid_localhost() {
1467 let result = validate_universe_domain(&SecretString::new("http://localhost:8080"));
1469 assert!(result.is_err());
1470 if let Err(e) = result {
1471 assert_eq!(e.code, "universe_domain_ssrf");
1472 }
1473 }
1474
1475 #[test]
1476 fn test_validate_universe_domain_invalid_private_ip() {
1477 let result = validate_universe_domain(&SecretString::new("http://192.168.1.1"));
1479 assert!(result.is_err());
1480 if let Err(e) = result {
1481 assert_eq!(e.code, "universe_domain_ssrf");
1482 }
1483
1484 let result = validate_universe_domain(&SecretString::new("http://10.0.0.1"));
1485 assert!(result.is_err());
1486 if let Err(e) = result {
1487 assert_eq!(e.code, "universe_domain_ssrf");
1488 }
1489 }
1490
1491 #[test]
1492 fn test_validate_universe_domain_rejects_non_allowlisted_domains() {
1493 let result = validate_universe_domain(&SecretString::new("https://evil.com"));
1496 assert!(result.is_err());
1497 if let Err(e) = result {
1498 assert_eq!(e.code, "universe_domain_ssrf");
1499 }
1500
1501 let result = validate_universe_domain(&SecretString::new(
1503 "https://cloudkms.googleapis.com.evil.com",
1504 ));
1505 assert!(result.is_err());
1506 if let Err(e) = result {
1507 assert_eq!(e.code, "universe_domain_ssrf");
1508 }
1509
1510 let result =
1512 validate_universe_domain(&SecretString::new("https://cloudkms.googleapis.org"));
1513 assert!(result.is_err());
1514 if let Err(e) = result {
1515 assert_eq!(e.code, "universe_domain_ssrf");
1516 }
1517
1518 let result = validate_universe_domain(&SecretString::new("example.com"));
1520 assert!(result.is_err());
1521 if let Err(e) = result {
1522 assert_eq!(e.code, "universe_domain_ssrf");
1523 }
1524 }
1525}