Skip to main content

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