From 3547143860ff32ef9622528148be02d664849ccf Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Fri, 13 Nov 2020 14:20:59 -0500 Subject: [PATCH 1/3] Expose client certificates to the user --- Cargo.toml | 2 ++ examples/certificates.rs | 63 ++++++++++++++++++++++++++++++++++++++++ examples/serve_dir.rs | 4 +-- src/lib.rs | 56 +++++++++++++++++++++++++++++++---- src/types.rs | 26 +++++++++++++++++ 5 files changed, 143 insertions(+), 8 deletions(-) create mode 100644 examples/certificates.rs diff --git a/Cargo.toml b/Cargo.toml index 684a12a..c2be8fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ description = "Gemini server implementation" [dependencies] anyhow = "1.0.33" +rustls = { version = "0.18.1", features = ["dangerous_configuration"] } tokio-rustls = "0.20.0" tokio = { version = "0.3.1", features = ["full"] } mime = "0.3.16" @@ -16,3 +17,4 @@ percent-encoding = "2.1.0" futures = "0.3.7" itertools = "0.9.0" log = "0.4.11" +webpki = "0.21.0" diff --git a/examples/certificates.rs b/examples/certificates.rs new file mode 100644 index 0000000..8c4b59a --- /dev/null +++ b/examples/certificates.rs @@ -0,0 +1,63 @@ +use anyhow::*; +use futures::{future::BoxFuture, FutureExt}; +use tokio::sync::RwLock; +use northstar::{Server, Request, Response, GEMINI_PORT, Certificate, gemini_mime}; +use std::collections::HashMap; +use std::sync::Arc; + +// Workaround for Certificates not being hashable +type CertBytes = Vec; + +#[tokio::main] +async fn main() -> Result<()> { + let users = Arc::>>::default(); + + Server::bind(("0.0.0.0", GEMINI_PORT)) + .serve(move|req, cert| handle_request(users.clone(), req, cert)) + .await +} + +/// An ultra-simple demonstration of simple authentication. +/// +/// If the user attempts to connect, they will be prompted to create a client certificate. +/// Once they've made one, they'll be given the opportunity to create an account by +/// selecting a username. They'll then get a message confirming their account creation. +/// Any time this user visits the site in the future, they'll get a personalized welcome +/// message. +fn handle_request(users: Arc>>, request: Request, cert: Option) -> BoxFuture<'static, Result> { + async move { + if let Some(Certificate(cert_bytes)) = cert { + // The user provided a certificate + let users_read = users.read().await; + if let Some(user) = users_read.get(&cert_bytes) { + // The user has already registered + Ok( + Response::success(&gemini_mime()?)? + .with_body(format!("Welcome {}!", user)) + ) + } else { + // The user still needs to register + drop(users_read); + if let Some(query_part) = request.uri().query() { + // The user provided some input (a username request) + let username = query_part.as_str(); + let mut users_write = users.write().await; + users_write.insert(cert_bytes, username.to_owned()); + Ok( + Response::success(&gemini_mime()?)? + .with_body(format!( + "Your account has been created {}! Welcome!", + username + )) + ) + } else { + // The user didn't provide input, and should be prompted + Response::input("What username would you like?") + } + } + } else { + // The user didn't provide a certificate + Response::needs_certificate() + } + }.boxed() +} diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index d4a6bb8..4d9fdbe 100644 --- a/examples/serve_dir.rs +++ b/examples/serve_dir.rs @@ -1,6 +1,6 @@ use anyhow::*; use futures::{future::BoxFuture, FutureExt}; -use northstar::{Server, Request, Response, GEMINI_PORT}; +use northstar::{Server, Request, Response, GEMINI_PORT, Certificate}; #[tokio::main] async fn main() -> Result<()> { @@ -9,7 +9,7 @@ async fn main() -> Result<()> { .await } -fn handle_request(request: Request) -> BoxFuture<'static, Result> { +fn handle_request(request: Request, _cert: Option) -> BoxFuture<'static, Result> { async move { let path = request.path_segments(); let response = northstar::util::serve_dir("public", &path).await?; diff --git a/src/lib.rs b/src/lib.rs index 03468ce..6f7c873 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ use tokio::{ net::{TcpStream, ToSocketAddrs}, }; use tokio::net::TcpListener; +use rustls::ClientCertVerifier; use tokio_rustls::{rustls, TlsAcceptor}; use rustls::*; use anyhow::*; @@ -20,11 +21,12 @@ pub mod util; pub use mime; pub use uriparse as uri; pub use types::*; +pub use rustls::Certificate; pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; -type Handler = Arc HandlerResponse + Send + Sync>; +type Handler = Arc) -> HandlerResponse + Send + Sync>; type HandlerResponse = BoxFuture<'static, Result>; #[derive(Clone)] @@ -59,7 +61,15 @@ impl Server { let request = receive_request(&mut stream).await?; debug!("Client requested: {}", request.uri()); - let handler = (self.handler)(request); + // Identify the client certificate from the tls stream. This is the first + // certificate in the certificate chain. + let client_cert = stream.get_ref() + .get_ref() + .1 + .get_peer_certificates() + .and_then(|mut v| if v.is_empty() {None} else {Some(v.remove(0))}); + + let handler = (self.handler)(request, client_cert); let handler = AssertUnwindSafe(handler); let response = handler.catch_unwind().await @@ -88,7 +98,7 @@ impl Builder { pub async fn serve(self, handler: F) -> Result<()> where - F: Fn(Request) -> HandlerResponse + Send + Sync + 'static, + F: Fn(Request, Option) -> HandlerResponse + Send + Sync + 'static, { let config = tls_config()?; @@ -97,7 +107,7 @@ impl Builder { listener: Arc::new(TcpListener::bind(self.addr).await?), handler: Arc::new(handler), }; - + server.serve().await } } @@ -159,7 +169,7 @@ async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin)) } fn tls_config() -> Result> { - let mut config = ServerConfig::new(NoClientAuth::new()); + let mut config = ServerConfig::new(AllowAnonOrSelfsignedClient::new()); let cert_chain = load_cert_chain()?; let key = load_key()?; @@ -181,7 +191,7 @@ fn load_key() -> Result { let mut keys = BufReader::new(std::fs::File::open("cert/key.pem")?); let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut keys) .map_err(|_| anyhow!("failed to load key"))?; - + ensure!(!keys.is_empty(), "no key found"); let key = keys.swap_remove(0); @@ -196,4 +206,38 @@ pub fn gemini_mime() -> Result { Ok(mime) } +/// A client cert verifier that accepts all connections +/// +/// Unfortunately, rustls doesn't provide a ClientCertVerifier that accepts self-signed +/// certificates, so we need to implement this ourselves. +struct AllowAnonOrSelfsignedClient { } +impl AllowAnonOrSelfsignedClient { + /// Create a new verifier + fn new() -> Arc { + Arc::new(Self {}) + } + +} + +impl ClientCertVerifier for AllowAnonOrSelfsignedClient { + + fn client_auth_root_subjects( + &self, + _: Option<&webpki::DNSName> + ) -> Option { + Some(Vec::new()) + } + + fn client_auth_mandatory(&self, _sni: Option<&webpki::DNSName>) -> Option { + Some(false) + } + + fn verify_client_cert( + &self, + _: &[Certificate], + _: Option<&webpki::DNSName> + ) -> Result { + Ok(ClientCertVerified::assertion()) + } +} diff --git a/src/types.rs b/src/types.rs index cabadcd..41d55c3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -91,6 +91,20 @@ impl ResponseHeader { }) } + pub fn needs_certificate() -> Result { + Ok(Self { + status: Status::CLIENT_CERTIFICATE_REQUIRED, + meta: Meta::new("No certificate provided")?, + }) + } + + pub fn not_authorized() -> Result { + Ok(Self { + status: Status::CERTIFICATE_NOT_AUTHORIZED, + meta: Meta::new("Your certificate is not authorized to view this content")?, + }) + } + pub fn status(&self) -> &Status { &self.status } @@ -120,6 +134,8 @@ impl Status { pub const PROXY_REQUEST_REFUSED: Self = Self(53); pub const BAD_REQUEST: Self = Self(59); pub const CLIENT_CERTIFICATE_REQUIRED: Self = Self(60); + pub const CERTIFICATE_NOT_AUTHORIZED: Self = Self(61); + pub const CERTIFICATE_NOT_VALID: Self = Self(62); pub fn code(&self) -> u8 { self.0 @@ -237,6 +253,16 @@ impl Response { Ok(Self::new(header)) } + pub fn needs_certificate() -> Result { + let header = ResponseHeader::needs_certificate()?; + Ok(Self::new(header)) + } + + pub fn not_authorized() -> Result { + let header = ResponseHeader::not_authorized()?; + Ok(Self::new(header)) + } + pub fn with_body(mut self, body: impl Into) -> Self { self.body = Some(body.into()); self From eaee14d174275f8ef68896b3894341975d1239b8 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Fri, 13 Nov 2020 15:04:25 -0500 Subject: [PATCH 2/3] Moved certificate into request, so this is no longer a breaking change --- examples/certificates.rs | 10 +++++----- examples/serve_dir.rs | 4 ++-- src/lib.rs | 10 ++++++---- src/types.rs | 20 +++++++++++++++++++- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/examples/certificates.rs b/examples/certificates.rs index 8c4b59a..5842f1d 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -13,7 +13,7 @@ async fn main() -> Result<()> { let users = Arc::>>::default(); Server::bind(("0.0.0.0", GEMINI_PORT)) - .serve(move|req, cert| handle_request(users.clone(), req, cert)) + .serve(move|req| handle_request(users.clone(), req)) .await } @@ -24,12 +24,12 @@ async fn main() -> Result<()> { /// selecting a username. They'll then get a message confirming their account creation. /// Any time this user visits the site in the future, they'll get a personalized welcome /// message. -fn handle_request(users: Arc>>, request: Request, cert: Option) -> BoxFuture<'static, Result> { +fn handle_request(users: Arc>>, request: Request) -> BoxFuture<'static, Result> { async move { - if let Some(Certificate(cert_bytes)) = cert { + if let Some(Certificate(cert_bytes)) = request.certificate() { // The user provided a certificate let users_read = users.read().await; - if let Some(user) = users_read.get(&cert_bytes) { + if let Some(user) = users_read.get(cert_bytes) { // The user has already registered Ok( Response::success(&gemini_mime()?)? @@ -42,7 +42,7 @@ fn handle_request(users: Arc>>, request: Reque // The user provided some input (a username request) let username = query_part.as_str(); let mut users_write = users.write().await; - users_write.insert(cert_bytes, username.to_owned()); + users_write.insert(cert_bytes.clone(), username.to_owned()); Ok( Response::success(&gemini_mime()?)? .with_body(format!( diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index 4d9fdbe..d4a6bb8 100644 --- a/examples/serve_dir.rs +++ b/examples/serve_dir.rs @@ -1,6 +1,6 @@ use anyhow::*; use futures::{future::BoxFuture, FutureExt}; -use northstar::{Server, Request, Response, GEMINI_PORT, Certificate}; +use northstar::{Server, Request, Response, GEMINI_PORT}; #[tokio::main] async fn main() -> Result<()> { @@ -9,7 +9,7 @@ async fn main() -> Result<()> { .await } -fn handle_request(request: Request, _cert: Option) -> BoxFuture<'static, Result> { +fn handle_request(request: Request) -> BoxFuture<'static, Result> { async move { let path = request.path_segments(); let response = northstar::util::serve_dir("public", &path).await?; diff --git a/src/lib.rs b/src/lib.rs index 6f7c873..3206a27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ pub use rustls::Certificate; pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; -type Handler = Arc) -> HandlerResponse + Send + Sync>; +type Handler = Arc HandlerResponse + Send + Sync>; type HandlerResponse = BoxFuture<'static, Result>; #[derive(Clone)] @@ -58,7 +58,7 @@ impl Server { let stream = self.tls_acceptor.accept(stream).await?; let mut stream = BufStream::new(stream); - let request = receive_request(&mut stream).await?; + let mut request = receive_request(&mut stream).await?; debug!("Client requested: {}", request.uri()); // Identify the client certificate from the tls stream. This is the first @@ -69,7 +69,9 @@ impl Server { .get_peer_certificates() .and_then(|mut v| if v.is_empty() {None} else {Some(v.remove(0))}); - let handler = (self.handler)(request, client_cert); + request.set_cert(client_cert); + + let handler = (self.handler)(request); let handler = AssertUnwindSafe(handler); let response = handler.catch_unwind().await @@ -98,7 +100,7 @@ impl Builder { pub async fn serve(self, handler: F) -> Result<()> where - F: Fn(Request, Option) -> HandlerResponse + Send + Sync + 'static, + F: Fn(Request) -> HandlerResponse + Send + Sync + 'static, { let config = tls_config()?; diff --git a/src/types.rs b/src/types.rs index 41d55c3..c0e9fb3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,14 +4,23 @@ use mime::Mime; use percent_encoding::percent_decode_str; use tokio::{io::AsyncRead, fs::File}; use uriparse::URIReference; +use rustls::Certificate; pub struct Request { uri: URIReference<'static>, input: Option, + certificate: Option, } impl Request { - pub fn from_uri(mut uri: URIReference<'static>) -> Result { + pub fn from_uri(uri: URIReference<'static>) -> Result { + Self::with_certificate(uri, None) + } + + pub fn with_certificate( + mut uri: URIReference<'static>, + certificate: Option + ) -> Result { uri.normalize(); let input = match uri.query() { @@ -27,6 +36,7 @@ impl Request { Ok(Self { uri, input, + certificate, }) } @@ -46,6 +56,14 @@ impl Request { pub fn input(&self) -> Option<&str> { self.input.as_deref() } + + pub fn set_cert(&mut self, cert: Option) { + self.certificate = cert; + } + + pub fn certificate(&self) -> Option<&Certificate> { + self.certificate.as_ref() + } } impl ops::Deref for Request { From 168bb56aa2b7021f1c50e1c689a3beb6b8a3c743 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Fri, 13 Nov 2020 17:54:06 -0500 Subject: [PATCH 3/3] Renamed methods to match spec --- examples/certificates.rs | 2 +- src/types.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/certificates.rs b/examples/certificates.rs index 5842f1d..095f65f 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -57,7 +57,7 @@ fn handle_request(users: Arc>>, request: Reque } } else { // The user didn't provide a certificate - Response::needs_certificate() + Response::client_certificate_required() } }.boxed() } diff --git a/src/types.rs b/src/types.rs index c0e9fb3..fb4dada 100644 --- a/src/types.rs +++ b/src/types.rs @@ -109,14 +109,14 @@ impl ResponseHeader { }) } - pub fn needs_certificate() -> Result { + pub fn client_certificate_required() -> Result { Ok(Self { status: Status::CLIENT_CERTIFICATE_REQUIRED, meta: Meta::new("No certificate provided")?, }) } - pub fn not_authorized() -> Result { + pub fn certificate_not_authorized() -> Result { Ok(Self { status: Status::CERTIFICATE_NOT_AUTHORIZED, meta: Meta::new("Your certificate is not authorized to view this content")?, @@ -271,13 +271,13 @@ impl Response { Ok(Self::new(header)) } - pub fn needs_certificate() -> Result { - let header = ResponseHeader::needs_certificate()?; + pub fn client_certificate_required() -> Result { + let header = ResponseHeader::client_certificate_required()?; Ok(Self::new(header)) } - pub fn not_authorized() -> Result { - let header = ResponseHeader::not_authorized()?; + pub fn certificate_not_authorized() -> Result { + let header = ResponseHeader::certificate_not_authorized()?; Ok(Self::new(header)) }