blob: 730c7c034ceeb11a8d0098620fc8330dff7d494a [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use fuchsia_hyper;
use futures::compat::Future01CompatExt;
use hyper;
use lazy_static::lazy_static;
use rustls::Certificate;
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
use webpki;
use webpki_roots_fuchsia;
type DateTime = chrono::DateTime<chrono::FixedOffset>;
#[derive(Debug, PartialEq)]
pub enum HttpsDateError {
InvalidHostname,
NoCertificatesPresented,
InvalidDate,
NetworkError,
NoDateInResponse,
InvalidCertificateChain,
CorruptLeafCertificate,
DateFormatError,
}
impl std::error::Error for HttpsDateError {}
impl std::fmt::Display for HttpsDateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
lazy_static! {
static ref BUILD_TIME: DateTime = {
let build_time = std::fs::read_to_string("/config/build-info/latest-commit-date")
.expect("Unable to load latest-commit-date");
DateTime::parse_from_rfc3339(&build_time.trim()).expect("Unable to parse build time")
};
}
// I'd love to drop RSA here, but google.com doesn't yet serve ECDSA
static ALLOWED_SIG_ALGS: &[&webpki::SignatureAlgorithm] = &[
&webpki::ECDSA_P256_SHA256,
&webpki::ECDSA_P256_SHA384,
&webpki::ECDSA_P384_SHA256,
&webpki::ECDSA_P384_SHA384,
&webpki::RSA_PKCS1_2048_8192_SHA256,
&webpki::RSA_PKCS1_2048_8192_SHA384,
&webpki::RSA_PKCS1_2048_8192_SHA512,
&webpki::RSA_PKCS1_3072_8192_SHA384,
];
#[derive(Default)]
// Because we don't yet have a system time we need a custom verifier
// that records the handshake information needed to perform a deferred
// trust evaluation
struct RecordingVerifier {
presented_certs: Mutex<RefCell<Vec<Certificate>>>,
}
impl RecordingVerifier {
// This is a standard TLS certificate verification, just using
// the certificate chain we stored during the TLS handshake
pub fn verify(
&self,
dns_name: webpki::DNSNameRef<'_>,
time: webpki::Time,
) -> Result<(), HttpsDateError> {
let presented_certs = self.presented_certs.lock().unwrap();
let presented_certs = presented_certs.borrow();
if presented_certs.len() == 0 {
return Err(HttpsDateError::NoCertificatesPresented);
};
let untrusted_der: Vec<&[u8]> =
presented_certs.iter().map(|certificate| certificate.0.as_slice()).collect();
let leaf = webpki::EndEntityCert::from(untrusted_der[0])
.map_err(|_| HttpsDateError::CorruptLeafCertificate)?;
leaf.verify_is_valid_tls_server_cert(
ALLOWED_SIG_ALGS,
&webpki_roots_fuchsia::TLS_SERVER_ROOTS,
&untrusted_der[1..],
time,
)
.map_err(|_| HttpsDateError::InvalidCertificateChain)?;
leaf.verify_is_valid_for_dns_name(dns_name)
.map_err(|_| HttpsDateError::InvalidCertificateChain)
}
}
impl rustls::ServerCertVerifier for RecordingVerifier {
fn verify_server_cert(
&self,
_root_store: &rustls::RootCertStore,
presented_certs: &[rustls::Certificate],
_dns_name: webpki::DNSNameRef<'_>,
_ocsp_response: &[u8],
) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
// Don't attempt to verify trust, just store the necessary details
// for deferred evaluation
*self.presented_certs.lock().unwrap().borrow_mut() = presented_certs.to_vec();
Ok(rustls::ServerCertVerified::assertion())
}
}
async fn get_network_time_backstop(
hostname: &str,
backstop_time: DateTime,
) -> Result<DateTime, HttpsDateError> {
let dns_name = webpki::DNSNameRef::try_from_ascii_str(hostname)
.map_err(|_| HttpsDateError::InvalidHostname)?;
let url = format!("https://{}/", hostname);
let url = url.parse::<hyper::Uri>().map_err(|_| HttpsDateError::InvalidHostname)?;
let verifier = Arc::new(RecordingVerifier::default());
// Because we don't currently have any idea what the "true" time is
// we need to use a non-standard verifier, `RecordingVerifier`, to allow
// us to defer trust evaluation until after we've parsed the response.
let mut config = rustls::ClientConfig::new();
config.root_store.add_server_trust_anchors(&webpki_roots_fuchsia::TLS_SERVER_ROOTS);
config
.dangerous()
.set_certificate_verifier(Arc::clone(&verifier) as Arc<dyn rustls::ServerCertVerifier>);
let client = fuchsia_hyper::new_https_client_dangerous(config);
let response = client.get(url).compat().await.map_err(|_| HttpsDateError::NetworkError)?;
// Ok, so now we pull the Date header out of the response.
// Technically the Date header is the date of page creation, but it's the best
// we can do in the absence of a defined "accurate time" request.
//
// This has been suggested as being wrapped by an X-HTTPSTIME header,
// or .well-known/time, but neither of these proposals appear to
// have gone anywhere.
let date_header: String = match response.headers().get("date") {
Some(date) => date.to_str().map_err(|_| HttpsDateError::DateFormatError)?.to_string(),
_ => return Err(HttpsDateError::NoDateInResponse),
};
// Per RFC7231 the date header is specified as RFC2822
let response_time = chrono::DateTime::parse_from_rfc2822(&date_header)
.map_err(|_| HttpsDateError::DateFormatError)?;
// Ensure that the response date is at least vaguely plausible: the date must
// be after the build date
if backstop_time.timestamp() > response_time.timestamp() {
return Err(HttpsDateError::InvalidDate);
}
// Finally verify the the certificate chain against the response time
let webpki_time = webpki::Time::from_seconds_since_unix_epoch(response_time.timestamp() as u64);
verifier.verify(dns_name, webpki_time)?;
Ok(response_time)
}
/// Makes a best effort to get network time via an HTTPS connection to
/// `hostname`.
///
/// # Errors
///
/// `get_network_time` will return errors for network failures, TLS failures,
/// or if the server provides a known incorrect time.
///
/// # Panics
///
/// `httpdate` needs access to the `root-ssl-certificates` and `build-info`
/// sandbox features. If they are not available this API will panic.
///
/// # Security
///
/// Validation of the TLS connection is deferred until after the handshake
/// and then performed with respect to the time provided by the remote host.
/// We ensure that the result time is at least plausible by verifying that it
/// is more recent the system build time, and we validate the TLS connection
/// against the system rootstore. This does mean that the best we can guarantee
/// is that the host certificates were valid at some point, but the server can
/// always provide a date that falls into the validity period of the certificates
/// they provide.
pub async fn get_network_time(hostname: &str) -> Result<DateTime, HttpsDateError> {
get_network_time_backstop(hostname, *BUILD_TIME).await
}
#[cfg(test)]
impl HttpsDateError {
pub fn is_network_error(&self) -> bool {
match self {
HttpsDateError::NetworkError => true,
_ => false,
}
}
pub fn is_pki_error(&self) -> bool {
use HttpsDateError::*;
match self {
NoCertificatesPresented | InvalidCertificateChain | CorruptLeafCertificate => true,
_ => false,
}
}
pub fn is_date_error(&self) -> bool {
use HttpsDateError::*;
match self {
DateFormatError | InvalidDate => true,
_ => false,
}
}
}
#[cfg(test)]
mod test {
// These tests all interpret network errors as being passing results
// in order to prevent flakiness due to unavoidable network flakiness.
use super::*;
use anyhow::Error;
use chrono::prelude::*;
use fuchsia_async::Executor;
#[ignore]
#[test]
fn test_get_network_time() -> Result<(), Error> {
let mut executor = Executor::new().expect("Error creating executor");
executor.run_singlethreaded(async {
let date = get_network_time("google.com").await;
if date.is_err() {
assert!(date.unwrap_err().is_network_error());
return Ok(());
}
assert!(BUILD_TIME.timestamp() <= date?.timestamp());
Ok(())
})
}
#[ignore]
#[test]
fn test_far_future() -> Result<(), Error> {
let mut executor = Executor::new().expect("Error creating executor");
executor.run_singlethreaded(async {
let future_date = FixedOffset::east(0).ymd(5000, 1, 1).and_hms(0, 0, 0);
let error = get_network_time_backstop("google.com", future_date).await.unwrap_err();
assert!(error.is_network_error() || error == HttpsDateError::InvalidDate);
Ok(())
})
}
#[test]
fn test_invalid_hostname() -> Result<(), Error> {
let mut executor = Executor::new().expect("Error creating executor");
executor.run_singlethreaded(async {
let future_date = FixedOffset::east(0).ymd(5000, 1, 1).and_hms(0, 0, 0);
let error = get_network_time_backstop("google com", future_date).await.unwrap_err();
assert!(error.is_network_error() || error == HttpsDateError::InvalidHostname);
Ok(())
})
}
}