openssl_verify/
lib.rs

1//! Hostname verification for OpenSSL.
2//!
3//! OpenSSL up until version 1.1.0 did not support verification that the
4//! certificate a server presents matches the domain a client is connecting to.
5//! This check is crucial, as an attacker otherwise needs only to obtain a
6//! legitimately signed certificate to *some* domain to execute a
7//! man-in-the-middle attack.
8//!
9//! The implementation in this crate is based off of libcurl's.
10//!
11//! # Examples
12//!
13//! In most cases, the `verify_callback` function should be used in OpenSSL's
14//! verification callback:
15//!
16//! ```
17//! extern crate openssl;
18//! extern crate openssl_verify;
19//!
20//! use std::net::TcpStream;
21//! use openssl::ssl::{SslContext, SslMethod, SslStream, SSL_VERIFY_PEER, IntoSsl};
22//! use openssl_verify::verify_callback;
23//!
24//! # fn main() {
25//! let domain = "google.com";
26//! let stream = TcpStream::connect((domain, 443)).unwrap();
27//!
28//! let mut ctx = SslContext::new(SslMethod::Sslv23).unwrap();
29//! ctx.set_default_verify_paths().unwrap();
30//!
31//! let mut ssl = ctx.into_ssl().unwrap();
32//! let domain = domain.to_owned();
33//! ssl.set_verify_callback(SSL_VERIFY_PEER, move |p, x| verify_callback(&domain, p, x));
34//!
35//! let ssl_stream = SslStream::connect(ssl, stream).unwrap();
36//! # }
37#![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
45/// A convenience wrapper around verify_hostname that implements the logic for
46/// OpenSSL's certificate verification callback.
47///
48/// If `preverify_ok` is false or the certificate depth is not 0, it will
49/// simply return the value of `preverify_ok`. It will otherwise validate the
50/// that the provided fully qualified domain name matches that of the leaf
51/// certificate.
52pub 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
63/// Validates that the certificate matches the provided fully qualified domain
64/// name.
65pub 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        // Unlike with SANs, IP addresses in the subject name don't have a
100        // different encoding. We need to pass this down to matches_dns to
101        // disallow wildcard matches with bogus patterns like *.0.0.1
102        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    // first strip trailing . off of pattern and hostname to normalize
114    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    // IP addresses and internationalized domains can't involved in wildcards
126    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    // Never match wildcards if the pattern has less than 2 '.'s (no *.com)
142    //
143    // This is a bit dubious, as it doesn't disallow other TLDs like *.co.uk.
144    // Chrome has a black- and white-list for this, but Firefox (via NSS) does
145    // the same thing we do here.
146    //
147    // The Public Suffix (https://www.publicsuffix.org/) list could
148    // potentically be used here, but it's both huge and updated frequently
149    // enough that management would be a PITA.
150    if dot_idxs.next().is_none() {
151        return None;
152    }
153
154    // Wildcards can only be in the first component
155    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    // check that the non-wildcard parts are identical
165    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    // check the prefix of the first label
175    if !hostname_label.starts_with(wildcard_prefix) {
176        return Some(false);
177    }
178
179    // and the suffix
180    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}