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 pub client_identity_pkcs12: Option<String>,
28 pub pkcs12_password: Option<String>,
30 pub min_version: Option<String>,
33}
34
35impl std::fmt::Debug for TlsClientConfig {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 let secret = |v: &Option<String>| v.as_ref().map(|_| "***");
38 f.debug_struct("TlsClientConfig")
39 .field("client_cert", &self.client_cert)
40 .field("client_key", &secret(&self.client_key))
41 .field("client_identity_pkcs12", &self.client_identity_pkcs12)
42 .field("pkcs12_password", &secret(&self.pkcs12_password))
43 .field("min_version", &self.min_version)
44 .finish()
45 }
46}
47
48impl TlsClientConfig {
49 pub fn validate(&self) -> Result<(), FaucetError> {
54 let has_pem = self.client_cert.is_some() || self.client_key.is_some();
55 let has_p12 = self.client_identity_pkcs12.is_some();
56 if has_pem && has_p12 {
57 return Err(FaucetError::Config(
58 "tls: specify either a PEM pair (client_cert + client_key) or \
59 client_identity_pkcs12, not both"
60 .into(),
61 ));
62 }
63 if !has_pem && !has_p12 {
64 return Err(FaucetError::Config(
65 "tls: provide a PEM pair (client_cert + client_key) or \
66 client_identity_pkcs12"
67 .into(),
68 ));
69 }
70 if has_pem && (self.client_cert.is_none() || self.client_key.is_none()) {
71 return Err(FaucetError::Config(
72 "tls: client_cert and client_key must be provided together".into(),
73 ));
74 }
75 if let Some(v) = &self.min_version
76 && !matches!(v.as_str(), "1.2" | "1.3")
77 {
78 return Err(FaucetError::Config(format!(
79 "tls: unsupported min_version {v:?} (expected \"1.2\" or \"1.3\")"
80 )));
81 }
82 Ok(())
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::TlsClientConfig;
89
90 #[test]
91 fn debug_never_prints_key_material() {
92 let cfg = TlsClientConfig {
93 client_cert: Some("CERT".into()),
94 client_key: Some("PRIVATE-KEY-PEM".into()),
95 client_identity_pkcs12: None,
96 pkcs12_password: Some("p12-pass".into()),
97 min_version: None,
98 };
99 let text = format!("{cfg:?}");
100 assert!(
101 !text.contains("PRIVATE-KEY-PEM") && !text.contains("p12-pass"),
102 "{text}"
103 );
104 assert!(text.contains("CERT") && text.contains("***"), "{text}");
105 }
106
107 fn pem() -> TlsClientConfig {
108 TlsClientConfig {
109 client_cert: Some("cert".into()),
110 client_key: Some("key".into()),
111 ..Default::default()
112 }
113 }
114
115 #[test]
116 fn valid_pem_pair_passes() {
117 assert!(pem().validate().is_ok());
118 }
119
120 #[test]
121 fn valid_pkcs12_passes() {
122 let c = TlsClientConfig {
123 client_identity_pkcs12: Some("/path/id.p12".into()),
124 pkcs12_password: Some("pw".into()),
125 ..Default::default()
126 };
127 assert!(c.validate().is_ok());
128 }
129
130 #[test]
131 fn pem_and_pkcs12_together_is_rejected() {
132 let c = TlsClientConfig {
133 client_cert: Some("cert".into()),
134 client_key: Some("key".into()),
135 client_identity_pkcs12: Some("/path/id.p12".into()),
136 ..Default::default()
137 };
138 assert!(c.validate().is_err());
139 }
140
141 #[test]
142 fn empty_config_is_rejected() {
143 assert!(TlsClientConfig::default().validate().is_err());
144 }
145
146 #[test]
147 fn half_pem_pair_is_rejected() {
148 let cert_only = TlsClientConfig {
149 client_cert: Some("cert".into()),
150 ..Default::default()
151 };
152 assert!(cert_only.validate().is_err());
153 let key_only = TlsClientConfig {
154 client_key: Some("key".into()),
155 ..Default::default()
156 };
157 assert!(key_only.validate().is_err());
158 }
159
160 #[test]
161 fn min_version_is_validated() {
162 let mut c = pem();
163 c.min_version = Some("1.3".into());
164 assert!(c.validate().is_ok());
165 c.min_version = Some("1.2".into());
166 assert!(c.validate().is_ok());
167 c.min_version = Some("1.1".into());
168 assert!(c.validate().is_err());
169 }
170}