openzeppelin_relayer/config/config_file/signer/
vault_transit.rs

1//! Configuration for HashiCorp Vault Transit engine signer
2//!
3//! This module provides configuration for using HashiCorp Vault's Transit engine
4//! as a signing mechanism. Transit is Vault's cryptographic backend that allows
5//! for signing operations without exposing private keys.
6//!
7//! The configuration supports:
8//! - Key name for the Transit engine key to use
9//! - Vault server address (URL)
10//! - AppRole authentication (role_id and secret_id)
11//! - Public key representation for verification
12//! - Optional mount point override for the Transit engine
13//! - Optional namespace (for Vault Enterprise)
14//!
15//! Unlike regular Vault configuration, this specifically targets the Transit
16//! engine use case where keys are managed and stored within Vault itself.
17use crate::{
18    config::ConfigFileError,
19    models::{validate_plain_or_env_value, PlainOrEnvValue},
20};
21use serde::{Deserialize, Serialize};
22use validator::Validate;
23
24use super::{validate_with_validator, SignerConfigValidate};
25
26#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Validate)]
27#[serde(deny_unknown_fields)]
28pub struct VaultTransitSignerFileConfig {
29    #[validate(length(min = 1, message = "Key name cannot be empty"))]
30    pub key_name: String,
31    #[validate(url)]
32    pub address: String,
33    #[validate(custom(function = "validate_plain_or_env_value"))]
34    pub role_id: PlainOrEnvValue,
35    #[validate(custom(function = "validate_plain_or_env_value"))]
36    pub secret_id: PlainOrEnvValue,
37    #[validate(length(min = 1, message = "pubkey cannot be empty"))]
38    pub pubkey: String,
39    pub mount_point: Option<String>,
40    pub namespace: Option<String>,
41}
42
43impl SignerConfigValidate for VaultTransitSignerFileConfig {
44    fn validate(&self) -> Result<(), ConfigFileError> {
45        validate_with_validator(self)
46    }
47}
48#[cfg(test)]
49mod tests {
50    use crate::models::SecretString;
51
52    use super::*;
53
54    #[test]
55    fn test_vault_transit_signer_file_config_valid() {
56        let config = VaultTransitSignerFileConfig {
57            key_name: "transit-key".to_string(),
58            address: "https://vault.example.com:8200".to_string(),
59            role_id: PlainOrEnvValue::Plain {
60                value: SecretString::new("role-123"),
61            },
62            secret_id: PlainOrEnvValue::Plain {
63                value: SecretString::new("secret-456"),
64            },
65            pubkey: "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEd+vn+WOG+lGUiJCzHsj8VItmr7Lmdv/Zr+tIhJM7rM+QT9QEzvEX2jWOPyXrvCwUyvVgWoMwUYIo3hd1PFTy7A==".to_string(),
66            mount_point: Some("transit".to_string()),
67            namespace: Some("namespace1".to_string()),
68        };
69
70        assert!(Validate::validate(&config).is_ok());
71        assert!(SignerConfigValidate::validate(&config).is_ok());
72    }
73
74    #[test]
75    fn test_vault_transit_signer_file_config_invalid_address() {
76        let config = VaultTransitSignerFileConfig {
77            key_name: "transit-key".to_string(),
78            address: "not-a-url".to_string(),
79            role_id: PlainOrEnvValue::Plain {
80                value: SecretString::new("role-123"),
81            },
82            secret_id: PlainOrEnvValue::Plain {
83                value: SecretString::new("secret-456"),
84            },
85            pubkey: "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEd+vn+WOG+lGUiJCzHsj8VItmr7Lmdv/Zr+tIhJM7rM+QT9QEzvEX2jWOPyXrvCwUyvVgWoMwUYIo3hd1PFTy7A==".to_string(),
86            mount_point: Some("transit".to_string()),
87            namespace: None,
88        };
89
90        let result = SignerConfigValidate::validate(&config);
91        assert!(result.is_err());
92        if let Err(e) = result {
93            let error_message = format!("{:?}", e);
94            assert!(error_message.contains("address"));
95        }
96    }
97
98    #[test]
99    fn test_vault_transit_signer_file_config_empty_key_name() {
100        let config = VaultTransitSignerFileConfig {
101            key_name: "".to_string(),
102            address: "https://vault.example.com:8200".to_string(),
103            role_id: PlainOrEnvValue::Plain {
104                value: SecretString::new("role-123"),
105            },
106            secret_id: PlainOrEnvValue::Plain {
107                value: SecretString::new("secret-456"),
108            },
109            pubkey: "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEd+vn+WOG+lGUiJCzHsj8VItmr7Lmdv/Zr+tIhJM7rM+QT9QEzvEX2jWOPyXrvCwUyvVgWoMwUYIo3hd1PFTy7A==".to_string(),
110            mount_point: Some("transit".to_string()),
111            namespace: None,
112        };
113
114        let result = SignerConfigValidate::validate(&config);
115        assert!(result.is_err());
116        if let Err(e) = result {
117            let error_message = format!("{:?}", e);
118            assert!(error_message.contains("key_name"));
119            assert!(error_message.contains("cannot be empty"));
120        }
121    }
122
123    #[test]
124    fn test_vault_transit_signer_file_config_empty_role_id() {
125        let config = VaultTransitSignerFileConfig {
126            key_name: "transit-key".to_string(),
127            address: "https://vault.example.com:8200".to_string(),
128            role_id: PlainOrEnvValue::Plain {
129                value: SecretString::new(""),
130            },
131            secret_id: PlainOrEnvValue::Plain {
132                value: SecretString::new("secret-456"),
133            },
134            pubkey: "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEd+vn+WOG+lGUiJCzHsj8VItmr7Lmdv/Zr+tIhJM7rM+QT9QEzvEX2jWOPyXrvCwUyvVgWoMwUYIo3hd1PFTy7A==".to_string(),
135            mount_point: Some("transit".to_string()),
136            namespace: None,
137        };
138
139        let result = SignerConfigValidate::validate(&config);
140        assert!(result.is_err());
141        if let Err(e) = result {
142            let error_message = format!("{:?}", e);
143            assert!(error_message.contains("role_id"));
144        }
145    }
146
147    #[test]
148    fn test_vault_transit_signer_file_config_empty_secret_id() {
149        let config = VaultTransitSignerFileConfig {
150            key_name: "transit-key".to_string(),
151            address: "https://vault.example.com:8200".to_string(),
152            role_id: PlainOrEnvValue::Plain {
153                value: SecretString::new("role-123"),
154            },
155            secret_id: PlainOrEnvValue::Plain {
156                value: SecretString::new(""),
157            },
158            pubkey: "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEd+vn+WOG+lGUiJCzHsj8VItmr7Lmdv/Zr+tIhJM7rM+QT9QEzvEX2jWOPyXrvCwUyvVgWoMwUYIo3hd1PFTy7A==".to_string(),
159            mount_point: Some("transit".to_string()),
160            namespace: None,
161        };
162
163        let result = SignerConfigValidate::validate(&config);
164        assert!(result.is_err());
165        if let Err(e) = result {
166            let error_message = format!("{:?}", e);
167            assert!(error_message.contains("secret_id"));
168        }
169    }
170
171    #[test]
172    fn test_vault_transit_signer_file_config_empty_pubkey() {
173        let config = VaultTransitSignerFileConfig {
174            key_name: "transit-key".to_string(),
175            address: "https://vault.example.com:8200".to_string(),
176            role_id: PlainOrEnvValue::Plain {
177                value: SecretString::new("role-123"),
178            },
179            secret_id: PlainOrEnvValue::Plain {
180                value: SecretString::new("secret-456"),
181            },
182            pubkey: "".to_string(),
183            mount_point: Some("transit".to_string()),
184            namespace: None,
185        };
186
187        let result = SignerConfigValidate::validate(&config);
188        assert!(result.is_err());
189        if let Err(e) = result {
190            let error_message = format!("{:?}", e);
191            assert!(error_message.contains("pubkey"));
192            assert!(error_message.contains("cannot be empty"));
193        }
194    }
195
196    #[test]
197    fn test_vault_transit_signer_file_config_optional_fields() {
198        let config = VaultTransitSignerFileConfig {
199            key_name: "transit-key".to_string(),
200            address: "https://vault.example.com:8200".to_string(),
201            role_id: PlainOrEnvValue::Plain {
202                value: SecretString::new("role-123"),
203            },
204            secret_id: PlainOrEnvValue::Plain {
205                value: SecretString::new("secret-456"),
206            },
207            pubkey: "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEd+vn+WOG+lGUiJCzHsj8VItmr7Lmdv/Zr+tIhJM7rM+QT9QEzvEX2jWOPyXrvCwUyvVgWoMwUYIo3hd1PFTy7A==".to_string(),
208            mount_point: None,
209            namespace: None,
210        };
211
212        assert!(SignerConfigValidate::validate(&config).is_ok());
213    }
214
215    #[test]
216    fn test_vault_transit_signer_file_config_multiple_errors() {
217        let config = VaultTransitSignerFileConfig {
218            key_name: "".to_string(),
219            address: "invalid-url".to_string(),
220            role_id: PlainOrEnvValue::Plain {
221                value: SecretString::new("role-123"),
222            },
223            secret_id: PlainOrEnvValue::Plain {
224                value: SecretString::new("secret-456"),
225            },
226            pubkey: "".to_string(),
227            mount_point: None,
228            namespace: None,
229        };
230
231        let result = validate_with_validator(&config);
232        assert!(result.is_err());
233
234        if let Err(e) = result {
235            if let ConfigFileError::InvalidFormat(msg) = e {
236                assert!(msg.contains("key_name"));
237                assert!(msg.contains("address"));
238                assert!(msg.contains("pubkey"));
239            } else {
240                panic!("Expected ConfigFileError::InvalidFormat, got {:?}", e);
241            }
242        }
243    }
244
245    #[test]
246    fn test_serde_deserialize() {
247        let json = r#"
248        {
249            "key_name": "transit-key",
250            "address": "https://vault.example.com:8200",
251            "role_id": {
252                "type": "plain",
253                "value": "role-123"
254            },
255            "secret_id": {
256                "type": "plain",
257                "value": "secret-456"
258            },
259            "pubkey": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEd+vn+WOG+lGUiJCzHsj8VItmr7Lmdv/Zr+tIhJM7rM+QT9QEzvEX2jWOPyXrvCwUyvVgWoMwUYIo3hd1PFTy7A==",
260            "mount_point": "transit",
261            "namespace": "my-namespace"
262        }
263        "#;
264
265        let config: VaultTransitSignerFileConfig = serde_json::from_str(json).unwrap();
266        assert_eq!(config.key_name, "transit-key");
267        assert_eq!(config.address, "https://vault.example.com:8200");
268        assert_eq!(
269            config.role_id.get_value().unwrap().to_str().as_str(),
270            "role-123"
271        );
272        assert_eq!(
273            config.secret_id.get_value().unwrap().to_str().as_str(),
274            "secret-456"
275        );
276        assert_eq!(config.pubkey, "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEd+vn+WOG+lGUiJCzHsj8VItmr7Lmdv/Zr+tIhJM7rM+QT9QEzvEX2jWOPyXrvCwUyvVgWoMwUYIo3hd1PFTy7A==");
277        assert_eq!(config.mount_point, Some("transit".to_string()));
278        assert_eq!(config.namespace, Some("my-namespace".to_string()));
279    }
280
281    #[test]
282    fn test_serde_unknown_field() {
283        let json = r#"
284        {
285            "key_name": "transit-key",
286            "address": "https://vault.example.com:8200",
287            "role_id": "role-123",
288            "secret_id": "secret-456",
289            "pubkey": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEd+vn+WOG+lGUiJCzHsj8VItmr7Lmdv/Zr+tIhJM7rM+QT9QEzvEX2jWOPyXrvCwUyvVgWoMwUYIo3hd1PFTy7A==",
290            "mount_point": "transit",
291            "namespace": "my-namespace",
292            "unknown_field": "should cause error"
293        }
294        "#;
295
296        let result: Result<VaultTransitSignerFileConfig, _> = serde_json::from_str(json);
297        assert!(result.is_err());
298    }
299
300    #[test]
301    fn test_serde_serialize_deserialize() {
302        let config = VaultTransitSignerFileConfig {
303            key_name: "transit-key".to_string(),
304            address: "https://vault.example.com:8200".to_string(),
305            role_id: PlainOrEnvValue::Plain {
306                value: SecretString::new("role-123"),
307            },
308            secret_id: PlainOrEnvValue::Plain {
309                value: SecretString::new("secret-456"),
310            },
311            pubkey: "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEd+vn+WOG+lGUiJCzHsj8VItmr7Lmdv/Zr+tIhJM7rM+QT9QEzvEX2jWOPyXrvCwUyvVgWoMwUYIo3hd1PFTy7A==".to_string(),
312            mount_point: Some("transit".to_string()),
313            namespace: Some("namespace1".to_string()),
314        };
315
316        let serialized = serde_json::to_string(&config).unwrap();
317        let deserialized: VaultTransitSignerFileConfig = serde_json::from_str(&serialized).unwrap();
318
319        assert_eq!(config.address, deserialized.address);
320        assert_eq!(config.key_name, deserialized.key_name);
321        assert_eq!(config.mount_point, deserialized.mount_point);
322        assert_eq!(config.namespace, deserialized.namespace);
323        assert_eq!(config.pubkey, deserialized.pubkey);
324        assert_ne!(config.role_id, deserialized.role_id);
325        assert_ne!(config.secret_id, deserialized.secret_id);
326    }
327}