oracledb_protocol/tls/
dn.rs1#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum DnMatchError {
23 CertDnMismatch { expected_dn: String },
26 NameMismatch { expected_name: String },
29}
30
31impl core::fmt::Display for DnMatchError {
32 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
33 match self {
34 Self::CertDnMismatch { expected_dn } => write!(
35 f,
36 "the distinguished name (DN) on the server certificate does not match \
37 the expected value \"{expected_dn}\""
38 ),
39 Self::NameMismatch { expected_name } => write!(
40 f,
41 "the server name \"{expected_name}\" does not match the names in the \
42 server certificate"
43 ),
44 }
45 }
46}
47
48impl std::error::Error for DnMatchError {}
49
50#[must_use]
63pub fn parse_dn(dn: &str) -> Vec<(String, String)> {
64 let mut out: Vec<(String, String)> = Vec::new();
65 let bytes: Vec<char> = dn.chars().collect();
66 let mut i = 0usize;
67 let n = bytes.len();
68 while i < n {
69 if bytes[i] == ',' {
71 i += 1;
72 if i < n && bytes[i] == ' ' {
73 i += 1;
74 }
75 }
76 while i < n && bytes[i] == ' ' {
78 i += 1;
79 }
80 if i >= n {
81 break;
82 }
83 let name_start = i;
85 while i < n && bytes[i].is_ascii_uppercase() {
86 i += 1;
87 }
88 if i == name_start || i >= n || bytes[i] != '=' {
89 while i < n && bytes[i] != ',' {
91 i += 1;
92 }
93 continue;
94 }
95 let name: String = bytes[name_start..i].iter().collect();
96 i += 1; let value = if i < n && bytes[i] == '"' {
100 i += 1; let mut val = String::new();
102 while i < n {
103 if bytes[i] == '"' {
104 if i + 1 < n && bytes[i + 1] == '"' {
105 val.push('"');
107 i += 2;
108 } else {
109 i += 1; break;
111 }
112 } else {
113 val.push(bytes[i]);
114 i += 1;
115 }
116 }
117 val
118 } else {
119 let val_start = i;
120 while i < n && bytes[i] != ',' {
121 i += 1;
122 }
123 let mut val: String = bytes[val_start..i].iter().collect();
124 if val.ends_with(' ') {
131 val = val.trim_end_matches(' ').to_string();
133 }
134 val
135 };
136 out.push((name, value));
137 }
138 out.sort();
139 out
140}
141
142pub fn check_cert_dn(expected_dn: &str, server_subject_dn: &str) -> Result<(), DnMatchError> {
149 let expected = parse_dn(expected_dn);
150 let server = parse_dn(server_subject_dn);
151 if expected == server {
152 Ok(())
153 } else {
154 Err(DnMatchError::CertDnMismatch {
155 expected_dn: expected_dn.to_string(),
156 })
157 }
158}
159
160#[must_use]
164pub fn name_matches(name_to_check: &str, cert_name: &str) -> bool {
165 let cert_name = cert_name.to_lowercase();
166 let name_to_check = name_to_check.to_lowercase();
167
168 if name_to_check == cert_name {
170 return true;
171 }
172
173 let check_pos = name_to_check.find('.');
175 let cert_pos = cert_name.find('.');
176 let (Some(check_pos), Some(cert_pos)) = (check_pos, cert_pos) else {
177 return false;
178 };
179 if check_pos == 0 || cert_pos == 0 {
180 return false;
181 }
182
183 if name_to_check[check_pos..] != cert_name[cert_pos..] {
185 return false;
186 }
187
188 let cert_label = &cert_name[..cert_pos];
190 let check_label = &name_to_check[..check_pos];
191 if cert_label == "*" {
192 return true;
193 } else if let Some(suffix) = cert_label.strip_prefix('*') {
194 return check_label.ends_with(suffix);
195 } else if let Some(prefix) = cert_label.strip_suffix('*') {
196 return check_label.starts_with(prefix);
197 }
198 match cert_name.find('*') {
200 None => false,
201 Some(_) => {
202 let wildcard_pos = cert_name.find('*').unwrap_or(0);
205 let pre = &cert_name[..wildcard_pos];
206 let post_start = wildcard_pos + 1;
207 let post = if post_start <= cert_label.len() {
211 &cert_label[post_start..]
212 } else {
213 ""
214 };
215 check_label.starts_with(pre) && check_label.ends_with(post)
216 }
217 }
218}
219
220pub fn check_server_name(
228 expected_name: &str,
229 san_dns_names: &[String],
230 common_names: &[String],
231) -> Result<(), DnMatchError> {
232 for name in san_dns_names {
233 if name_matches(expected_name, name) {
234 return Ok(());
235 }
236 }
237 for name in common_names {
238 if name_matches(expected_name, name) {
239 return Ok(());
240 }
241 }
242 Err(DnMatchError::NameMismatch {
243 expected_name: expected_name.to_string(),
244 })
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn parse_dn_simple() {
253 let parsed = parse_dn("CN=db.example.com,O=Example,C=US");
254 assert_eq!(
255 parsed,
256 vec![
257 ("C".to_string(), "US".to_string()),
258 ("CN".to_string(), "db.example.com".to_string()),
259 ("O".to_string(), "Example".to_string()),
260 ]
261 );
262 }
263
264 #[test]
265 fn parse_dn_order_independent_equality() {
266 let a = parse_dn("CN=x,O=y");
267 let b = parse_dn("O=y,CN=x");
268 assert_eq!(a, b);
269 }
270
271 #[test]
272 fn parse_dn_comma_space_separator() {
273 let a = parse_dn("CN=x, O=y, C=Z");
274 assert_eq!(
275 a,
276 vec![
277 ("C".to_string(), "Z".to_string()),
278 ("CN".to_string(), "x".to_string()),
279 ("O".to_string(), "y".to_string()),
280 ]
281 );
282 }
283
284 #[test]
285 fn parse_dn_quoted_value() {
286 let a = parse_dn(r#"CN="Acme, Inc.",C=US"#);
287 assert!(a.contains(&("CN".to_string(), "Acme, Inc.".to_string())));
289 assert!(a.contains(&("C".to_string(), "US".to_string())));
290 }
291
292 #[test]
293 fn check_cert_dn_accept_exact() {
294 assert!(check_cert_dn("CN=x,O=y", "O=y,CN=x").is_ok());
295 }
296
297 #[test]
298 fn check_cert_dn_reject_diff() {
299 let err = check_cert_dn("CN=x,O=y", "CN=z,O=y").unwrap_err();
300 assert!(matches!(err, DnMatchError::CertDnMismatch { .. }));
301 }
302
303 #[test]
304 fn check_cert_dn_reject_extra_attr() {
305 let err = check_cert_dn("CN=x", "CN=x,O=y").unwrap_err();
306 assert!(matches!(err, DnMatchError::CertDnMismatch { .. }));
307 }
308
309 #[test]
310 fn name_matches_full_case_insensitive() {
311 assert!(name_matches("DB.example.com", "db.example.COM"));
312 }
313
314 #[test]
315 fn name_matches_leading_wildcard() {
316 assert!(name_matches("host.example.com", "*.example.com"));
317 assert!(!name_matches("host.sub.example.com", "*.example.com"));
318 }
319
320 #[test]
321 fn name_matches_prefix_wildcard_label() {
322 assert!(name_matches("webserver.example.com", "web*.example.com"));
324 assert!(!name_matches("appserver.example.com", "web*.example.com"));
325 }
326
327 #[test]
328 fn name_matches_suffix_wildcard_label() {
329 assert!(name_matches("serverweb.example.com", "*web.example.com"));
330 }
331
332 #[test]
333 fn name_matches_rejects_single_label() {
334 assert!(!name_matches("localhost", "*"));
335 }
336
337 #[test]
338 fn check_server_name_san_first() {
339 assert!(check_server_name("db.example.com", &["db.example.com".to_string()], &[]).is_ok());
340 }
341
342 #[test]
343 fn check_server_name_falls_back_to_cn() {
344 assert!(check_server_name("db.example.com", &[], &["db.example.com".to_string()]).is_ok());
345 }
346
347 #[test]
348 fn check_server_name_rejects_unknown() {
349 let err = check_server_name("evil.example.com", &["db.example.com".to_string()], &[])
350 .unwrap_err();
351 assert!(matches!(err, DnMatchError::NameMismatch { .. }));
352 }
353}