1#![doc(html_root_url="https://sfackler.github.io/rust-openssl-verify/doc/v0.2.0")]
38
39extern crate openssl;
40
41use openssl::nid::Nid;
42use openssl::x509::{X509StoreContext, X509Ref, GeneralNames, X509Name};
43use std::net::IpAddr;
44
45pub fn verify_callback(domain: &str, preverify_ok: bool, x509_ctx: &X509StoreContext) -> bool {
53 if !preverify_ok || x509_ctx.error_depth() != 0 {
54 return preverify_ok;
55 }
56
57 match x509_ctx.current_cert() {
58 Some(x509) => verify_hostname(domain, &x509),
59 None => true,
60 }
61}
62
63pub fn verify_hostname(domain: &str, cert: &X509Ref) -> bool {
66 match cert.subject_alt_names() {
67 Some(names) => verify_subject_alt_names(domain, &names),
68 None => verify_subject_name(domain, &cert.subject_name()),
69 }
70}
71
72fn verify_subject_alt_names(domain: &str, names: &GeneralNames) -> bool {
73 let ip = domain.parse();
74
75 for name in names {
76 match ip {
77 Ok(ip) => {
78 if let Some(actual) = name.ipaddress() {
79 if matches_ip(&ip, actual) {
80 return true;
81 }
82 }
83 }
84 Err(_) => {
85 if let Some(pattern) = name.dnsname() {
86 if matches_dns(pattern, domain, false) {
87 return true;
88 }
89 }
90 }
91 }
92 }
93
94 false
95}
96
97fn verify_subject_name(domain: &str, subject_name: &X509Name) -> bool {
98 if let Some(pattern) = subject_name.text_by_nid(Nid::CN) {
99 let is_ip = domain.parse::<IpAddr>().is_ok();
103
104 if matches_dns(&pattern, domain, is_ip) {
105 return true;
106 }
107 }
108
109 false
110}
111
112fn matches_dns(mut pattern: &str, mut hostname: &str, is_ip: bool) -> bool {
113 if pattern.ends_with('.') {
115 pattern = &pattern[..pattern.len() - 1];
116 }
117 if hostname.ends_with('.') {
118 hostname = &hostname[..hostname.len() - 1];
119 }
120
121 matches_wildcard(pattern, hostname, is_ip).unwrap_or_else(|| pattern == hostname)
122}
123
124fn matches_wildcard(pattern: &str, hostname: &str, is_ip: bool) -> Option<bool> {
125 if is_ip || pattern.starts_with("xn--") {
127 return None;
128 }
129
130 let wildcard_location = match pattern.find('*') {
131 Some(l) => l,
132 None => return None,
133 };
134
135 let mut dot_idxs = pattern.match_indices('.').map(|(l, _)| l);
136 let wildcard_end = match dot_idxs.next() {
137 Some(l) => l,
138 None => return None,
139 };
140
141 if dot_idxs.next().is_none() {
151 return None;
152 }
153
154 if wildcard_location > wildcard_end {
156 return None;
157 }
158
159 let hostname_label_end = match hostname.find('.') {
160 Some(l) => l,
161 None => return None,
162 };
163
164 if pattern[wildcard_end..] != hostname[hostname_label_end..] {
166 return Some(false);
167 }
168
169 let wildcard_prefix = &pattern[..wildcard_location];
170 let wildcard_suffix = &pattern[wildcard_location + 1..wildcard_end];
171
172 let hostname_label = &hostname[..hostname_label_end];
173
174 if !hostname_label.starts_with(wildcard_prefix) {
176 return Some(false);
177 }
178
179 if !hostname_label[wildcard_prefix.len()..].ends_with(wildcard_suffix) {
181 return Some(false);
182 }
183
184 Some(true)
185}
186
187fn matches_ip(expected: &IpAddr, actual: &[u8]) -> bool {
188 match (expected, actual.len()) {
189 (&IpAddr::V4(ref addr), 4) => actual == addr.octets(),
190 (&IpAddr::V6(ref addr), 16) => {
191 let segments = [((actual[0] as u16) << 8) | actual[1] as u16,
192 ((actual[2] as u16) << 8) | actual[3] as u16,
193 ((actual[4] as u16) << 8) | actual[5] as u16,
194 ((actual[6] as u16) << 8) | actual[7] as u16,
195 ((actual[8] as u16) << 8) | actual[9] as u16,
196 ((actual[10] as u16) << 8) | actual[11] as u16,
197 ((actual[12] as u16) << 8) | actual[13] as u16,
198 ((actual[14] as u16) << 8) | actual[15] as u16];
199 segments == addr.segments()
200 }
201 _ => false,
202 }
203}
204
205#[cfg(test)]
206mod test {
207 use openssl::ssl::{SslContext, SslMethod, IntoSsl, SslStream, SSL_VERIFY_PEER};
208 use openssl::ssl::HandshakeError;
209 use std::io;
210 use std::net::TcpStream;
211 use std::process::{Command, Child, Stdio};
212 use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering};
213 use std::thread;
214 use std::time::Duration;
215
216 use super::*;
217
218 static NEXT_PORT: AtomicUsize = ATOMIC_USIZE_INIT;
219
220 struct Server {
221 child: Child,
222 port: u16,
223 }
224
225 impl Drop for Server {
226 fn drop(&mut self) {
227 let _ = self.child.kill();
228 }
229 }
230
231 impl Server {
232 fn start(cert: &str, key: &str) -> Server {
233 let port = 15410 + NEXT_PORT.fetch_add(1, Ordering::SeqCst) as u16;
234
235 let child = Command::new("openssl")
236 .arg("s_server")
237 .arg("-accept")
238 .arg(port.to_string())
239 .arg("-cert")
240 .arg(cert)
241 .arg("-key")
242 .arg(key)
243 .stdout(Stdio::null())
244 .stderr(Stdio::null())
245 .stdin(Stdio::piped())
246 .spawn()
247 .unwrap();
248
249 Server {
250 child: child,
251 port: port,
252 }
253 }
254 }
255
256 fn connect(cert: &str, key: &str) -> (Server, TcpStream) {
257 let server = Server::start(cert, key);
258
259 for _ in 0..20 {
260 match TcpStream::connect(("localhost", server.port)) {
261 Ok(s) => return (server, s),
262 Err(ref e) if e.kind() == io::ErrorKind::ConnectionRefused => {
263 thread::sleep(Duration::from_millis(100));
264 }
265 Err(e) => panic!("failed to connect: {}", e),
266 }
267 }
268 panic!("server never came online");
269 }
270
271 fn negotiate(cert: &str,
272 key: &str,
273 domain: &str)
274 -> Result<SslStream<TcpStream>, HandshakeError<TcpStream>> {
275 let (_server, stream) = connect(cert, key);
276
277 let mut ctx = SslContext::new(SslMethod::Sslv23).unwrap();
278 ctx.set_CA_file(cert).unwrap();
279 let mut ssl = ctx.into_ssl().unwrap();
280
281 let domain = domain.to_owned();
282 ssl.set_verify_callback(SSL_VERIFY_PEER, move |p, x| verify_callback(&domain, p, x));
283
284 SslStream::connect(ssl, stream)
285 }
286
287 #[test]
288 fn google_valid() {
289 let stream = TcpStream::connect("google.com:443").unwrap();
290 let mut ctx = SslContext::new(SslMethod::Sslv23).unwrap();
291 ctx.set_default_verify_paths().unwrap();
292 let mut ssl = ctx.into_ssl().unwrap();
293
294 ssl.set_verify_callback(SSL_VERIFY_PEER, |p, x| verify_callback("google.com", p, x));
295
296 SslStream::connect(ssl, stream).unwrap();
297 }
298
299 #[test]
300 fn google_bad_domain() {
301 let stream = TcpStream::connect("google.com:443").unwrap();
302 let mut ctx = SslContext::new(SslMethod::Sslv23).unwrap();
303 ctx.set_default_verify_paths().unwrap();
304 let mut ssl = ctx.into_ssl().unwrap();
305
306 ssl.set_verify_callback(SSL_VERIFY_PEER, |p, x| verify_callback("foo.com", p, x));
307
308 SslStream::connect(ssl, stream).unwrap_err();
309 }
310
311 #[test]
312 fn valid_sname() {
313 negotiate("test/valid-sn.cert.pem",
314 "test/valid-sn.key.pem",
315 "foobar.com")
316 .unwrap();
317 }
318
319 #[test]
320 fn invalid_sname() {
321 negotiate("test/valid-sn.cert.pem",
322 "test/valid-sn.key.pem",
323 "fizzbuzz.com")
324 .unwrap_err();
325 }
326
327 #[test]
328 fn sans_prefered_to_cn() {
329 negotiate("test/valid-san.cert.pem",
330 "test/valid-san.key.pem",
331 "foobar.com")
332 .unwrap_err();
333 }
334
335 #[test]
336 fn valid_double_wildcard() {
337 negotiate("test/valid-san.cert.pem",
338 "test/valid-san.key.pem",
339 "headfootail.doublewild.com")
340 .unwrap();
341 }
342
343 #[test]
344 fn valid_double_wildcard_minimal() {
345 negotiate("test/valid-san.cert.pem",
346 "test/valid-san.key.pem",
347 "headtail.doublewild.com")
348 .unwrap();
349 }
350
351 #[test]
352 fn invalid_double_wildcard_footer() {
353 negotiate("test/valid-san.cert.pem",
354 "test/valid-san.key.pem",
355 "headfootaill.doublewild.com")
356 .unwrap_err();
357 }
358
359 #[test]
360 fn invalid_double_wildcard_header() {
361 negotiate("test/valid-san.cert.pem",
362 "test/valid-san.key.pem",
363 "bheadfootaill.doublewild.com")
364 .unwrap_err();
365 }
366
367 #[test]
368 fn valid_tail_wildcard() {
369 negotiate("test/valid-san.cert.pem",
370 "test/valid-san.key.pem",
371 "footail.tailwild.com")
372 .unwrap();
373 }
374
375 #[test]
376 fn valid_tail_wildcard_minimal() {
377 negotiate("test/valid-san.cert.pem",
378 "test/valid-san.key.pem",
379 "tail.tailwild.com")
380 .unwrap();
381 }
382
383 #[test]
384 fn invalid_tail_wildcard() {
385 negotiate("test/valid-san.cert.pem",
386 "test/valid-san.key.pem",
387 "footaill.tailwild.com")
388 .unwrap_err();
389 }
390
391 #[test]
392 fn valid_head_wildcard() {
393 negotiate("test/valid-san.cert.pem",
394 "test/valid-san.key.pem",
395 "headfoo.headwild.com")
396 .unwrap();
397 }
398
399 #[test]
400 fn valid_head_wildcard_minimal() {
401 negotiate("test/valid-san.cert.pem",
402 "test/valid-san.key.pem",
403 "head.headwild.com")
404 .unwrap();
405 }
406
407 #[test]
408 fn invalid_head_wildcard() {
409 negotiate("test/valid-san.cert.pem",
410 "test/valid-san.key.pem",
411 "bheadfoo.headwild.com")
412 .unwrap_err();
413 }
414
415 #[test]
416 fn valid_bare_wildcard() {
417 negotiate("test/valid-san.cert.pem",
418 "test/valid-san.key.pem",
419 "foo.barewild.com")
420 .unwrap();
421 }
422
423 #[test]
424 fn invalid_wildcard_too_deep() {
425 negotiate("test/valid-san.cert.pem",
426 "test/valid-san.key.pem",
427 "bar.foo.barewild.com")
428 .unwrap_err();
429 }
430
431 #[test]
432 fn invalid_wildcard_too_short() {
433 negotiate("test/valid-san.cert.pem",
434 "test/valid-san.key.pem",
435 "barewild.com")
436 .unwrap_err();
437 }
438
439 #[test]
440 fn valid_ipv4() {
441 negotiate("test/valid-san.cert.pem",
442 "test/valid-san.key.pem",
443 "192.168.1.1")
444 .unwrap();
445 }
446
447 #[test]
448 fn invalid_ipv4() {
449 negotiate("test/valid-san.cert.pem",
450 "test/valid-san.key.pem",
451 "192.168.1.2")
452 .unwrap_err();
453 }
454
455 #[test]
456 fn valid_ipv6() {
457 negotiate("test/valid-san.cert.pem",
458 "test/valid-san.key.pem",
459 "2001:DB8:85A3:0:0:8A2E:370:7334")
460 .unwrap();
461 }
462
463 #[test]
464 fn invalid_ipv6() {
465 negotiate("test/valid-san.cert.pem",
466 "test/valid-san.key.pem",
467 "2001:DB8:85A3:0:0:8A2E:370:7335")
468 .unwrap_err();
469 }
470
471 #[test]
472 fn bogus_wildcard_not_last() {
473 negotiate("test/invalid-san.cert.pem",
474 "test/invalid-san.key.pem",
475 "server1.foo.example.com")
476 .unwrap_err();
477 }
478
479 #[test]
480 fn bogus_wildcard_too_short() {
481 negotiate("test/invalid-san.cert.pem",
482 "test/invalid-san.key.pem",
483 "foo.com")
484 .unwrap_err();
485 }
486}