1use std::path::PathBuf;
33
34use super::{Role, User};
35
36#[derive(Debug, Clone)]
39pub struct CertAuthConfig {
40 pub enabled: bool,
43 pub trust_bundle: PathBuf,
46 pub identity_mode: CertIdentityMode,
48 pub role_oid: Option<String>,
51 pub default_role: Role,
53 pub map_to_existing_users: bool,
57}
58
59impl Default for CertAuthConfig {
60 fn default() -> Self {
61 Self {
62 enabled: false,
63 trust_bundle: PathBuf::from("./certs/client-ca.pem"),
64 identity_mode: CertIdentityMode::CommonName,
65 role_oid: None,
66 default_role: Role::Read,
67 map_to_existing_users: true,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum CertIdentityMode {
75 CommonName,
77 SanRfc822Name,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct CertIdentity {
88 pub username: String,
89 pub role: Role,
90 pub subject_dn: String,
93 pub serial_hex: String,
95 pub not_after_unix_secs: i64,
98}
99
100#[derive(Debug, Clone)]
102pub enum CertAuthError {
103 MissingIdentity(CertIdentityMode),
106 MissingRoleExtension(String),
109 UnknownUser(String),
111 Expired { not_after_unix_secs: i64 },
113 TrustBundle(String),
115 Parse(String),
117}
118
119impl std::fmt::Display for CertAuthError {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {
122 CertAuthError::MissingIdentity(mode) => {
123 write!(f, "client cert missing {:?} identity field", mode)
124 }
125 CertAuthError::MissingRoleExtension(oid) => {
126 write!(f, "client cert missing role extension {oid}")
127 }
128 CertAuthError::UnknownUser(u) => write!(f, "cert user '{u}' not in auth store"),
129 CertAuthError::Expired {
130 not_after_unix_secs,
131 } => write!(f, "client cert expired at unix {not_after_unix_secs}"),
132 CertAuthError::TrustBundle(m) => write!(f, "trust bundle error: {m}"),
133 CertAuthError::Parse(m) => write!(f, "cert parse error: {m}"),
134 }
135 }
136}
137
138impl std::error::Error for CertAuthError {}
139
140#[derive(Debug, Clone)]
145pub struct ParsedClientCert {
146 pub subject_dn: String,
147 pub common_name: Option<String>,
148 pub san_rfc822: Vec<String>,
149 pub serial_hex: String,
150 pub not_after_unix_secs: i64,
151 pub extensions: std::collections::HashMap<String, Vec<u8>>,
155}
156
157pub struct CertAuthenticator {
161 config: CertAuthConfig,
162}
163
164impl CertAuthenticator {
165 pub fn new(config: CertAuthConfig) -> Self {
166 Self { config }
167 }
168
169 pub fn validate<F>(
175 &self,
176 cert: &ParsedClientCert,
177 now_unix_secs: i64,
178 lookup_user: F,
179 ) -> Result<CertIdentity, CertAuthError>
180 where
181 F: Fn(&str) -> Option<User>,
182 {
183 if !self.config.enabled {
184 return Err(CertAuthError::Parse(
185 "cert auth disabled on this listener".into(),
186 ));
187 }
188
189 if cert.not_after_unix_secs < now_unix_secs {
190 return Err(CertAuthError::Expired {
191 not_after_unix_secs: cert.not_after_unix_secs,
192 });
193 }
194
195 let username = match self.config.identity_mode {
196 CertIdentityMode::CommonName => cert
197 .common_name
198 .clone()
199 .ok_or(CertAuthError::MissingIdentity(CertIdentityMode::CommonName))?,
200 CertIdentityMode::SanRfc822Name => {
201 cert.san_rfc822
202 .first()
203 .cloned()
204 .ok_or(CertAuthError::MissingIdentity(
205 CertIdentityMode::SanRfc822Name,
206 ))?
207 }
208 };
209
210 let role = if self.config.map_to_existing_users {
212 match lookup_user(&username) {
213 Some(user) => user.role,
214 None => self.derive_role_from_cert(cert)?,
215 }
216 } else {
217 self.derive_role_from_cert(cert)?
218 };
219
220 Ok(CertIdentity {
221 username,
222 role,
223 subject_dn: cert.subject_dn.clone(),
224 serial_hex: cert.serial_hex.clone(),
225 not_after_unix_secs: cert.not_after_unix_secs,
226 })
227 }
228
229 fn derive_role_from_cert(&self, cert: &ParsedClientCert) -> Result<Role, CertAuthError> {
230 let Some(oid) = &self.config.role_oid else {
231 return Ok(self.config.default_role);
232 };
233 let bytes = cert
234 .extensions
235 .get(oid)
236 .ok_or_else(|| CertAuthError::MissingRoleExtension(oid.clone()))?;
237 let name = std::str::from_utf8(bytes)
241 .map_err(|e| CertAuthError::Parse(format!("role extension not valid UTF-8: {e}")))?;
242 Role::from_str(name.trim())
243 .ok_or_else(|| CertAuthError::Parse(format!("unknown role '{name}'")))
244 }
245
246 pub fn config(&self) -> &CertAuthConfig {
247 &self.config
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use std::collections::HashMap;
255
256 fn base_cert() -> ParsedClientCert {
257 ParsedClientCert {
258 subject_dn: "CN=alice,O=reddb,C=BR".to_string(),
259 common_name: Some("alice".to_string()),
260 san_rfc822: vec!["alice@example.com".to_string()],
261 serial_hex: "ABCDEF".to_string(),
262 not_after_unix_secs: 2_000_000_000,
263 extensions: HashMap::new(),
264 }
265 }
266
267 fn cfg(mode: CertIdentityMode) -> CertAuthConfig {
268 CertAuthConfig {
269 enabled: true,
270 identity_mode: mode,
271 ..CertAuthConfig::default()
272 }
273 }
274
275 #[test]
276 fn common_name_maps_to_username() {
277 let auth = CertAuthenticator::new(cfg(CertIdentityMode::CommonName));
278 let id = auth
279 .validate(&base_cert(), 1_000_000_000, |_| None)
280 .unwrap();
281 assert_eq!(id.username, "alice");
282 assert_eq!(id.role, Role::Read);
283 }
284
285 #[test]
286 fn san_rfc822_maps_to_email() {
287 let auth = CertAuthenticator::new(cfg(CertIdentityMode::SanRfc822Name));
288 let id = auth
289 .validate(&base_cert(), 1_000_000_000, |_| None)
290 .unwrap();
291 assert_eq!(id.username, "alice@example.com");
292 }
293
294 #[test]
295 fn missing_cn_field_rejected() {
296 let mut cert = base_cert();
297 cert.common_name = None;
298 let auth = CertAuthenticator::new(cfg(CertIdentityMode::CommonName));
299 let err = auth.validate(&cert, 1_000_000_000, |_| None).unwrap_err();
300 assert!(matches!(err, CertAuthError::MissingIdentity(_)));
301 }
302
303 #[test]
304 fn expired_cert_rejected() {
305 let mut cert = base_cert();
306 cert.not_after_unix_secs = 500;
307 let auth = CertAuthenticator::new(cfg(CertIdentityMode::CommonName));
308 let err = auth.validate(&cert, 1_000, |_| None).unwrap_err();
309 assert!(matches!(err, CertAuthError::Expired { .. }));
310 }
311
312 #[test]
313 fn role_extension_overrides_default_role() {
314 let mut cert = base_cert();
315 cert.extensions
316 .insert("1.3.6.1.4.1.99999.1".to_string(), b"admin".to_vec());
317 let mut config = cfg(CertIdentityMode::CommonName);
318 config.role_oid = Some("1.3.6.1.4.1.99999.1".to_string());
319 config.map_to_existing_users = false;
320 let auth = CertAuthenticator::new(config);
321 let id = auth.validate(&cert, 1_000_000_000, |_| None).unwrap();
322 assert_eq!(id.role, Role::Admin);
323 }
324
325 #[test]
326 fn missing_role_extension_errors_when_configured() {
327 let mut config = cfg(CertIdentityMode::CommonName);
328 config.role_oid = Some("1.2.3".to_string());
329 config.map_to_existing_users = false;
330 let auth = CertAuthenticator::new(config);
331 let err = auth
332 .validate(&base_cert(), 1_000_000_000, |_| None)
333 .unwrap_err();
334 assert!(matches!(err, CertAuthError::MissingRoleExtension(_)));
335 }
336}