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..095f65f --- /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| handle_request(users.clone(), req)) + .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) -> BoxFuture<'static, Result> { + async move { + 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) { + // 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.clone(), 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::client_certificate_required() + } + }.boxed() +} diff --git a/src/lib.rs b/src/lib.rs index 03468ce..3206a27 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,6 +21,7 @@ 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; @@ -56,9 +58,19 @@ 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 + // 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))}); + + request.set_cert(client_cert); + let handler = (self.handler)(request); let handler = AssertUnwindSafe(handler); @@ -97,7 +109,7 @@ impl Builder { listener: Arc::new(TcpListener::bind(self.addr).await?), handler: Arc::new(handler), }; - + server.serve().await } } @@ -159,7 +171,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 +193,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 +208,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..fb4dada 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 { @@ -91,6 +109,20 @@ impl ResponseHeader { }) } + pub fn client_certificate_required() -> Result { + Ok(Self { + status: Status::CLIENT_CERTIFICATE_REQUIRED, + meta: Meta::new("No certificate provided")?, + }) + } + + 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")?, + }) + } + pub fn status(&self) -> &Status { &self.status } @@ -120,6 +152,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 +271,16 @@ impl Response { Ok(Self::new(header)) } + pub fn client_certificate_required() -> Result { + let header = ResponseHeader::client_certificate_required()?; + Ok(Self::new(header)) + } + + pub fn certificate_not_authorized() -> Result { + let header = ResponseHeader::certificate_not_authorized()?; + Ok(Self::new(header)) + } + pub fn with_body(mut self, body: impl Into) -> Self { self.body = Some(body.into()); self