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, 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
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/// Azure Key Vault authentication mode
133#[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/// Azure Key Vault signer configuration
148#[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/// Vault signer configuration
177#[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/// Vault Transit signer configuration
198#[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/// Turnkey signer configuration
221#[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/// CDP signer configuration
239#[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/// Google Cloud KMS service account configuration
259///
260/// All fields are stored as SecretString to ensure they are encrypted at rest
261/// in Redis.
262#[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/// Google Cloud KMS key configuration
323///
324/// All string fields are stored as SecretString to ensure they are encrypted
325/// at rest in Redis, preventing attackers from modifying key identifiers.
326#[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/// Google Cloud KMS signer configuration
347#[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
355/// Custom validator for SecretString
356fn 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
363/// Custom validator for SecretString that must contain a valid URL
364fn 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
457/// Allowed Google Cloud KMS universe domains
458/// These are the legitimate Google Cloud domains where KMS services can be hosted.
459/// See: https://cloud.google.com/kms/docs/reference/rest
460const ALLOWED_KMS_DOMAINS: &[&str] = &[
461    "cloudkms.googleapis.com", // Standard Google Cloud
462];
463
464/// Custom validator for Google Cloud KMS universe_domain to prevent SSRF attacks.
465/// Uses an allowlist approach - only explicitly approved Google Cloud domains are permitted.
466fn validate_universe_domain(secret: &SecretString) -> Result<(), validator::ValidationError> {
467    let value = secret.to_str();
468    // Construct the URL exactly as get_base_url() does in the service
469    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    // Only permit known Google Cloud KMS domains
478    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
485/// Custom validator for CDP signer configuration
486fn validate_cdp_config(config: &CdpSignerConfig) -> Result<(), validator::ValidationError> {
487    // Validate api_key_secret is valid base64
488    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    // Validate wallet_secret is valid base64
498    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    // Check if it's an EVM address (0x-prefixed hex)
510    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        // Check if the hex part is valid
520        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        // Assume it's a Solana address - validate using Pubkey::from_str
529        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/// Domain signer configuration enum containing all supported signer types
540#[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    /// Validates the configuration using the appropriate validator
554    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    /// Get local signer config if this is a local signer
603    pub fn get_local(&self) -> Option<&LocalSignerConfig> {
604        match self {
605            Self::Local(config) => Some(config),
606            _ => None,
607        }
608    }
609
610    /// Get AWS KMS signer config if this is an AWS KMS signer
611    pub fn get_aws_kms(&self) -> Option<&AwsKmsSignerConfig> {
612        match self {
613            Self::AwsKms(config) => Some(config),
614            _ => None,
615        }
616    }
617
618    /// Get Azure Key Vault signer config if this is an Azure Key Vault signer
619    pub fn get_azure_key_vault(&self) -> Option<&AzureKeyVaultSignerConfig> {
620        match self {
621            Self::AzureKeyVault(config) => Some(config),
622            _ => None,
623        }
624    }
625
626    /// Get Vault signer config if this is a Vault signer
627    pub fn get_vault(&self) -> Option<&VaultSignerConfig> {
628        match self {
629            Self::Vault(config) => Some(config),
630            _ => None,
631        }
632    }
633
634    /// Get Vault Transit signer config if this is a Vault Transit signer
635    pub fn get_vault_transit(&self) -> Option<&VaultTransitSignerConfig> {
636        match self {
637            Self::VaultTransit(config) => Some(config),
638            _ => None,
639        }
640    }
641
642    /// Get Turnkey signer config if this is a Turnkey signer
643    pub fn get_turnkey(&self) -> Option<&TurnkeySignerConfig> {
644        match self {
645            Self::Turnkey(config) => Some(config),
646            _ => None,
647        }
648    }
649
650    /// Get CDP signer config if this is a CDP signer
651    pub fn get_cdp(&self) -> Option<&CdpSignerConfig> {
652        match self {
653            Self::Cdp(config) => Some(config),
654            _ => None,
655        }
656    }
657
658    /// Get Google Cloud KMS signer config if this is a Google Cloud KMS signer
659    pub fn get_google_cloud_kms(&self) -> Option<&GoogleCloudKmsSignerConfig> {
660        match self {
661            Self::GoogleCloudKms(config) => Some(config),
662            _ => None,
663        }
664    }
665
666    /// Get the signer type from the configuration
667    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
681/// Helper function to format validation errors
682fn 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/// Core signer domain model containing both metadata and configuration
704#[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/// Signer type enum used for validation and API responses
718#[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    /// Creates a new signer with configuration
737    pub fn new(id: String, config: SignerConfig) -> Self {
738        Self { id, config }
739    }
740
741    /// Gets the signer type from the configuration
742    pub fn signer_type(&self) -> SignerType {
743        self.config.get_signer_type()
744    }
745
746    /// Validates the signer using both struct validation and config validation
747    pub fn validate(&self) -> Result<(), SignerValidationError> {
748        // First validate struct-level constraints (ID format, etc.)
749        Validate::validate(self).map_err(|validation_errors| {
750            // Convert validator errors to our custom error type
751            // Return the first error for simplicity
752            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, // fallback
759                    };
760                }
761            }
762            // Fallback error
763            SignerValidationError::InvalidIdFormat
764        })?;
765
766        // Then validate the configuration
767        self.config.validate()?;
768
769        Ok(())
770    }
771}
772
773/// Validation errors for signers
774#[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
784/// Centralized conversion from SignerValidationError to ApiError
785impl 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)), // Use valid non-zero key
889        });
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)), // Use valid non-zero key
903        });
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)), // Use valid non-zero key
917        });
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)), // Invalid length: 16 bytes instead of 32
931        });
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)), // Invalid: all zeros
949        });
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)), // Valid: 32 bytes, non-zero
966        });
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        // Test Local config accessor
1001        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        // Test AWS KMS config accessor
1009        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"), // Invalid URL
1109                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        // Test empty secret
1139        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        // Test valid secret
1147        let result = validate_secret_string(&SecretString::new("secret"));
1148        assert!(result.is_ok());
1149    }
1150
1151    #[test]
1152    fn test_validation_error_formatting() {
1153        // Create an invalid config to trigger multiple nested validation errors
1154        let invalid_config = GoogleCloudKmsSignerConfig {
1155            service_account: GoogleCloudKmsSignerServiceAccountConfig {
1156                private_key: SecretString::new(""), // Invalid: empty
1157                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(""), // Invalid: empty
1161                auth_uri: SecretString::new("not-a-url"), // Invalid: not a URL
1162                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(""), // Invalid: empty
1174                key_id: SecretString::new("test-key"),
1175                key_version: 1,
1176            },
1177        };
1178
1179        let errors = invalid_config.validate().unwrap_err();
1180
1181        // Format the errors using the helper function
1182        let formatted = format_validation_errors(&errors);
1183
1184        println!("formatted: {formatted}");
1185
1186        // Check that messages from nested fields are correctly formatted
1187        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        // Test Vault config getter
1196        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        // Test VaultTransit config getter
1208        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        // Test Turnkey config getter
1222        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        // Test Google Cloud KMS config getter
1234        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"), // Valid base64: "secret"
1268            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1269            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"), // Valid base64: "secret"
1281            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1282            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"), // Valid base64: "secret"
1294            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1295            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"), // Valid base64: "secret"
1312            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1313            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"), // Valid base64: "secret"
1330            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1331            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"), // Valid base64: "secret"
1348            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1349            account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44".to_string(), // Too short
1350        };
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"), // Valid base64: "secret"
1366            wallet_secret: SecretString::new("d2FsbGV0LXNlY3JldA=="), // Valid base64: "wallet-secret"
1367            account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm0".to_string(), // Contains '0' which is invalid in Base58
1368        };
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!@#"), // Invalid base64
1384            wallet_secret: SecretString::new("dGVzdC13YWxsZXQtc2VjcmV0"), // Valid base64: "test-wallet-secret"
1385            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=="), // Valid base64: "test-api-key-secret"
1402            wallet_secret: SecretString::new("invalid-base64!@#"),             // Invalid base64
1403            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=="), // Valid base64: "test-api-key-secret"
1420            wallet_secret: SecretString::new("dGVzdC13YWxsZXQtc2VjcmV0"), // Valid base64: "test-wallet-secret"
1421            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        // Valid: default Google domain
1432        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        // Valid: explicit HTTPS URL
1439        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        // Invalid: AWS metadata endpoint
1447        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        // Invalid: GCP metadata endpoint
1457        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        // Invalid: localhost
1468        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        // Invalid: private IP addresses
1478        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        // Invalid: arbitrary public domains not in the allowlist
1494        // This tests the allowlist approach - even valid public URLs are rejected if not in allowlist
1495        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        // Invalid: attacker-controlled domain with "googleapis" in subdomain
1502        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        // Invalid: similar-looking domain
1511        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        // Invalid: using domain value directly that constructs non-allowlisted URL
1519        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}