1use 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#[derive(Error)]
29pub struct HttpsDateError {
30 error_type: HttpsDateErrorType,
32 source: Option<anyhow::Error>,
34}
35
36impl HttpsDateError {
37 pub fn new(error_type: HttpsDateErrorType) -> Self {
39 Self { error_type, source: None }
40 }
41
42 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
53trait HttpsDateResultExt<T> {
55 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
68impl 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
86static 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#[derive(Default)]
102struct RecordingVerifier {
103 presented_certs: Mutex<RefCell<Vec<Certificate>>>,
104}
105
106impl RecordingVerifier {
107 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 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
163pub struct NetworkTimeClient {
165 verifier: Arc<RecordingVerifier>,
167 trust_anchors: &'static [webpki::TrustAnchor<'static>],
169 client: fuchsia_hyper::HttpsClient,
171}
172
173impl NetworkTimeClient {
174 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 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 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 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 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 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 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 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 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 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 fn parse_pem(contents: &str) -> Vec<Vec<u8>> {
375 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 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}