httpdate_hyper/
lib.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fuchsia_sync::Mutex;
6use rustls::Certificate;
7use rustls::client::{ServerCertVerified, ServerCertVerifier};
8use std::cell::RefCell;
9use std::sync::Arc;
10use std::time::SystemTime;
11use thiserror::Error;
12use {fuchsia_hyper, hyper};
13
14type DateTime = chrono::DateTime<chrono::FixedOffset>;
15#[derive(Debug, PartialEq, Clone, Copy, Hash, Eq)]
16pub enum HttpsDateErrorType {
17    InvalidHostname,
18    SchemeNotHttps,
19    NoCertificatesPresented,
20    NetworkError,
21    NoDateInResponse,
22    InvalidCertificateChain,
23    CorruptLeafCertificate,
24    DateFormatError,
25}
26
27/// An error encountered while retrieving time from a server.
28#[derive(Error)]
29pub struct HttpsDateError {
30    /// The rough category of error.
31    error_type: HttpsDateErrorType,
32    /// The underlying error, if any, that triggered the error.
33    source: Option<anyhow::Error>,
34}
35
36impl HttpsDateError {
37    /// Create a new `HttpsDateError`.
38    pub fn new(error_type: HttpsDateErrorType) -> Self {
39        Self { error_type, source: None }
40    }
41
42    /// Add or replace the underlying source error.
43    pub fn with_source(mut self, source: anyhow::Error) -> Self {
44        self.source = Some(source);
45        self
46    }
47
48    pub fn error_type(&self) -> HttpsDateErrorType {
49        self.error_type
50    }
51}
52
53/// An extension trait to simplify mapping general errors to `HttpsDateError`.
54trait HttpsDateResultExt<T> {
55    /// Map an error in a Result to HttpsDateError with the given error_type.
56    fn httpsdate_err(self, error_type: HttpsDateErrorType) -> Result<T, HttpsDateError>;
57}
58
59impl<T, E> HttpsDateResultExt<T> for Result<T, E>
60where
61    E: std::error::Error + Send + Sync + 'static,
62{
63    fn httpsdate_err(self, error_type: HttpsDateErrorType) -> Result<T, HttpsDateError> {
64        self.map_err(|e| HttpsDateError::new(error_type).with_source(anyhow::Error::new(e)))
65    }
66}
67
68// Manual implementation provided to shorten output in logs.
69impl std::fmt::Debug for HttpsDateError {
70    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        match self.source.as_ref() {
72            None => self.error_type.fmt(formatter),
73            Some(source) => {
74                formatter.write_fmt(format_args!("{:?}: {:?}", self.error_type, source))
75            }
76        }
77    }
78}
79
80impl std::fmt::Display for HttpsDateError {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        std::fmt::Debug::fmt(self, f)
83    }
84}
85
86// I'd love to drop RSA here, but google.com doesn't yet serve ECDSA
87static ALLOWED_SIG_ALGS: &[&webpki::SignatureAlgorithm] = &[
88    &webpki::ECDSA_P256_SHA256,
89    &webpki::ECDSA_P256_SHA384,
90    &webpki::ECDSA_P384_SHA256,
91    &webpki::ECDSA_P384_SHA384,
92    &webpki::RSA_PKCS1_2048_8192_SHA256,
93    &webpki::RSA_PKCS1_2048_8192_SHA384,
94    &webpki::RSA_PKCS1_2048_8192_SHA512,
95    &webpki::RSA_PKCS1_3072_8192_SHA384,
96];
97
98// Because we don't yet have a system time we need a custom verifier
99// that records the handshake information needed to perform a deferred
100// trust evaluation
101#[derive(Default)]
102struct RecordingVerifier {
103    presented_certs: Mutex<RefCell<Vec<Certificate>>>,
104}
105
106impl RecordingVerifier {
107    // Verify the certificate chain stored during the TLS handshake against the
108    // given |time| and |trust_anchors| using standard TLS verification.
109    pub fn verify(
110        &self,
111        dns_name: webpki::DnsNameRef<'_>,
112        time: webpki::Time,
113        trust_anchors: &'static [webpki::TrustAnchor<'static>],
114    ) -> Result<(), HttpsDateError> {
115        let presented_certs = self.presented_certs.lock();
116        let presented_certs = presented_certs.borrow();
117        if presented_certs.len() == 0 {
118            return Err(HttpsDateError::new(HttpsDateErrorType::NoCertificatesPresented));
119        };
120
121        let untrusted_der: Vec<&[u8]> =
122            presented_certs.iter().map(|certificate| certificate.0.as_slice()).collect();
123        let leaf = webpki::EndEntityCert::try_from(untrusted_der[0])
124            .httpsdate_err(HttpsDateErrorType::CorruptLeafCertificate)?;
125
126        let crls = &[];
127
128        leaf.verify_for_usage(
129            ALLOWED_SIG_ALGS,
130            trust_anchors,
131            &untrusted_der[1..],
132            time,
133            webpki::KeyUsage::server_auth(),
134            crls,
135        )
136        .httpsdate_err(HttpsDateErrorType::InvalidCertificateChain)?;
137
138        leaf.verify_is_valid_for_subject_name(webpki::SubjectNameRef::DnsName(dns_name))
139            .httpsdate_err(HttpsDateErrorType::InvalidCertificateChain)
140    }
141}
142
143impl ServerCertVerifier for RecordingVerifier {
144    fn verify_server_cert(
145        &self,
146        end_entity: &rustls::Certificate,
147        intermediates: &[rustls::Certificate],
148        _server_name: &rustls::ServerName,
149        _scts: &mut dyn Iterator<Item = &[u8]>,
150        _ocsp_response: &[u8],
151        _now: SystemTime,
152    ) -> Result<ServerCertVerified, rustls::Error> {
153        // Don't attempt to verify trust, just store the necessary details
154        // for deferred evaluation
155        let mut presented_certs = Vec::with_capacity(1 + intermediates.len());
156        presented_certs.push(end_entity.clone());
157        presented_certs.extend(intermediates.iter().cloned());
158        *self.presented_certs.lock().borrow_mut() = presented_certs;
159        Ok(ServerCertVerified::assertion())
160    }
161}
162
163/// An HTTPS client that reports the contents of the response Date header.
164pub struct NetworkTimeClient {
165    /// The custom verifier used for certificate validation.
166    verifier: Arc<RecordingVerifier>,
167    /// The set of trust anchors used to verify a response.
168    trust_anchors: &'static [webpki::TrustAnchor<'static>],
169    /// The underlying client for making requests.
170    client: fuchsia_hyper::HttpsClient,
171}
172
173impl NetworkTimeClient {
174    /// Create a new `NetworkTimeClient` that uses the trust anchors provided through
175    /// the 'root-ssl-certificates' component feature.
176    pub fn new() -> Self {
177        Self::new_with_trust_anchors(&webpki_roots_fuchsia::TLS_SERVER_ROOTS)
178    }
179
180    fn new_with_trust_anchors(trust_anchors: &'static [webpki::TrustAnchor<'static>]) -> Self {
181        let mut root_store = rustls::RootCertStore::empty();
182
183        root_store.add_trust_anchors(trust_anchors.iter().map(|cert| {
184            rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
185                cert.subject,
186                cert.spki,
187                cert.name_constraints,
188            )
189        }));
190
191        // Because we don't currently have any idea what the "true" time is
192        // we need to use a non-standard verifier, `RecordingVerifier`, to allow
193        // us to defer trust evaluation until after we've parsed the response.
194        let verifier = Arc::new(RecordingVerifier::default());
195        let mut config = rustls::ClientConfig::builder()
196            .with_safe_defaults()
197            .with_root_certificates(root_store)
198            .with_no_client_auth();
199
200        config
201            .dangerous()
202            .set_certificate_verifier(Arc::clone(&verifier) as Arc<dyn ServerCertVerifier>);
203
204        let client = fuchsia_hyper::new_https_client_dangerous(config, Default::default());
205
206        NetworkTimeClient { verifier, client, trust_anchors }
207    }
208
209    /// Makes a best effort to get network time via an HTTPS connection to
210    /// `uri`.
211    ///
212    /// # Errors
213    ///
214    /// `get_network_time` will return errors for network failures and TLS failures.
215    ///
216    /// # Panics
217    ///
218    /// `httpdate` needs access to the `root-ssl-certificates` sandbox feature. If
219    /// it is not available this API will panic.
220    ///
221    /// # Security
222    ///
223    /// Validation of the TLS connection is deferred until after the handshake
224    /// and then performed with respect to the time provided by the remote host.
225    /// We validate the TLS connection against the system rootstore and time the server
226    /// reports. This does mean that the best we can guarantee is that the host
227    /// certificates were valid at some point, but the server can always provide a date
228    /// that falls into the validity period of the certificates they provide.
229    pub async fn get_network_time(&mut self, uri: hyper::Uri) -> Result<DateTime, HttpsDateError> {
230        match uri.scheme_str() {
231            Some("https") => (),
232            _ => return Err(HttpsDateError::new(HttpsDateErrorType::SchemeNotHttps)),
233        }
234        let dns_name = match uri.host() {
235            Some(host) => webpki::DnsNameRef::try_from_ascii_str(host)
236                .httpsdate_err(HttpsDateErrorType::InvalidHostname)?,
237            None => return Err(HttpsDateError::new(HttpsDateErrorType::InvalidHostname)),
238        };
239
240        let response =
241            self.client.get(uri.clone()).await.httpsdate_err(HttpsDateErrorType::NetworkError)?;
242
243        // Ok, so now we pull the Date header out of the response.
244        // Technically the Date header is the date of page creation, but it's the best
245        // we can do in the absence of a defined "accurate time" request.
246        //
247        // This has been suggested as being wrapped by an X-HTTPSTIME header,
248        // or .well-known/time, but neither of these proposals appear to
249        // have gone anywhere.
250        let date_header: String = match response.headers().get("date") {
251            Some(date) => {
252                date.to_str().httpsdate_err(HttpsDateErrorType::DateFormatError)?.to_string()
253            }
254            _ => return Err(HttpsDateError::new(HttpsDateErrorType::NoDateInResponse)),
255        };
256
257        // Per RFC7231 the date header is specified as RFC2822 with a UTC timezone.
258        let response_time = DateTime::parse_from_rfc2822(&date_header)
259            .httpsdate_err(HttpsDateErrorType::DateFormatError)?;
260        if response_time.timezone().utc_minus_local() != 0 {
261            return Err(HttpsDateError::new(HttpsDateErrorType::DateFormatError));
262        }
263
264        // Finally verify the the certificate chain against the response time
265        let webpki_time =
266            webpki::Time::from_seconds_since_unix_epoch(response_time.timestamp() as u64);
267        self.verifier.verify(dns_name, webpki_time, self.trust_anchors)?;
268        Ok(response_time)
269    }
270}
271
272#[cfg(test)]
273mod test {
274    use super::*;
275    use anyhow::Error;
276    use base64::engine::Engine as _;
277    use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
278    use fuchsia_async as fasync;
279    use futures::future::{TryFutureExt, ready};
280    use futures::stream::{StreamExt, TryStreamExt};
281    use hyper::server::accept::from_stream;
282    use hyper::service::{make_service_fn, service_fn};
283    use hyper::{Body, Response, Server, StatusCode};
284    use log::warn;
285    use std::convert::Infallible;
286    use std::net::{Ipv6Addr, SocketAddr};
287    use std::sync::LazyLock;
288
289    static TEST_CERT_CHAIN: LazyLock<Vec<rustls::Certificate>> = LazyLock::new(|| {
290        parse_pem(&include_str!("../certs/server.certchain"))
291            .into_iter()
292            .map(rustls::Certificate)
293            .collect()
294    });
295    static TEST_PRIVATE_KEY: LazyLock<rustls::PrivateKey> = LazyLock::new(|| {
296        parse_pem(&include_str!("../certs/server.rsa")).pop().map(rustls::PrivateKey).unwrap()
297    });
298    static CERT_NOT_BEFORE: LazyLock<DateTime> = LazyLock::new(|| {
299        DateTime::parse_from_rfc3339(include_str!("../certs/notbefore").trim()).unwrap()
300    });
301    static CERT_NOT_AFTER: LazyLock<DateTime> = LazyLock::new(|| {
302        DateTime::parse_from_rfc3339(include_str!("../certs/notafter").trim()).unwrap()
303    });
304    static TEST_CERT_ROOT: LazyLock<rustls::Certificate> = LazyLock::new(|| {
305        parse_pem(&include_str!("../certs/ca.cert")).pop().map(rustls::Certificate).unwrap()
306    });
307    static TEST_TRUST_ANCHORS: LazyLock<Vec<webpki::TrustAnchor<'static>>> = LazyLock::new(|| {
308        vec![webpki::TrustAnchor::try_from_cert_der(TEST_CERT_ROOT.as_ref()).unwrap()]
309    });
310
311    /// Spawn an HTTPS server that signs responses with TEST_PRIVATE_KEY and always returns
312    /// `served_time` in the Date header. Listens for requests on 'localhost:port', where port
313    /// is the returned port number.
314    fn serve_fake(served_time: DateTime) -> u16 {
315        let addr = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0);
316        let listener = fasync::net::TcpListener::bind(&addr).unwrap();
317        let server_port = listener.local_addr().unwrap().port();
318
319        let listener = listener
320            .accept_stream()
321            .map_err(Error::from)
322            .map_ok(|(conn, _addr)| fuchsia_hyper::TcpStream { stream: conn });
323
324        // build a server configuration using a test CA and cert chain
325        let tls_config = rustls::ServerConfig::builder()
326            .with_safe_defaults()
327            .with_no_client_auth()
328            .with_single_cert(TEST_CERT_CHAIN.clone(), TEST_PRIVATE_KEY.clone())
329            .unwrap();
330
331        let tls_acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(tls_config));
332
333        // wrap incoming tcp streams
334        let connections =
335            listener.and_then(move |conn| tls_acceptor.accept(conn).map_err(Error::from));
336
337        let served_time_arc = Arc::new(served_time);
338        let make_svc = make_service_fn(move |_socket| {
339            let time_arc = Arc::clone(&served_time_arc);
340            ready(Ok::<_, Infallible>(service_fn(move |_req| {
341                let time = Arc::clone(&time_arc);
342                ready(
343                    Response::builder()
344                        .header("Date", time.to_rfc2822())
345                        .status(StatusCode::OK)
346                        .body(Body::from("")),
347                )
348            })))
349        });
350        let server = Server::builder(from_stream(connections))
351            .executor(fuchsia_hyper::Executor)
352            .serve(make_svc)
353            .unwrap_or_else(|e| warn!("Error serving HTTPS server, {:?}", e));
354        fasync::Task::spawn(server).detach();
355
356        server_port
357    }
358
359    /// Serve a fake server that crashes when receiving a request.
360    fn serve_crash() -> u16 {
361        let addr = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0);
362        let listener = fasync::net::TcpListener::bind(&addr).unwrap();
363        let server_port = listener.local_addr().unwrap().port();
364
365        let connection_dropper =
366            listener.accept_stream().for_each(|conn_result| ready(drop(conn_result)));
367
368        fasync::Task::spawn(connection_dropper).detach();
369
370        server_port
371    }
372
373    /// Simple pem parser that doesn't validate format.
374    fn parse_pem(contents: &str) -> Vec<Vec<u8>> {
375        // Blindly assume format is correct for our test
376        let mut parsed = vec![];
377        let mut current_encoded = vec![];
378        for line in contents.split('\n') {
379            if line.starts_with("-----BEGIN") {
380                ()
381            } else if line.starts_with("-----END") {
382                let encoded = current_encoded.join("");
383                current_encoded = vec![];
384                parsed.push(BASE64_STANDARD.decode(&encoded).unwrap());
385            } else {
386                current_encoded.push(line.trim());
387            }
388        }
389        parsed
390    }
391
392    #[fuchsia::test]
393    async fn test_get_network_time() {
394        let set_time = *CERT_NOT_BEFORE + chrono::Duration::days(1);
395        let open_port = serve_fake(set_time.clone());
396
397        let mut client = NetworkTimeClient::new_with_trust_anchors(&TEST_TRUST_ANCHORS);
398
399        let url = format!("https://localhost:{}/", open_port).parse::<hyper::Uri>().unwrap();
400        let date = client.get_network_time(url).await.unwrap();
401        assert_eq!(date, set_time);
402    }
403
404    #[fuchsia::test]
405    async fn test_network_err() {
406        let open_port = serve_crash();
407
408        let mut client = NetworkTimeClient::new_with_trust_anchors(&TEST_TRUST_ANCHORS);
409
410        let url = format!("https://localhost:{}/", open_port).parse::<hyper::Uri>().unwrap();
411        assert_eq!(
412            client.get_network_time(url).await.unwrap_err().error_type(),
413            HttpsDateErrorType::NetworkError
414        );
415    }
416
417    #[fuchsia::test]
418    async fn test_untrusted_cert() {
419        let time = *CERT_NOT_BEFORE + chrono::Duration::days(1);
420        let open_port = serve_fake(time);
421
422        // The test cert vended by our server should be rejected if we verify against real server
423        // roots.
424        let mut client =
425            NetworkTimeClient::new_with_trust_anchors(&webpki_roots_fuchsia::TLS_SERVER_ROOTS);
426
427        let url = format!("https://localhost:{}/", open_port).parse::<hyper::Uri>().unwrap();
428        assert_eq!(
429            client.get_network_time(url).await.unwrap_err().error_type(),
430            HttpsDateErrorType::InvalidCertificateChain
431        );
432    }
433
434    #[fuchsia::test]
435    async fn test_time_after_cert_expired() {
436        let time = *CERT_NOT_AFTER + chrono::Duration::days(2);
437        let open_port = serve_fake(time);
438
439        let mut client = NetworkTimeClient::new_with_trust_anchors(&TEST_TRUST_ANCHORS);
440
441        let url = format!("https://localhost:{}/", open_port).parse::<hyper::Uri>().unwrap();
442        assert_eq!(
443            client.get_network_time(url).await.unwrap_err().error_type(),
444            HttpsDateErrorType::InvalidCertificateChain
445        );
446    }
447
448    #[fuchsia::test]
449    async fn test_http_rejected() {
450        let mut client = NetworkTimeClient::new_with_trust_anchors(&TEST_TRUST_ANCHORS);
451        let url = "http://localhost/".parse::<hyper::Uri>().unwrap();
452        assert_eq!(
453            client.get_network_time(url).await.unwrap_err().error_type(),
454            HttpsDateErrorType::SchemeNotHttps
455        );
456    }
457
458    #[fuchsia::test]
459    async fn test_bad_timezone() {
460        let set_time = (*CERT_NOT_BEFORE + chrono::Duration::days(1))
461            .with_timezone(&chrono::FixedOffset::east_opt(1 * 60 * 60).unwrap());
462        let open_port = serve_fake(set_time.clone());
463
464        let mut client = NetworkTimeClient::new_with_trust_anchors(&TEST_TRUST_ANCHORS);
465
466        let url = format!("https://localhost:{}/", open_port).parse::<hyper::Uri>().unwrap();
467        assert_eq!(
468            client.get_network_time(url).await.unwrap_err().error_type(),
469            HttpsDateErrorType::DateFormatError
470        );
471    }
472}