ssl_expiration2/
lib.rs

1//! Checks SSL certificate expiration.
2//!
3//! This crate will try to connect a remote server and check SSL certificate expiration.
4//!
5//! Basic usage example:
6//!
7//! ```rust
8//! use ssl_expiration2::SslExpiration;
9//!
10//! let expiration = SslExpiration::from_domain_name("google.com").unwrap();
11//! if expiration.is_expired() {
12//!     // do something if SSL certificate expired
13//! }
14//! ```
15//!
16//! Check days before expiration example:
17//!
18//! ```rust
19//! use ssl_expiration2::SslExpiration;
20//!
21//! let expiration =
22//!     SslExpiration::from_domain_name("google.com").expect("Domain validation has to work");
23//! if expiration.days() < 14 {
24//!     // SSL certificate will expire in less than 2 weeks, run notification…
25//! }
26//! ```
27
28
29#[macro_use]
30extern crate error_chain;
31
32use error::{Error, ErrorKind, Result};
33use error_chain::State;
34use openssl::{
35    asn1::*,
36    ssl::{Ssl, SslContext, SslMethod, SslVerifyMode},
37};
38use openssl_sys::ASN1_TIME;
39use std::{
40    net::{TcpStream, ToSocketAddrs},
41    os::raw::c_int,
42    time::Duration,
43};
44
45
46extern "C" {
47    fn ASN1_TIME_diff(
48        pday: *mut c_int,
49        psec: *mut c_int,
50        from: *const ASN1_TIME,
51        to: *const ASN1_TIME,
52    );
53}
54
55
56pub struct SslExpiration(c_int);
57
58impl SslExpiration {
59    /// Creates new SslExpiration from domain name.
60    ///
61    /// This function will use HTTPS port (443) to check SSL certificate with 30 seconds timeout.
62    pub fn from_domain_name(domain: &str) -> Result<SslExpiration> {
63        SslExpiration::from_addr(format!("{}:443", domain), domain, 30) // seconds
64    }
65
66    /// This function will use HTTPS port (443) to check SSL certificate with custom timeout.
67    pub fn from_domain_name_with_timeout(domain: &str, timeout: u64) -> Result<SslExpiration> {
68        SslExpiration::from_addr(format!("{}:443", domain), domain, timeout)
69    }
70
71    /// Creates new SslExpiration from SocketAddr.
72    #[link(name = "ssl")]
73    pub fn from_addr<A: ToSocketAddrs>(
74        addr: A,
75        domain: &str,
76        timeout: u64,
77    ) -> Result<SslExpiration> {
78        let context = {
79            let mut context = SslContext::builder(SslMethod::tls())?;
80            context.set_verify(SslVerifyMode::empty());
81            context.build()
82        };
83        let mut connector = Ssl::new(&context)?;
84        connector.set_hostname(domain)?;
85        match addr.to_socket_addrs()?.next() {
86            Some(first_address) => {
87                let stream =
88                    TcpStream::connect_timeout(&first_address, Duration::from_secs(timeout))?;
89                stream.set_write_timeout(Some(Duration::from_secs(timeout)))?;
90                stream.set_read_timeout(Some(Duration::from_secs(timeout)))?;
91
92                let stream = connector
93                    .connect(stream)
94                    .map_err(|e| ErrorKind::HandshakeError(e.to_string()))?;
95                let cert = stream
96                    .ssl()
97                    .peer_certificate()
98                    .ok_or("Certificate not found")?;
99
100                let now = Asn1Time::days_from_now(0)?;
101
102                let (mut pday, mut psec) = (0, 0);
103                let ptr_pday: *mut c_int = &mut pday;
104                let ptr_psec: *mut c_int = &mut psec;
105                let now_ptr = &now as *const _ as *const _;
106                let after_ptr = &cert.not_after() as *const _ as *const _;
107                unsafe {
108                    ASN1_TIME_diff(ptr_pday, ptr_psec, *now_ptr, *after_ptr);
109                }
110
111                Ok(SslExpiration(pday * 24 * 60 * 60 + psec))
112            }
113            None => {
114                Err(Error(
115                    ErrorKind::HandshakeError(format!(
116                        "Couldn't resolve any address from domain: {}",
117                        &domain
118                    )),
119                    State::default(),
120                ))
121            }
122        }
123    }
124
125    /// How many seconds until SSL certificate expires.
126    ///
127    /// This function will return minus if SSL certificate is already expired.
128    pub fn secs(&self) -> i32 {
129        self.0
130    }
131
132    /// How many days until SSL certificate expires
133    ///
134    /// This function will return minus if SSL certificate is already expired.
135    pub fn days(&self) -> i32 {
136        self.0 / 60 / 60 / 24
137    }
138
139    /// Returns true if SSL certificate is expired
140    pub fn is_expired(&self) -> bool {
141        self.0 < 0
142    }
143}
144
145pub mod error {
146    use std::io;
147
148    error_chain! {
149        foreign_links {
150            OpenSslErrorStack(openssl::error::ErrorStack);
151            IoError(io::Error);
152        }
153        errors {
154            HandshakeError(e: String) {
155                display("HandshakeError: {}", e)
156            }
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165
166    #[test]
167    fn test_ssl_not_expired() {
168        assert!(
169            !SslExpiration::from_domain_name("google.com")
170                .unwrap()
171                .is_expired()
172        );
173        let days = SslExpiration::from_domain_name("google.com")
174            .unwrap()
175            .days();
176        assert!(days > 14)
177    }
178
179
180    #[test]
181    fn test_too_small_timeout_chain() {
182        SslExpiration::from_domain_name_with_timeout("google.com", 0)
183            .and_then(|_| Ok(assert!(false)))
184            .unwrap_or_else(|_| assert!(true));
185    }
186
187
188    #[test]
189    fn test_unresolvable_timeout_chain() {
190        SslExpiration::from_domain_name_with_timeout("unresolvable.net", 3)
191            .and_then(|_| Ok(assert!(false)))
192            .map_err(|e| println!("Error: {:?}", e))
193            .unwrap_or_else(|_| assert!(true));
194    }
195
196
197    #[test]
198    fn test_sufficient_timeout_chain() {
199        SslExpiration::from_domain_name_with_timeout("google.com", 30)
200            .and_then(|_| Ok(assert!(true)))
201            .unwrap_or_else(|_| assert!(false));
202    }
203
204
205    #[test]
206    fn test_non_panicing_chain() {
207        SslExpiration::from_domain_name("google.com")
208            .and_then(|validity| Ok(assert!(validity.days() > 14)))
209            .unwrap_or_else(|_| assert!(false));
210    }
211
212
213    #[test]
214    fn test_ssl_expired() {
215        assert!(
216            SslExpiration::from_domain_name("expired.identrustssl.com")
217                .unwrap()
218                .is_expired()
219        );
220    }
221}