openzeppelin_relayer/models/signer/
config.rs

1//! Configuration file representation and parsing for signers.
2//!
3//! This module handles the configuration file format for signers, providing:
4//!
5//! - **Config Models**: Structures that match the configuration file schema
6//! - **Conversions**: Bidirectional mapping between config and domain models
7//! - **Collections**: Container types for managing multiple signer configurations
8//!
9//! Used primarily during application startup to parse signer settings from config files.
10//! Validation is handled by the domain model in signer.rs to ensure reusability.
11
12use crate::{
13    config::ConfigFileError,
14    models::signer::{
15        AwsKmsSignerConfig, GoogleCloudKmsSignerConfig, GoogleCloudKmsSignerKeyConfig,
16        GoogleCloudKmsSignerServiceAccountConfig, LocalSignerConfig, Signer, SignerConfig,
17        TurnkeySignerConfig, VaultSignerConfig, VaultTransitSignerConfig,
18    },
19    models::PlainOrEnvValue,
20};
21use secrets::SecretVec;
22use serde::{Deserialize, Serialize};
23use std::{collections::HashSet, path::Path};
24
25#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
26#[serde(deny_unknown_fields)]
27pub struct LocalSignerFileConfig {
28    pub path: String,
29    pub passphrase: PlainOrEnvValue,
30}
31
32#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
33#[serde(deny_unknown_fields)]
34pub struct AwsKmsSignerFileConfig {
35    pub region: String,
36    pub key_id: String,
37}
38
39#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
40#[serde(deny_unknown_fields)]
41pub struct TurnkeySignerFileConfig {
42    pub api_public_key: String,
43    pub api_private_key: PlainOrEnvValue,
44    pub organization_id: String,
45    pub private_key_id: String,
46    pub public_key: String,
47}
48
49#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
50#[serde(deny_unknown_fields)]
51pub struct VaultSignerFileConfig {
52    pub address: String,
53    pub namespace: Option<String>,
54    pub role_id: PlainOrEnvValue,
55    pub secret_id: PlainOrEnvValue,
56    pub key_name: String,
57    pub mount_point: Option<String>,
58}
59
60#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
61#[serde(deny_unknown_fields)]
62pub struct VaultTransitSignerFileConfig {
63    pub key_name: String,
64    pub address: String,
65    pub role_id: PlainOrEnvValue,
66    pub secret_id: PlainOrEnvValue,
67    pub pubkey: String,
68    pub mount_point: Option<String>,
69    pub namespace: Option<String>,
70}
71
72fn google_cloud_default_auth_uri() -> String {
73    "https://accounts.google.com/o/oauth2/auth".to_string()
74}
75
76fn google_cloud_default_token_uri() -> String {
77    "https://oauth2.googleapis.com/token".to_string()
78}
79
80fn google_cloud_default_auth_provider_x509_cert_url() -> String {
81    "https://www.googleapis.com/oauth2/v1/certs".to_string()
82}
83
84fn google_cloud_default_client_x509_cert_url() -> String {
85    "https://www.googleapis.com/robot/v1/metadata/x509/solana-signer%40forward-emitter-459820-r7.iam.gserviceaccount.com".to_string()
86}
87
88fn google_cloud_default_universe_domain() -> String {
89    "googleapis.com".to_string()
90}
91
92fn google_cloud_default_key_version() -> u32 {
93    1
94}
95
96fn google_cloud_default_location() -> String {
97    "global".to_string()
98}
99
100#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
101#[serde(deny_unknown_fields)]
102pub struct GoogleCloudKmsServiceAccountFileConfig {
103    pub project_id: String,
104    pub private_key_id: PlainOrEnvValue,
105    pub private_key: PlainOrEnvValue,
106    pub client_email: PlainOrEnvValue,
107    pub client_id: String,
108    #[serde(default = "google_cloud_default_auth_uri")]
109    pub auth_uri: String,
110    #[serde(default = "google_cloud_default_token_uri")]
111    pub token_uri: String,
112    #[serde(default = "google_cloud_default_auth_provider_x509_cert_url")]
113    pub auth_provider_x509_cert_url: String,
114    #[serde(default = "google_cloud_default_client_x509_cert_url")]
115    pub client_x509_cert_url: String,
116    #[serde(default = "google_cloud_default_universe_domain")]
117    pub universe_domain: String,
118}
119
120#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
121#[serde(deny_unknown_fields)]
122pub struct GoogleCloudKmsKeyFileConfig {
123    #[serde(default = "google_cloud_default_location")]
124    pub location: String,
125    pub key_ring_id: String,
126    pub key_id: String,
127    #[serde(default = "google_cloud_default_key_version")]
128    pub key_version: u32,
129}
130
131#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
132#[serde(deny_unknown_fields)]
133pub struct GoogleCloudKmsSignerFileConfig {
134    pub service_account: GoogleCloudKmsServiceAccountFileConfig,
135    pub key: GoogleCloudKmsKeyFileConfig,
136}
137
138/// Main enum for all signer config types
139#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
140#[serde(tag = "type", rename_all = "lowercase", content = "config")]
141pub enum SignerFileConfigEnum {
142    Local(LocalSignerFileConfig),
143    #[serde(rename = "aws_kms")]
144    AwsKms(AwsKmsSignerFileConfig),
145    Turnkey(TurnkeySignerFileConfig),
146    Vault(VaultSignerFileConfig),
147    #[serde(rename = "vault_transit")]
148    VaultTransit(VaultTransitSignerFileConfig),
149    #[serde(rename = "google_cloud_kms")]
150    GoogleCloudKms(GoogleCloudKmsSignerFileConfig),
151}
152
153/// Individual signer configuration from config file
154#[derive(Debug, Serialize, Deserialize, Clone)]
155#[serde(deny_unknown_fields)]
156pub struct SignerFileConfig {
157    pub id: String,
158    #[serde(flatten)]
159    pub config: SignerFileConfigEnum,
160}
161
162/// Collection of signer configurations
163#[derive(Debug, Serialize, Deserialize, Clone)]
164#[serde(deny_unknown_fields)]
165pub struct SignersFileConfig {
166    pub signers: Vec<SignerFileConfig>,
167}
168
169impl SignerFileConfig {
170    pub fn validate_basic(&self) -> Result<(), ConfigFileError> {
171        if self.id.is_empty() {
172            return Err(ConfigFileError::InvalidIdLength(
173                "Signer ID cannot be empty".into(),
174            ));
175        }
176        Ok(())
177    }
178}
179
180impl SignersFileConfig {
181    pub fn new(signers: Vec<SignerFileConfig>) -> Self {
182        Self { signers }
183    }
184
185    pub fn validate(&self) -> Result<(), ConfigFileError> {
186        if self.signers.is_empty() {
187            return Ok(());
188        }
189
190        let mut ids = HashSet::new();
191        for signer in &self.signers {
192            signer.validate_basic()?;
193            if !ids.insert(signer.id.clone()) {
194                return Err(ConfigFileError::DuplicateId(signer.id.clone()));
195            }
196        }
197        Ok(())
198    }
199}
200
201impl TryFrom<LocalSignerFileConfig> for LocalSignerConfig {
202    type Error = ConfigFileError;
203
204    fn try_from(config: LocalSignerFileConfig) -> Result<Self, Self::Error> {
205        if config.path.is_empty() {
206            return Err(ConfigFileError::InvalidIdLength(
207                "Signer path cannot be empty".into(),
208            ));
209        }
210
211        let path = Path::new(&config.path);
212        if !path.exists() {
213            return Err(ConfigFileError::FileNotFound(format!(
214                "Signer file not found at path: {}",
215                path.display()
216            )));
217        }
218
219        if !path.is_file() {
220            return Err(ConfigFileError::InvalidFormat(format!(
221                "Path exists but is not a file: {}",
222                path.display()
223            )));
224        }
225
226        let passphrase = config.passphrase.get_value().map_err(|e| {
227            ConfigFileError::InvalidFormat(format!("Failed to get passphrase value: {}", e))
228        })?;
229
230        if passphrase.is_empty() {
231            return Err(ConfigFileError::InvalidFormat(
232                "Local signer passphrase cannot be empty".into(),
233            ));
234        }
235
236        let raw_key = SecretVec::new(32, |buffer| {
237            let loaded = oz_keystore::LocalClient::load(
238                Path::new(&config.path).to_path_buf(),
239                passphrase.to_str().as_str().to_string(),
240            );
241            buffer.copy_from_slice(&loaded);
242        });
243
244        Ok(LocalSignerConfig { raw_key })
245    }
246}
247
248impl TryFrom<AwsKmsSignerFileConfig> for AwsKmsSignerConfig {
249    type Error = ConfigFileError;
250
251    fn try_from(config: AwsKmsSignerFileConfig) -> Result<Self, Self::Error> {
252        Ok(AwsKmsSignerConfig {
253            region: Some(config.region),
254            key_id: config.key_id,
255        })
256    }
257}
258
259impl TryFrom<TurnkeySignerFileConfig> for TurnkeySignerConfig {
260    type Error = ConfigFileError;
261
262    fn try_from(config: TurnkeySignerFileConfig) -> Result<Self, Self::Error> {
263        let api_private_key = config.api_private_key.get_value().map_err(|e| {
264            ConfigFileError::InvalidFormat(format!("Failed to get API private key: {}", e))
265        })?;
266
267        Ok(TurnkeySignerConfig {
268            api_public_key: config.api_public_key,
269            api_private_key,
270            organization_id: config.organization_id,
271            private_key_id: config.private_key_id,
272            public_key: config.public_key,
273        })
274    }
275}
276
277impl TryFrom<VaultSignerFileConfig> for VaultSignerConfig {
278    type Error = ConfigFileError;
279
280    fn try_from(config: VaultSignerFileConfig) -> Result<Self, Self::Error> {
281        let role_id = config
282            .role_id
283            .get_value()
284            .map_err(|e| ConfigFileError::InvalidFormat(format!("Failed to get role ID: {}", e)))?;
285
286        let secret_id = config.secret_id.get_value().map_err(|e| {
287            ConfigFileError::InvalidFormat(format!("Failed to get secret ID: {}", e))
288        })?;
289
290        Ok(VaultSignerConfig {
291            address: config.address,
292            namespace: config.namespace,
293            role_id,
294            secret_id,
295            key_name: config.key_name,
296            mount_point: config.mount_point,
297        })
298    }
299}
300
301impl TryFrom<VaultTransitSignerFileConfig> for VaultTransitSignerConfig {
302    type Error = ConfigFileError;
303
304    fn try_from(config: VaultTransitSignerFileConfig) -> Result<Self, Self::Error> {
305        let role_id = config
306            .role_id
307            .get_value()
308            .map_err(|e| ConfigFileError::InvalidFormat(format!("Failed to get role ID: {}", e)))?;
309
310        let secret_id = config.secret_id.get_value().map_err(|e| {
311            ConfigFileError::InvalidFormat(format!("Failed to get secret ID: {}", e))
312        })?;
313
314        Ok(VaultTransitSignerConfig {
315            key_name: config.key_name,
316            address: config.address,
317            namespace: config.namespace,
318            role_id,
319            secret_id,
320            pubkey: config.pubkey,
321            mount_point: config.mount_point,
322        })
323    }
324}
325
326impl TryFrom<GoogleCloudKmsSignerFileConfig> for GoogleCloudKmsSignerConfig {
327    type Error = ConfigFileError;
328
329    fn try_from(config: GoogleCloudKmsSignerFileConfig) -> Result<Self, Self::Error> {
330        let private_key = config
331            .service_account
332            .private_key
333            .get_value()
334            .map_err(|e| {
335                ConfigFileError::InvalidFormat(format!("Failed to get private key: {}", e))
336            })?;
337
338        let private_key_id = config
339            .service_account
340            .private_key_id
341            .get_value()
342            .map_err(|e| {
343                ConfigFileError::InvalidFormat(format!("Failed to get private key ID: {}", e))
344            })?;
345
346        let client_email = config
347            .service_account
348            .client_email
349            .get_value()
350            .map_err(|e| {
351                ConfigFileError::InvalidFormat(format!("Failed to get client email: {}", e))
352            })?;
353
354        let service_account = GoogleCloudKmsSignerServiceAccountConfig {
355            private_key,
356            private_key_id,
357            project_id: config.service_account.project_id,
358            client_email,
359            client_id: config.service_account.client_id,
360            auth_uri: config.service_account.auth_uri,
361            token_uri: config.service_account.token_uri,
362            auth_provider_x509_cert_url: config.service_account.auth_provider_x509_cert_url,
363            client_x509_cert_url: config.service_account.client_x509_cert_url,
364            universe_domain: config.service_account.universe_domain,
365        };
366
367        let key = GoogleCloudKmsSignerKeyConfig {
368            location: config.key.location,
369            key_ring_id: config.key.key_ring_id,
370            key_id: config.key.key_id,
371            key_version: config.key.key_version,
372        };
373
374        Ok(GoogleCloudKmsSignerConfig {
375            service_account,
376            key,
377        })
378    }
379}
380
381impl TryFrom<SignerFileConfigEnum> for SignerConfig {
382    type Error = ConfigFileError;
383
384    fn try_from(config: SignerFileConfigEnum) -> Result<Self, Self::Error> {
385        match config {
386            SignerFileConfigEnum::Local(local) => {
387                Ok(SignerConfig::Local(LocalSignerConfig::try_from(local)?))
388            }
389            SignerFileConfigEnum::AwsKms(aws_kms) => {
390                Ok(SignerConfig::AwsKms(AwsKmsSignerConfig::try_from(aws_kms)?))
391            }
392            SignerFileConfigEnum::Turnkey(turnkey) => Ok(SignerConfig::Turnkey(
393                TurnkeySignerConfig::try_from(turnkey)?,
394            )),
395            SignerFileConfigEnum::Vault(vault) => {
396                Ok(SignerConfig::Vault(VaultSignerConfig::try_from(vault)?))
397            }
398            SignerFileConfigEnum::VaultTransit(vault_transit) => Ok(SignerConfig::VaultTransit(
399                VaultTransitSignerConfig::try_from(vault_transit)?,
400            )),
401            SignerFileConfigEnum::GoogleCloudKms(gcp_kms) => Ok(SignerConfig::GoogleCloudKms(
402                GoogleCloudKmsSignerConfig::try_from(gcp_kms)?,
403            )),
404        }
405    }
406}
407
408impl TryFrom<SignerFileConfig> for Signer {
409    type Error = ConfigFileError;
410
411    fn try_from(config: SignerFileConfig) -> Result<Self, Self::Error> {
412        config.validate_basic()?;
413
414        let signer_config = SignerConfig::try_from(config.config)?;
415
416        // Create core signer with configuration
417        let signer = Signer::new(config.id, signer_config);
418
419        // Validate using domain model validation logic
420        signer.validate().map_err(|e| match e {
421            crate::models::signer::SignerValidationError::EmptyId => {
422                ConfigFileError::MissingField("signer id".into())
423            }
424            crate::models::signer::SignerValidationError::InvalidIdFormat => {
425                ConfigFileError::InvalidFormat("Invalid signer ID format".into())
426            }
427            crate::models::signer::SignerValidationError::InvalidConfig(msg) => {
428                ConfigFileError::InvalidFormat(format!("Invalid signer configuration: {}", msg))
429            }
430        })?;
431
432        Ok(signer)
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use crate::models::SecretString;
440
441    #[test]
442    fn test_aws_kms_conversion() {
443        let config = AwsKmsSignerFileConfig {
444            region: "us-east-1".to_string(),
445            key_id: "test-key-id".to_string(),
446        };
447
448        let result = AwsKmsSignerConfig::try_from(config);
449        assert!(result.is_ok());
450
451        let aws_config = result.unwrap();
452        assert_eq!(aws_config.region, Some("us-east-1".to_string()));
453        assert_eq!(aws_config.key_id, "test-key-id");
454    }
455
456    #[test]
457    fn test_turnkey_conversion() {
458        let config = TurnkeySignerFileConfig {
459            api_public_key: "test-public-key".to_string(),
460            api_private_key: PlainOrEnvValue::Plain {
461                value: SecretString::new("test-private-key"),
462            },
463            organization_id: "test-org".to_string(),
464            private_key_id: "test-private-key-id".to_string(),
465            public_key: "test-public-key".to_string(),
466        };
467
468        let result = TurnkeySignerConfig::try_from(config);
469        assert!(result.is_ok());
470
471        let turnkey_config = result.unwrap();
472        assert_eq!(turnkey_config.api_public_key, "test-public-key");
473        assert_eq!(turnkey_config.organization_id, "test-org");
474    }
475
476    #[test]
477    fn test_signer_file_config_validation() {
478        let signer_config = SignerFileConfig {
479            id: "test-signer".to_string(),
480            config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
481                path: "test-path".to_string(),
482                passphrase: PlainOrEnvValue::Plain {
483                    value: SecretString::new("test-passphrase"),
484                },
485            }),
486        };
487
488        assert!(signer_config.validate_basic().is_ok());
489    }
490
491    #[test]
492    fn test_empty_signer_id() {
493        let signer_config = SignerFileConfig {
494            id: "".to_string(),
495            config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
496                path: "test-path".to_string(),
497                passphrase: PlainOrEnvValue::Plain {
498                    value: SecretString::new("test-passphrase"),
499                },
500            }),
501        };
502
503        assert!(signer_config.validate_basic().is_err());
504    }
505
506    #[test]
507    fn test_signers_config_validation() {
508        let configs = SignersFileConfig::new(vec![
509            SignerFileConfig {
510                id: "signer1".to_string(),
511                config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
512                    path: "test-path".to_string(),
513                    passphrase: PlainOrEnvValue::Plain {
514                        value: SecretString::new("test-passphrase"),
515                    },
516                }),
517            },
518            SignerFileConfig {
519                id: "signer2".to_string(),
520                config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
521                    path: "test-path".to_string(),
522                    passphrase: PlainOrEnvValue::Plain {
523                        value: SecretString::new("test-passphrase"),
524                    },
525                }),
526            },
527        ]);
528
529        assert!(configs.validate().is_ok());
530    }
531
532    #[test]
533    fn test_duplicate_signer_ids() {
534        let configs = SignersFileConfig::new(vec![
535            SignerFileConfig {
536                id: "signer1".to_string(),
537                config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
538                    path: "test-path".to_string(),
539                    passphrase: PlainOrEnvValue::Plain {
540                        value: SecretString::new("test-passphrase"),
541                    },
542                }),
543            },
544            SignerFileConfig {
545                id: "signer1".to_string(), // Duplicate ID
546                config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
547                    path: "test-path".to_string(),
548                    passphrase: PlainOrEnvValue::Plain {
549                        value: SecretString::new("test-passphrase"),
550                    },
551                }),
552            },
553        ]);
554
555        assert!(matches!(
556            configs.validate(),
557            Err(ConfigFileError::DuplicateId(_))
558        ));
559    }
560
561    #[test]
562    fn test_local_conversion_invalid_path() {
563        let config = LocalSignerFileConfig {
564            path: "non-existent-path".to_string(),
565            passphrase: PlainOrEnvValue::Plain {
566                value: SecretString::new("test-passphrase"),
567            },
568        };
569
570        let result = LocalSignerConfig::try_from(config);
571        assert!(result.is_err());
572        if let Err(ConfigFileError::FileNotFound(msg)) = result {
573            assert!(msg.contains("Signer file not found"));
574        } else {
575            panic!("Expected FileNotFound error");
576        }
577    }
578
579    #[test]
580    fn test_vault_conversion() {
581        let config = VaultSignerFileConfig {
582            address: "https://vault.example.com".to_string(),
583            namespace: Some("test-namespace".to_string()),
584            role_id: PlainOrEnvValue::Plain {
585                value: SecretString::new("test-role"),
586            },
587            secret_id: PlainOrEnvValue::Plain {
588                value: SecretString::new("test-secret"),
589            },
590            key_name: "test-key".to_string(),
591            mount_point: Some("test-mount".to_string()),
592        };
593
594        let result = VaultSignerConfig::try_from(config);
595        assert!(result.is_ok());
596
597        let vault_config = result.unwrap();
598        assert_eq!(vault_config.address, "https://vault.example.com");
599        assert_eq!(vault_config.namespace, Some("test-namespace".to_string()));
600    }
601
602    #[test]
603    fn test_google_cloud_kms_conversion() {
604        let config = GoogleCloudKmsSignerFileConfig {
605            service_account: GoogleCloudKmsServiceAccountFileConfig {
606                project_id: "test-project".to_string(),
607                private_key_id: PlainOrEnvValue::Plain {
608                    value: SecretString::new("test-key-id"),
609                },
610                private_key: PlainOrEnvValue::Plain {
611                    value: SecretString::new("test-private-key"),
612                },
613                client_email: PlainOrEnvValue::Plain {
614                    value: SecretString::new("test@email.com"),
615                },
616                client_id: "test-client-id".to_string(),
617                auth_uri: google_cloud_default_auth_uri(),
618                token_uri: google_cloud_default_token_uri(),
619                auth_provider_x509_cert_url: google_cloud_default_auth_provider_x509_cert_url(),
620                client_x509_cert_url: google_cloud_default_client_x509_cert_url(),
621                universe_domain: google_cloud_default_universe_domain(),
622            },
623            key: GoogleCloudKmsKeyFileConfig {
624                location: google_cloud_default_location(),
625                key_ring_id: "test-ring".to_string(),
626                key_id: "test-key".to_string(),
627                key_version: google_cloud_default_key_version(),
628            },
629        };
630
631        let result = GoogleCloudKmsSignerConfig::try_from(config);
632        assert!(result.is_ok());
633
634        let gcp_config = result.unwrap();
635        assert_eq!(gcp_config.key.key_id, "test-key");
636        assert_eq!(gcp_config.service_account.project_id, "test-project");
637    }
638}