1use 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#[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#[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#[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 let signer = Signer::new(config.id, signer_config);
418
419 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(), 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}