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};
32use secrets::SecretVec;
33use serde::{Deserialize, Serialize, Serializer};
34use utoipa::ToSchema;
35use validator::Validate;
36
37/// Helper function to serialize secrets as redacted
38fn serialize_secret_redacted<S>(_secret: &SecretVec<u8>, serializer: S) -> Result<S::Ok, S::Error>
39where
40    S: Serializer,
41{
42    serializer.serialize_str("[REDACTED]")
43}
44
45/// Local signer configuration for storing private keys
46#[derive(Debug, Clone, Serialize)]
47pub struct LocalSignerConfig {
48    #[serde(serialize_with = "serialize_secret_redacted")]
49    pub raw_key: SecretVec<u8>,
50}
51
52impl LocalSignerConfig {
53    /// Validates the raw key for cryptographic requirements
54    pub fn validate(&self) -> Result<(), SignerValidationError> {
55        let key_bytes = self.raw_key.borrow();
56
57        // Check key length - must be exactly 32 bytes for crypto operations
58        if key_bytes.len() != 32 {
59            return Err(SignerValidationError::InvalidConfig(format!(
60                "Raw key must be exactly 32 bytes, got {} bytes",
61                key_bytes.len()
62            )));
63        }
64
65        // Check if key is all zeros (cryptographically invalid)
66        if key_bytes.iter().all(|&b| b == 0) {
67            return Err(SignerValidationError::InvalidConfig(
68                "Raw key cannot be all zeros".to_string(),
69            ));
70        }
71
72        Ok(())
73    }
74}
75
76impl<'de> Deserialize<'de> for LocalSignerConfig {
77    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78    where
79        D: serde::Deserializer<'de>,
80    {
81        #[derive(Deserialize)]
82        struct LocalSignerConfigHelper {
83            raw_key: String,
84        }
85
86        let helper = LocalSignerConfigHelper::deserialize(deserializer)?;
87        let raw_key = if helper.raw_key == "[REDACTED]" {
88            // Return a zero-filled SecretVec when deserializing redacted data
89            SecretVec::zero(32)
90        } else {
91            // For actual data, assume it's the raw bytes represented as a string
92            // In practice, this would come from proper key loading
93            SecretVec::new(helper.raw_key.len(), |v| {
94                v.copy_from_slice(helper.raw_key.as_bytes())
95            })
96        };
97
98        Ok(LocalSignerConfig { raw_key })
99    }
100}
101
102/// AWS KMS signer configuration
103/// The configuration supports:
104/// - AWS Region (aws_region) - important for region-specific key
105/// - KMS Key identification (key_id)
106///
107/// The AWS authentication is carried out
108/// through recommended credential providers as outlined in
109/// https://docs.aws.amazon.com/sdk-for-rust/latest/dg/credproviders.html
110/// Currently only EVM signing is supported since, as of June 2025,
111/// AWS does not support ed25519 scheme
112#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
113pub struct AwsKmsSignerConfig {
114    #[validate(length(min = 1, message = "Region cannot be empty"))]
115    pub region: Option<String>,
116    #[validate(length(min = 1, message = "Key ID cannot be empty"))]
117    pub key_id: String,
118}
119
120/// Vault signer configuration
121#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
122pub struct VaultSignerConfig {
123    #[validate(url(message = "Address must be a valid URL"))]
124    pub address: String,
125    pub namespace: Option<String>,
126    #[validate(custom(
127        function = "validate_secret_string",
128        message = "Role ID cannot be empty"
129    ))]
130    pub role_id: SecretString,
131    #[validate(custom(
132        function = "validate_secret_string",
133        message = "Secret ID cannot be empty"
134    ))]
135    pub secret_id: SecretString,
136    #[validate(length(min = 1, message = "Vault key name cannot be empty"))]
137    pub key_name: String,
138    pub mount_point: Option<String>,
139}
140
141/// Vault Transit signer configuration
142#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
143pub struct VaultTransitSignerConfig {
144    #[validate(length(min = 1, message = "Key name cannot be empty"))]
145    pub key_name: String,
146    #[validate(url(message = "Address must be a valid URL"))]
147    pub address: String,
148    pub namespace: Option<String>,
149    #[validate(custom(
150        function = "validate_secret_string",
151        message = "Role ID cannot be empty"
152    ))]
153    pub role_id: SecretString,
154    #[validate(custom(
155        function = "validate_secret_string",
156        message = "Secret ID cannot be empty"
157    ))]
158    pub secret_id: SecretString,
159    #[validate(length(min = 1, message = "pubkey cannot be empty"))]
160    pub pubkey: String,
161    pub mount_point: Option<String>,
162}
163
164/// Turnkey signer configuration
165#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
166pub struct TurnkeySignerConfig {
167    #[validate(length(min = 1, message = "API public key cannot be empty"))]
168    pub api_public_key: String,
169    #[validate(custom(
170        function = "validate_secret_string",
171        message = "API private key cannot be empty"
172    ))]
173    pub api_private_key: SecretString,
174    #[validate(length(min = 1, message = "Organization ID cannot be empty"))]
175    pub organization_id: String,
176    #[validate(length(min = 1, message = "Private key ID cannot be empty"))]
177    pub private_key_id: String,
178    #[validate(length(min = 1, message = "Public key cannot be empty"))]
179    pub public_key: String,
180}
181
182/// Google Cloud KMS service account configuration
183#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
184pub struct GoogleCloudKmsSignerServiceAccountConfig {
185    #[validate(custom(
186        function = "validate_secret_string",
187        message = "Private key cannot be empty"
188    ))]
189    pub private_key: SecretString,
190    #[validate(custom(
191        function = "validate_secret_string",
192        message = "Private key ID cannot be empty"
193    ))]
194    pub private_key_id: SecretString,
195    #[validate(length(min = 1, message = "Project ID cannot be empty"))]
196    pub project_id: String,
197    #[validate(custom(
198        function = "validate_secret_string",
199        message = "Client email cannot be empty"
200    ))]
201    pub client_email: SecretString,
202    #[validate(length(min = 1, message = "Client ID cannot be empty"))]
203    pub client_id: String,
204    #[validate(url(message = "Auth URI must be a valid URL"))]
205    pub auth_uri: String,
206    #[validate(url(message = "Token URI must be a valid URL"))]
207    pub token_uri: String,
208    #[validate(url(message = "Auth provider x509 cert URL must be a valid URL"))]
209    pub auth_provider_x509_cert_url: String,
210    #[validate(url(message = "Client x509 cert URL must be a valid URL"))]
211    pub client_x509_cert_url: String,
212    pub universe_domain: String,
213}
214
215/// Google Cloud KMS key configuration
216#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
217pub struct GoogleCloudKmsSignerKeyConfig {
218    pub location: String,
219    #[validate(length(min = 1, message = "Key ring ID cannot be empty"))]
220    pub key_ring_id: String,
221    #[validate(length(min = 1, message = "Key ID cannot be empty"))]
222    pub key_id: String,
223    pub key_version: u32,
224}
225
226/// Google Cloud KMS signer configuration
227#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
228pub struct GoogleCloudKmsSignerConfig {
229    #[validate(nested)]
230    pub service_account: GoogleCloudKmsSignerServiceAccountConfig,
231    #[validate(nested)]
232    pub key: GoogleCloudKmsSignerKeyConfig,
233}
234
235/// Custom validator for SecretString
236fn validate_secret_string(secret: &SecretString) -> Result<(), validator::ValidationError> {
237    if secret.to_str().is_empty() {
238        return Err(validator::ValidationError::new("empty_secret"));
239    }
240    Ok(())
241}
242
243/// Domain signer configuration enum containing all supported signer types
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub enum SignerConfig {
246    Local(LocalSignerConfig),
247    Vault(VaultSignerConfig),
248    VaultTransit(VaultTransitSignerConfig),
249    AwsKms(AwsKmsSignerConfig),
250    Turnkey(TurnkeySignerConfig),
251    GoogleCloudKms(GoogleCloudKmsSignerConfig),
252}
253
254impl SignerConfig {
255    /// Validates the configuration using the appropriate validator
256    pub fn validate(&self) -> Result<(), SignerValidationError> {
257        match self {
258            Self::Local(config) => config.validate(),
259            Self::AwsKms(config) => Validate::validate(config).map_err(|e| {
260                SignerValidationError::InvalidConfig(format!(
261                    "AWS KMS validation failed: {}",
262                    format_validation_errors(&e)
263                ))
264            }),
265            Self::Vault(config) => Validate::validate(config).map_err(|e| {
266                SignerValidationError::InvalidConfig(format!(
267                    "Vault validation failed: {}",
268                    format_validation_errors(&e)
269                ))
270            }),
271            Self::VaultTransit(config) => Validate::validate(config).map_err(|e| {
272                SignerValidationError::InvalidConfig(format!(
273                    "Vault Transit validation failed: {}",
274                    format_validation_errors(&e)
275                ))
276            }),
277            Self::Turnkey(config) => Validate::validate(config).map_err(|e| {
278                SignerValidationError::InvalidConfig(format!(
279                    "Turnkey validation failed: {}",
280                    format_validation_errors(&e)
281                ))
282            }),
283            Self::GoogleCloudKms(config) => Validate::validate(config).map_err(|e| {
284                SignerValidationError::InvalidConfig(format!(
285                    "Google Cloud KMS validation failed: {}",
286                    format_validation_errors(&e)
287                ))
288            }),
289        }
290    }
291
292    /// Get local signer config if this is a local signer
293    pub fn get_local(&self) -> Option<&LocalSignerConfig> {
294        match self {
295            Self::Local(config) => Some(config),
296            _ => None,
297        }
298    }
299
300    /// Get AWS KMS signer config if this is an AWS KMS signer
301    pub fn get_aws_kms(&self) -> Option<&AwsKmsSignerConfig> {
302        match self {
303            Self::AwsKms(config) => Some(config),
304            _ => None,
305        }
306    }
307
308    /// Get Vault signer config if this is a Vault signer
309    pub fn get_vault(&self) -> Option<&VaultSignerConfig> {
310        match self {
311            Self::Vault(config) => Some(config),
312            _ => None,
313        }
314    }
315
316    /// Get Vault Transit signer config if this is a Vault Transit signer
317    pub fn get_vault_transit(&self) -> Option<&VaultTransitSignerConfig> {
318        match self {
319            Self::VaultTransit(config) => Some(config),
320            _ => None,
321        }
322    }
323
324    /// Get Turnkey signer config if this is a Turnkey signer
325    pub fn get_turnkey(&self) -> Option<&TurnkeySignerConfig> {
326        match self {
327            Self::Turnkey(config) => Some(config),
328            _ => None,
329        }
330    }
331
332    /// Get Google Cloud KMS signer config if this is a Google Cloud KMS signer
333    pub fn get_google_cloud_kms(&self) -> Option<&GoogleCloudKmsSignerConfig> {
334        match self {
335            Self::GoogleCloudKms(config) => Some(config),
336            _ => None,
337        }
338    }
339
340    /// Get the signer type from the configuration
341    pub fn get_signer_type(&self) -> SignerType {
342        match self {
343            Self::Local(_) => SignerType::Local,
344            Self::AwsKms(_) => SignerType::AwsKms,
345            Self::Vault(_) => SignerType::Vault,
346            Self::VaultTransit(_) => SignerType::VaultTransit,
347            Self::Turnkey(_) => SignerType::Turnkey,
348            Self::GoogleCloudKms(_) => SignerType::GoogleCloudKms,
349        }
350    }
351}
352
353/// Helper function to format validation errors
354fn format_validation_errors(errors: &validator::ValidationErrors) -> String {
355    let mut messages = Vec::new();
356
357    for (field, field_errors) in errors.field_errors().iter() {
358        let field_msgs: Vec<String> = field_errors
359            .iter()
360            .map(|error| error.message.clone().unwrap_or_default().to_string())
361            .collect();
362        messages.push(format!("{}: {}", field, field_msgs.join(", ")));
363    }
364
365    for (struct_field, kind) in errors.errors().iter() {
366        if let validator::ValidationErrorsKind::Struct(nested) = kind {
367            let nested_msgs = format_validation_errors(nested);
368            messages.push(format!("{}.{}", struct_field, nested_msgs));
369        }
370    }
371
372    messages.join("; ")
373}
374
375/// Core signer domain model containing both metadata and configuration
376#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
377pub struct Signer {
378    #[validate(
379        length(min = 1, max = 36, message = "ID must be between 1 and 36 characters"),
380        regex(
381            path = "*ID_REGEX",
382            message = "ID must contain only letters, numbers, dashes and underscores"
383        )
384    )]
385    pub id: String,
386    pub config: SignerConfig,
387}
388
389/// Signer type enum used for validation and API responses
390#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
391#[serde(rename_all = "lowercase")]
392pub enum SignerType {
393    Local,
394    #[serde(rename = "aws_kms")]
395    AwsKms,
396    #[serde(rename = "google_cloud_kms")]
397    GoogleCloudKms,
398    Vault,
399    #[serde(rename = "vault_transit")]
400    VaultTransit,
401    Turnkey,
402}
403
404impl Signer {
405    /// Creates a new signer with configuration
406    pub fn new(id: String, config: SignerConfig) -> Self {
407        Self { id, config }
408    }
409
410    /// Gets the signer type from the configuration
411    pub fn signer_type(&self) -> SignerType {
412        self.config.get_signer_type()
413    }
414
415    /// Validates the signer using both struct validation and config validation
416    pub fn validate(&self) -> Result<(), SignerValidationError> {
417        // First validate struct-level constraints (ID format, etc.)
418        Validate::validate(self).map_err(|validation_errors| {
419            // Convert validator errors to our custom error type
420            // Return the first error for simplicity
421            for (field, errors) in validation_errors.field_errors() {
422                if let Some(error) = errors.first() {
423                    let field_str = field.as_ref();
424                    return match (field_str, error.code.as_ref()) {
425                        ("id", "length") => SignerValidationError::InvalidIdFormat,
426                        ("id", "regex") => SignerValidationError::InvalidIdFormat,
427                        _ => SignerValidationError::InvalidIdFormat, // fallback
428                    };
429                }
430            }
431            // Fallback error
432            SignerValidationError::InvalidIdFormat
433        })?;
434
435        // Then validate the configuration
436        self.config.validate()?;
437
438        Ok(())
439    }
440}
441
442/// Validation errors for signers
443#[derive(Debug, thiserror::Error)]
444pub enum SignerValidationError {
445    #[error("Signer ID cannot be empty")]
446    EmptyId,
447    #[error("Signer ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long")]
448    InvalidIdFormat,
449    #[error("Invalid signer configuration: {0}")]
450    InvalidConfig(String),
451}
452
453/// Centralized conversion from SignerValidationError to ApiError
454impl From<SignerValidationError> for crate::models::ApiError {
455    fn from(error: SignerValidationError) -> Self {
456        use crate::models::ApiError;
457
458        ApiError::BadRequest(match error {
459            SignerValidationError::EmptyId => "ID cannot be empty".to_string(),
460            SignerValidationError::InvalidIdFormat => {
461                "ID must contain only letters, numbers, dashes and underscores and must be at most 36 characters long".to_string()
462            }
463            SignerValidationError::InvalidConfig(msg) => format!("Invalid signer configuration: {}", msg),
464        })
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    #[test]
473    fn test_valid_local_signer() {
474        let config = SignerConfig::Local(LocalSignerConfig {
475            raw_key: SecretVec::new(32, |v| v.fill(1)),
476        });
477
478        let signer = Signer::new("valid-id".to_string(), config);
479
480        assert!(signer.validate().is_ok());
481        assert_eq!(signer.signer_type(), SignerType::Local);
482    }
483
484    #[test]
485    fn test_valid_aws_kms_signer() {
486        let config = SignerConfig::AwsKms(AwsKmsSignerConfig {
487            region: Some("us-east-1".to_string()),
488            key_id: "test-key-id".to_string(),
489        });
490
491        let signer = Signer::new("aws-signer".to_string(), config);
492
493        assert!(signer.validate().is_ok());
494        assert_eq!(signer.signer_type(), SignerType::AwsKms);
495    }
496
497    #[test]
498    fn test_empty_id() {
499        let config = SignerConfig::Local(LocalSignerConfig {
500            raw_key: SecretVec::new(32, |v| v.fill(1)), // Use valid non-zero key
501        });
502
503        let signer = Signer::new("".to_string(), config);
504
505        assert!(matches!(
506            signer.validate(),
507            Err(SignerValidationError::InvalidIdFormat)
508        ));
509    }
510
511    #[test]
512    fn test_id_too_long() {
513        let config = SignerConfig::Local(LocalSignerConfig {
514            raw_key: SecretVec::new(32, |v| v.fill(1)), // Use valid non-zero key
515        });
516
517        let signer = Signer::new("a".repeat(37), config);
518
519        assert!(matches!(
520            signer.validate(),
521            Err(SignerValidationError::InvalidIdFormat)
522        ));
523    }
524
525    #[test]
526    fn test_invalid_id_format() {
527        let config = SignerConfig::Local(LocalSignerConfig {
528            raw_key: SecretVec::new(32, |v| v.fill(1)), // Use valid non-zero key
529        });
530
531        let signer = Signer::new("invalid@id".to_string(), config);
532
533        assert!(matches!(
534            signer.validate(),
535            Err(SignerValidationError::InvalidIdFormat)
536        ));
537    }
538
539    #[test]
540    fn test_local_signer_invalid_key_length() {
541        let config = SignerConfig::Local(LocalSignerConfig {
542            raw_key: SecretVec::new(16, |v| v.fill(1)), // Invalid length: 16 bytes instead of 32
543        });
544
545        let signer = Signer::new("valid-id".to_string(), config);
546
547        let result = signer.validate();
548        assert!(result.is_err());
549        if let Err(SignerValidationError::InvalidConfig(msg)) = result {
550            assert!(msg.contains("Raw key must be exactly 32 bytes"));
551            assert!(msg.contains("got 16 bytes"));
552        } else {
553            panic!("Expected InvalidConfig error for invalid key length");
554        }
555    }
556
557    #[test]
558    fn test_local_signer_all_zero_key() {
559        let config = SignerConfig::Local(LocalSignerConfig {
560            raw_key: SecretVec::new(32, |v| v.fill(0)), // Invalid: all zeros
561        });
562
563        let signer = Signer::new("valid-id".to_string(), config);
564
565        let result = signer.validate();
566        assert!(result.is_err());
567        if let Err(SignerValidationError::InvalidConfig(msg)) = result {
568            assert_eq!(msg, "Raw key cannot be all zeros");
569        } else {
570            panic!("Expected InvalidConfig error for all-zero key");
571        }
572    }
573
574    #[test]
575    fn test_local_signer_valid_key() {
576        let config = SignerConfig::Local(LocalSignerConfig {
577            raw_key: SecretVec::new(32, |v| v.fill(1)), // Valid: 32 bytes, non-zero
578        });
579
580        let signer = Signer::new("valid-id".to_string(), config);
581
582        assert!(signer.validate().is_ok());
583    }
584
585    #[test]
586    fn test_signer_type_serialization() {
587        use serde_json::{from_str, to_string};
588
589        assert_eq!(to_string(&SignerType::Local).unwrap(), "\"local\"");
590        assert_eq!(to_string(&SignerType::AwsKms).unwrap(), "\"aws_kms\"");
591        assert_eq!(
592            to_string(&SignerType::GoogleCloudKms).unwrap(),
593            "\"google_cloud_kms\""
594        );
595        assert_eq!(
596            to_string(&SignerType::VaultTransit).unwrap(),
597            "\"vault_transit\""
598        );
599
600        assert_eq!(
601            from_str::<SignerType>("\"local\"").unwrap(),
602            SignerType::Local
603        );
604        assert_eq!(
605            from_str::<SignerType>("\"aws_kms\"").unwrap(),
606            SignerType::AwsKms
607        );
608    }
609
610    #[test]
611    fn test_config_accessor_methods() {
612        // Test Local config accessor
613        let local_config = LocalSignerConfig {
614            raw_key: SecretVec::new(32, |v| v.fill(1)),
615        };
616        let config = SignerConfig::Local(local_config);
617        assert!(config.get_local().is_some());
618        assert!(config.get_aws_kms().is_none());
619
620        // Test AWS KMS config accessor
621        let aws_config = AwsKmsSignerConfig {
622            region: Some("us-east-1".to_string()),
623            key_id: "test-key".to_string(),
624        };
625        let config = SignerConfig::AwsKms(aws_config);
626        assert!(config.get_aws_kms().is_some());
627        assert!(config.get_local().is_none());
628    }
629
630    #[test]
631    fn test_error_conversion_to_api_error() {
632        let error = SignerValidationError::InvalidIdFormat;
633        let api_error: crate::models::ApiError = error.into();
634
635        if let crate::models::ApiError::BadRequest(msg) = api_error {
636            assert!(msg.contains("ID must contain only letters, numbers, dashes and underscores"));
637        } else {
638            panic!("Expected BadRequest error");
639        }
640    }
641
642    #[test]
643    fn test_valid_vault_signer() {
644        let config = SignerConfig::Vault(VaultSignerConfig {
645            address: "https://vault.example.com".to_string(),
646            namespace: Some("test".to_string()),
647            role_id: SecretString::new("role-id"),
648            secret_id: SecretString::new("secret-id"),
649            key_name: "test-key".to_string(),
650            mount_point: None,
651        });
652
653        let signer = Signer::new("vault-signer".to_string(), config);
654        assert!(signer.validate().is_ok());
655        assert_eq!(signer.signer_type(), SignerType::Vault);
656    }
657
658    #[test]
659    fn test_invalid_vault_signer_url() {
660        let config = SignerConfig::Vault(VaultSignerConfig {
661            address: "not-a-url".to_string(),
662            namespace: Some("test".to_string()),
663            role_id: SecretString::new("role-id"),
664            secret_id: SecretString::new("secret-id"),
665            key_name: "test-key".to_string(),
666            mount_point: None,
667        });
668
669        let signer = Signer::new("vault-signer".to_string(), config);
670        let result = signer.validate();
671        assert!(result.is_err());
672        if let Err(SignerValidationError::InvalidConfig(msg)) = result {
673            assert!(msg.contains("Address must be a valid URL"));
674        } else {
675            panic!("Expected InvalidConfig error for invalid URL");
676        }
677    }
678
679    #[test]
680    fn test_valid_google_cloud_kms_signer() {
681        let config = SignerConfig::GoogleCloudKms(GoogleCloudKmsSignerConfig {
682            service_account: GoogleCloudKmsSignerServiceAccountConfig {
683                private_key: SecretString::new("private-key"),
684                private_key_id: SecretString::new("key-id"),
685                project_id: "project".to_string(),
686                client_email: SecretString::new("client@example.com"),
687                client_id: "client-id".to_string(),
688                auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
689                token_uri: "https://oauth2.googleapis.com/token".to_string(),
690                auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
691                    .to_string(),
692                client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test"
693                    .to_string(),
694                universe_domain: "googleapis.com".to_string(),
695            },
696            key: GoogleCloudKmsSignerKeyConfig {
697                location: "us-central1".to_string(),
698                key_ring_id: "test-ring".to_string(),
699                key_id: "test-key".to_string(),
700                key_version: 1,
701            },
702        });
703
704        let signer = Signer::new("gcp-kms-signer".to_string(), config);
705        assert!(signer.validate().is_ok());
706        assert_eq!(signer.signer_type(), SignerType::GoogleCloudKms);
707    }
708
709    #[test]
710    fn test_invalid_google_cloud_kms_urls() {
711        let config = SignerConfig::GoogleCloudKms(GoogleCloudKmsSignerConfig {
712            service_account: GoogleCloudKmsSignerServiceAccountConfig {
713                private_key: SecretString::new("private-key"),
714                private_key_id: SecretString::new("key-id"),
715                project_id: "project".to_string(),
716                client_email: SecretString::new("client@example.com"),
717                client_id: "client-id".to_string(),
718                auth_uri: "not-a-url".to_string(), // Invalid URL
719                token_uri: "https://oauth2.googleapis.com/token".to_string(),
720                auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
721                    .to_string(),
722                client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test"
723                    .to_string(),
724                universe_domain: "googleapis.com".to_string(),
725            },
726            key: GoogleCloudKmsSignerKeyConfig {
727                location: "us-central1".to_string(),
728                key_ring_id: "test-ring".to_string(),
729                key_id: "test-key".to_string(),
730                key_version: 1,
731            },
732        });
733
734        let signer = Signer::new("gcp-kms-signer".to_string(), config);
735        let result = signer.validate();
736        assert!(result.is_err());
737        if let Err(SignerValidationError::InvalidConfig(msg)) = result {
738            assert!(msg.contains("Auth URI must be a valid URL"));
739        } else {
740            panic!("Expected InvalidConfig error for invalid URL");
741        }
742    }
743
744    #[test]
745    fn test_secret_string_validation() {
746        // Test empty secret
747        let result = validate_secret_string(&SecretString::new(""));
748        if let Err(e) = result {
749            assert_eq!(e.code, "empty_secret");
750        } else {
751            panic!("Expected validation error for empty secret");
752        }
753
754        // Test valid secret
755        let result = validate_secret_string(&SecretString::new("secret"));
756        assert!(result.is_ok());
757    }
758
759    #[test]
760    fn test_validation_error_formatting() {
761        // Create an invalid config to trigger multiple nested validation errors
762        let invalid_config = GoogleCloudKmsSignerConfig {
763            service_account: GoogleCloudKmsSignerServiceAccountConfig {
764                private_key: SecretString::new(""), // Invalid: empty
765                private_key_id: SecretString::new("key-id"),
766                project_id: "project".to_string(),
767                client_email: SecretString::new("client@example.com"),
768                client_id: "".to_string(),         // Invalid: empty
769                auth_uri: "not-a-url".to_string(), // Invalid: not a URL
770                token_uri: "https://oauth2.googleapis.com/token".to_string(),
771                auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
772                    .to_string(),
773                client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test"
774                    .to_string(),
775                universe_domain: "googleapis.com".to_string(),
776            },
777            key: GoogleCloudKmsSignerKeyConfig {
778                location: "us-central1".to_string(),
779                key_ring_id: "".to_string(), // Invalid: empty
780                key_id: "test-key".to_string(),
781                key_version: 1,
782            },
783        };
784
785        let errors = invalid_config.validate().unwrap_err();
786
787        // Format the errors using the helper function
788        let formatted = format_validation_errors(&errors);
789
790        println!("formatted: {}", formatted);
791
792        // Check that messages from nested fields are correctly formatted
793        assert!(formatted.contains("client_id: Client ID cannot be empty"));
794        assert!(formatted.contains("private_key: Private key cannot be empty"));
795        assert!(formatted.contains("auth_uri: Auth URI must be a valid URL"));
796        assert!(formatted.contains("key_ring_id: Key ring ID cannot be empty"));
797    }
798
799    #[test]
800    fn test_config_type_getters() {
801        // Test Vault config getter
802        let vault_config = VaultSignerConfig {
803            address: "https://vault.example.com".to_string(),
804            namespace: None,
805            role_id: SecretString::new("role"),
806            secret_id: SecretString::new("secret"),
807            key_name: "key".to_string(),
808            mount_point: None,
809        };
810        let config = SignerConfig::Vault(vault_config);
811        assert!(config.get_vault().is_some());
812
813        // Test VaultTransit config getter
814        let vault_transit_config = VaultTransitSignerConfig {
815            key_name: "key".to_string(),
816            address: "https://vault.example.com".to_string(),
817            namespace: None,
818            role_id: SecretString::new("role"),
819            secret_id: SecretString::new("secret"),
820            pubkey: "pubkey".to_string(),
821            mount_point: None,
822        };
823        let config = SignerConfig::VaultTransit(vault_transit_config);
824        assert!(config.get_vault_transit().is_some());
825        assert!(config.get_turnkey().is_none());
826
827        // Test Turnkey config getter
828        let turnkey_config = TurnkeySignerConfig {
829            api_public_key: "public".to_string(),
830            api_private_key: SecretString::new("private"),
831            organization_id: "org".to_string(),
832            private_key_id: "key-id".to_string(),
833            public_key: "pubkey".to_string(),
834        };
835        let config = SignerConfig::Turnkey(turnkey_config);
836        assert!(config.get_turnkey().is_some());
837        assert!(config.get_google_cloud_kms().is_none());
838
839        // Test Google Cloud KMS config getter
840        let gcp_config = GoogleCloudKmsSignerConfig {
841            service_account: GoogleCloudKmsSignerServiceAccountConfig {
842                private_key: SecretString::new("private-key"),
843                private_key_id: SecretString::new("key-id"),
844                project_id: "project".to_string(),
845                client_email: SecretString::new("client@example.com"),
846                client_id: "client-id".to_string(),
847                auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
848                token_uri: "https://oauth2.googleapis.com/token".to_string(),
849                auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
850                    .to_string(),
851                client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test"
852                    .to_string(),
853                universe_domain: "googleapis.com".to_string(),
854            },
855            key: GoogleCloudKmsSignerKeyConfig {
856                location: "us-central1".to_string(),
857                key_ring_id: "test-ring".to_string(),
858                key_id: "test-key".to_string(),
859                key_version: 1,
860            },
861        };
862        let config = SignerConfig::GoogleCloudKms(gcp_config);
863        assert!(config.get_google_cloud_kms().is_some());
864        assert!(config.get_local().is_none());
865    }
866}