1use crate::FaucetError;
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12#[derive(Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
20pub struct TlsClientConfig {
21 pub client_cert: Option<String>,
23 pub client_key: Option<String>,
25 #[schemars(extend("x-faucet-resource" = "path"))]
28 pub client_identity_pkcs12: Option<String>,
29 pub pkcs12_password: Option<String>,
31 pub min_version: Option<String>,
34}
35
36impl std::fmt::Debug for TlsClientConfig {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 let secret = |v: &Option<String>| v.as_ref().map(|_| "***");
39 f.debug_struct("TlsClientConfig")
40 .field("client_cert", &self.client_cert)
41 .field("client_key", &secret(&self.client_key))
42 .field("client_identity_pkcs12", &self.client_identity_pkcs12)
43 .field("pkcs12_password", &secret(&self.pkcs12_password))
44 .field("min_version", &self.min_version)
45 .finish()
46 }
47}
48
49impl TlsClientConfig {
50 pub fn validate(&self) -> Result<(), FaucetError> {
55 let has_pem = self.client_cert.is_some() || self.client_key.is_some();
56 let has_p12 = self.client_identity_pkcs12.is_some();
57 if has_pem && has_p12 {
58 return Err(FaucetError::Config(
59 "tls: specify either a PEM pair (client_cert + client_key) or \
60 client_identity_pkcs12, not both"
61 .into(),
62 ));
63 }
64 if !has_pem && !has_p12 {
65 return Err(FaucetError::Config(
66 "tls: provide a PEM pair (client_cert + client_key) or \
67 client_identity_pkcs12"
68 .into(),
69 ));
70 }
71 if has_pem && (self.client_cert.is_none() || self.client_key.is_none()) {
72 return Err(FaucetError::Config(
73 "tls: client_cert and client_key must be provided together".into(),
74 ));
75 }
76 if let Some(v) = &self.min_version
77 && !matches!(v.as_str(), "1.2" | "1.3")
78 {
79 return Err(FaucetError::Config(format!(
80 "tls: unsupported min_version {v:?} (expected \"1.2\" or \"1.3\")"
81 )));
82 }
83 Ok(())
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::TlsClientConfig;
90
91 #[test]
92 fn the_pkcs12_path_is_a_declared_resource() {
93 let schema = serde_json::to_value(schemars::schema_for!(TlsClientConfig)).unwrap();
94 let config = serde_json::json!({"client_identity_pkcs12": "/etc/id.p12"});
95 let refs = crate::resolve::annotated_resources(&schema, &config);
96 assert!(
97 refs.iter()
98 .any(|r| r.kind == crate::resolve::ResourceKind::Path && r.value == "/etc/id.p12"),
99 "{refs:?}"
100 );
101 }
102
103 #[test]
104 fn debug_never_prints_key_material() {
105 let cfg = TlsClientConfig {
106 client_cert: Some("CERT".into()),
107 client_key: Some("PRIVATE-KEY-PEM".into()),
108 client_identity_pkcs12: None,
109 pkcs12_password: Some("p12-pass".into()),
110 min_version: None,
111 };
112 let text = format!("{cfg:?}");
113 assert!(
114 !text.contains("PRIVATE-KEY-PEM") && !text.contains("p12-pass"),
115 "{text}"
116 );
117 assert!(text.contains("CERT") && text.contains("***"), "{text}");
118 }
119
120 fn pem() -> TlsClientConfig {
121 TlsClientConfig {
122 client_cert: Some("cert".into()),
123 client_key: Some("key".into()),
124 ..Default::default()
125 }
126 }
127
128 #[test]
129 fn valid_pem_pair_passes() {
130 assert!(pem().validate().is_ok());
131 }
132
133 #[test]
134 fn valid_pkcs12_passes() {
135 let c = TlsClientConfig {
136 client_identity_pkcs12: Some("/path/id.p12".into()),
137 pkcs12_password: Some("pw".into()),
138 ..Default::default()
139 };
140 assert!(c.validate().is_ok());
141 }
142
143 #[test]
144 fn pem_and_pkcs12_together_is_rejected() {
145 let c = TlsClientConfig {
146 client_cert: Some("cert".into()),
147 client_key: Some("key".into()),
148 client_identity_pkcs12: Some("/path/id.p12".into()),
149 ..Default::default()
150 };
151 assert!(c.validate().is_err());
152 }
153
154 #[test]
155 fn empty_config_is_rejected() {
156 assert!(TlsClientConfig::default().validate().is_err());
157 }
158
159 #[test]
160 fn half_pem_pair_is_rejected() {
161 let cert_only = TlsClientConfig {
162 client_cert: Some("cert".into()),
163 ..Default::default()
164 };
165 assert!(cert_only.validate().is_err());
166 let key_only = TlsClientConfig {
167 client_key: Some("key".into()),
168 ..Default::default()
169 };
170 assert!(key_only.validate().is_err());
171 }
172
173 #[test]
174 fn min_version_is_validated() {
175 let mut c = pem();
176 c.min_version = Some("1.3".into());
177 assert!(c.validate().is_ok());
178 c.min_version = Some("1.2".into());
179 assert!(c.validate().is_ok());
180 c.min_version = Some("1.1".into());
181 assert!(c.validate().is_err());
182 }
183}