openzeppelin_relayer/models/signer/
mod.rs

1//! Core signer domain model and business logic.
2//!
3//! This module provides the central `Signer` type that represents signers
4//! throughout the relayer system, including:
5//!
6//! - **Domain Model**: Core `Signer` struct with validation and configuration
7//! - **Business Logic**: Update operations and validation rules
8//! - **Error Handling**: Comprehensive validation error types
9//! - **Interoperability**: Conversions between API, config, and repository representations
10//!
11//! The signer model supports multiple signer types including local keys, AWS KMS,
12//! Google Cloud KMS, Vault, and Turnkey service integrations.
13
14mod 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
43/// Helper function to serialize secrets as redacted
44fn 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/// Local signer configuration for storing private keys
52#[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    /// Validates the raw key for cryptographic requirements
60    pub fn validate(&self) -> Result<(), SignerValidationError> {
61        let key_bytes = self.raw_key.borrow();
62
63        // Check key length - must be exactly 32 bytes for crypto operations
64        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        // Check if key is all zeros (cryptographically invalid)
72        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            // Return a zero-filled SecretVec when deserializing redacted data
95            SecretVec::zero(32)
96        } else {
97            // For actual data, assume it's the raw bytes represented as a string
98            // In practice, this would come from proper key loading
99            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/// AWS KMS signer configuration
109/// The configuration supports:
110/// - AWS Region (aws_region) - important for region-specific key
111/// - KMS Key identification (key_id)
112///
113/// The AWS authentication is carried out
114/// through recommended credential providers as outlined in
115/// https://docs.aws.amazon.com/sdk-for-rust/latest/dg/credproviders.html
116///
117/// Supports:
118/// - EVM networks using secp256k1 (ECDSA_SHA_256)
119/// - Solana using Ed25519 (ED25519_SHA_512)
120/// - Stellar using Ed25519 (ED25519_SHA_512)
121///
122/// Note: Ed25519 support was added to AWS KMS in November 2025.
123/// See: https://aws.amazon.com/about-aws/whats-new/2025/11/aws-kms-edwards-curve-digital-signature-algorithm/
124#[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/// Vault signer configuration
133#[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/// Vault Transit signer configuration
154#[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/// Turnkey signer configuration
177#[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/// CDP signer configuration
195#[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/// Google Cloud KMS service account configuration
215///
216/// All fields are stored as SecretString to ensure they are encrypted at rest
217/// in Redis.
218#[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/// Google Cloud KMS key configuration
279///
280/// All string fields are stored as SecretString to ensure they are encrypted
281/// at rest in Redis, preventing attackers from modifying key identifiers.
282#[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/// Google Cloud KMS signer configuration
303#[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
311/// Custom validator for SecretString
312fn 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
319/// Custom validator for SecretString that must contain a valid URL
320fn 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
330/// Allowed Google Cloud KMS universe domains
331/// These are the legitimate Google Cloud domains where KMS services can be hosted.
332/// See: https://cloud.google.com/kms/docs/reference/rest
333const ALLOWED_KMS_DOMAINS: &[&str] = &[
334    "cloudkms.googleapis.com", // Standard Google Cloud
335];
336
337/// Custom validator for Google Cloud KMS universe_domain to prevent SSRF attacks.
338/// Uses an allowlist approach - only explicitly approved Google Cloud domains are permitted.
339fn validate_universe_domain(secret: &SecretString) -> Result<(), validator::ValidationError> {
340    let value = secret.to_str();
341    // Construct the URL exactly as get_base_url() does in the service
342    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    // Only permit known Google Cloud KMS domains
351    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
358/// Custom validator for CDP signer configuration
359fn validate_cdp_config(config: &CdpSignerConfig) -> Result<(), validator::ValidationError> {
360    // Validate api_key_secret is valid base64
361    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    // Validate wallet_secret is valid base64
371    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    // Check if it's an EVM address (0x-prefixed hex)
383    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        // Check if the hex part is valid
393        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        // Assume it's a Solana address - validate using Pubkey::from_str
402        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/// Domain signer configuration enum containing all supported signer types
413#[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    /// Validates the configuration using the appropriate validator
426    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    /// Get local signer config if this is a local signer
469    pub fn get_local(&self) -> Option<&LocalSignerConfig> {
470        match self {
471            Self::Local(config) => Some(config),
472            _ => None,
473        }
474    }
475
476    /// Get AWS KMS signer config if this is an AWS KMS signer
477    pub fn get_aws_kms(&self) -> Option<&AwsKmsSignerConfig> {
478        match self {
479            Self::AwsKms(config) => Some(config),
480            _ => None,
481        }
482    }
483
484    /// Get Vault signer config if this is a Vault signer
485    pub fn get_vault(&self) -> Option<&VaultSignerConfig> {
486        match self {
487            Self::Vault(config) => Some(config),
488            _ => None,
489        }
490    }
491
492    /// Get Vault Transit signer config if this is a Vault Transit signer
493    pub fn get_vault_transit(&self) -> Option<&VaultTransitSignerConfig> {
494        match self {
495            Self::VaultTransit(config) => Some(config),
496            _ => None,
497        }
498    }
499
500    /// Get Turnkey signer config if this is a Turnkey signer
501    pub fn get_turnkey(&self) -> Option<&TurnkeySignerConfig> {
502        match self {
503            Self::Turnkey(config) => Some(config),
504            _ => None,
505        }
506    }
507
508    /// Get CDP signer config if this is a CDP signer
509    pub fn get_cdp(&self) -> Option<&CdpSignerConfig> {
510        match self {
511            Self::Cdp(config) => Some(config),
512            _ => None,
513        }
514    }
515
516    /// Get Google Cloud KMS signer config if this is a Google Cloud KMS signer
517    pub fn get_google_cloud_kms(&self) -> Option<&GoogleCloudKmsSignerConfig> {
518        match self {
519            Self::GoogleCloudKms(config) => Some(config),
520            _ => None,
521        }
522    }
523
524    /// Get the signer type from the configuration
525    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
538/// Helper function to format validation errors
539fn 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/// Core signer domain model containing both metadata and configuration
561#[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/// Signer type enum used for validation and API responses
575#[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    /// Creates a new signer with configuration
592    pub fn new(id: String, config: SignerConfig) -> Self {
593        Self { id, config }
594    }
595
596    /// Gets the signer type from the configuration
597    pub fn signer_type(&self) -> SignerType {
598        self.config.get_signer_type()
599    }
600
601    /// Validates the signer using both struct validation and config validation
602    pub fn validate(&self) -> Result<(), SignerValidationError> {
603        // First validate struct-level constraints (ID format, etc.)
604        Validate::validate(self).map_err(|validation_errors| {
605            // Convert validator errors to our custom error type
606            // Return the first error for simplicity
607            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, // fallback
614                    };
615                }
616            }
617            // Fallback error
618            SignerValidationError::InvalidIdFormat
619        })?;
620
621        // Then validate the configuration
622        self.config.validate()?;
623
624        Ok(())
625    }
626}
627
628/// Validation errors for signers
629#[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
639/// Centralized conversion from SignerValidationError to ApiError
640impl 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)), // Use valid non-zero key
687        });
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)), // Use valid non-zero key
701        });
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)), // Use valid non-zero key
715        });
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)), // Invalid length: 16 bytes instead of 32
729        });
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)), // Invalid: all zeros
747        });
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)), // Valid: 32 bytes, non-zero
764        });
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        // Test Local config accessor
799        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        // Test AWS KMS config accessor
807        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"), // Invalid URL
907                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        // Test empty secret
937        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        // Test valid secret
945        let result = validate_secret_string(&SecretString::new("secret"));
946        assert!(result.is_ok());
947    }
948
949    #[test]
950    fn test_validation_error_formatting() {
951        // Create an invalid config to trigger multiple nested validation errors
952        let invalid_config = GoogleCloudKmsSignerConfig {
953            service_account: GoogleCloudKmsSignerServiceAccountConfig {
954                private_key: SecretString::new(""), // Invalid: empty
955                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(""), // Invalid: empty
959                auth_uri: SecretString::new("not-a-url"), // Invalid: not a URL
960                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(""), // Invalid: empty
972                key_id: SecretString::new("test-key"),
973                key_version: 1,
974            },
975        };
976
977        let errors = invalid_config.validate().unwrap_err();
978
979        // Format the errors using the helper function
980        let formatted = format_validation_errors(&errors);
981
982        println!("formatted: {formatted}");
983
984        // Check that messages from nested fields are correctly formatted
985        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        // Test Vault config getter
994        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        // Test VaultTransit config getter
1006        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        // Test Turnkey config getter
1020        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        // Test Google Cloud KMS config getter
1032        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"), // Valid base64: "secret"
1066            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1067            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"), // Valid base64: "secret"
1079            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1080            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"), // Valid base64: "secret"
1092            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1093            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"), // Valid base64: "secret"
1110            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1111            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"), // Valid base64: "secret"
1128            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1129            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"), // Valid base64: "secret"
1146            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1147            account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44".to_string(), // Too short
1148        };
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"), // Valid base64: "secret"
1164            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1165            account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm0".to_string(), // Contains '0' which is invalid in Base58
1166        };
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!@#"), // Invalid base64
1182            wallet_secret: SecretString::new("dGVzdC13YWxsZXQtc2VjcmV0"), // Valid base64: "test-wallet-secret"
1183            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=="), // Valid base64: "test-api-key-secret"
1200            wallet_secret: SecretString::new("invalid-base64!@#"),             // Invalid base64
1201            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=="), // Valid base64: "test-api-key-secret"
1218            wallet_secret: SecretString::new("dGVzdC13YWxsZXQtc2VjcmV0"), // Valid base64: "test-wallet-secret"
1219            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        // Valid: default Google domain
1230        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        // Valid: explicit HTTPS URL
1237        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        // Invalid: AWS metadata endpoint
1245        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        // Invalid: GCP metadata endpoint
1255        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        // Invalid: localhost
1266        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        // Invalid: private IP addresses
1276        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        // Invalid: arbitrary public domains not in the allowlist
1292        // This tests the allowlist approach - even valid public URLs are rejected if not in allowlist
1293        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        // Invalid: attacker-controlled domain with "googleapis" in subdomain
1300        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        // Invalid: similar-looking domain
1309        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        // Invalid: using domain value directly that constructs non-allowlisted URL
1317        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}