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