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