Expose client certificates to the user
This commit is contained in:
parent
4b3ba3f5ae
commit
3547143860
|
@ -8,6 +8,7 @@ description = "Gemini server implementation"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.33"
|
anyhow = "1.0.33"
|
||||||
|
rustls = { version = "0.18.1", features = ["dangerous_configuration"] }
|
||||||
tokio-rustls = "0.20.0"
|
tokio-rustls = "0.20.0"
|
||||||
tokio = { version = "0.3.1", features = ["full"] }
|
tokio = { version = "0.3.1", features = ["full"] }
|
||||||
mime = "0.3.16"
|
mime = "0.3.16"
|
||||||
|
@ -16,3 +17,4 @@ percent-encoding = "2.1.0"
|
||||||
futures = "0.3.7"
|
futures = "0.3.7"
|
||||||
itertools = "0.9.0"
|
itertools = "0.9.0"
|
||||||
log = "0.4.11"
|
log = "0.4.11"
|
||||||
|
webpki = "0.21.0"
|
||||||
|
|
63
examples/certificates.rs
Normal file
63
examples/certificates.rs
Normal file
|
@ -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<u8>;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let users = Arc::<RwLock::<HashMap<CertBytes, String>>>::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<RwLock<HashMap<CertBytes, String>>>, request: Request, cert: Option<Certificate>) -> BoxFuture<'static, Result<Response>> {
|
||||||
|
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()
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use futures::{future::BoxFuture, FutureExt};
|
use futures::{future::BoxFuture, FutureExt};
|
||||||
use northstar::{Server, Request, Response, GEMINI_PORT};
|
use northstar::{Server, Request, Response, GEMINI_PORT, Certificate};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
@ -9,7 +9,7 @@ async fn main() -> Result<()> {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_request(request: Request) -> BoxFuture<'static, Result<Response>> {
|
fn handle_request(request: Request, _cert: Option<Certificate>) -> BoxFuture<'static, Result<Response>> {
|
||||||
async move {
|
async move {
|
||||||
let path = request.path_segments();
|
let path = request.path_segments();
|
||||||
let response = northstar::util::serve_dir("public", &path).await?;
|
let response = northstar::util::serve_dir("public", &path).await?;
|
||||||
|
|
56
src/lib.rs
56
src/lib.rs
|
@ -9,6 +9,7 @@ use tokio::{
|
||||||
net::{TcpStream, ToSocketAddrs},
|
net::{TcpStream, ToSocketAddrs},
|
||||||
};
|
};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
use rustls::ClientCertVerifier;
|
||||||
use tokio_rustls::{rustls, TlsAcceptor};
|
use tokio_rustls::{rustls, TlsAcceptor};
|
||||||
use rustls::*;
|
use rustls::*;
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
|
@ -20,11 +21,12 @@ pub mod util;
|
||||||
pub use mime;
|
pub use mime;
|
||||||
pub use uriparse as uri;
|
pub use uriparse as uri;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
pub use rustls::Certificate;
|
||||||
|
|
||||||
pub const REQUEST_URI_MAX_LEN: usize = 1024;
|
pub const REQUEST_URI_MAX_LEN: usize = 1024;
|
||||||
pub const GEMINI_PORT: u16 = 1965;
|
pub const GEMINI_PORT: u16 = 1965;
|
||||||
|
|
||||||
type Handler = Arc<dyn Fn(Request) -> HandlerResponse + Send + Sync>;
|
type Handler = Arc<dyn Fn(Request, Option<Certificate>) -> HandlerResponse + Send + Sync>;
|
||||||
type HandlerResponse = BoxFuture<'static, Result<Response>>;
|
type HandlerResponse = BoxFuture<'static, Result<Response>>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -59,7 +61,15 @@ impl Server {
|
||||||
let request = receive_request(&mut stream).await?;
|
let request = receive_request(&mut stream).await?;
|
||||||
debug!("Client requested: {}", request.uri());
|
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 handler = AssertUnwindSafe(handler);
|
||||||
|
|
||||||
let response = handler.catch_unwind().await
|
let response = handler.catch_unwind().await
|
||||||
|
@ -88,7 +98,7 @@ impl<A: ToSocketAddrs> Builder<A> {
|
||||||
|
|
||||||
pub async fn serve<F>(self, handler: F) -> Result<()>
|
pub async fn serve<F>(self, handler: F) -> Result<()>
|
||||||
where
|
where
|
||||||
F: Fn(Request) -> HandlerResponse + Send + Sync + 'static,
|
F: Fn(Request, Option<Certificate>) -> HandlerResponse + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
let config = tls_config()?;
|
let config = tls_config()?;
|
||||||
|
|
||||||
|
@ -97,7 +107,7 @@ impl<A: ToSocketAddrs> Builder<A> {
|
||||||
listener: Arc::new(TcpListener::bind(self.addr).await?),
|
listener: Arc::new(TcpListener::bind(self.addr).await?),
|
||||||
handler: Arc::new(handler),
|
handler: Arc::new(handler),
|
||||||
};
|
};
|
||||||
|
|
||||||
server.serve().await
|
server.serve().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,7 +169,7 @@ async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tls_config() -> Result<Arc<ServerConfig>> {
|
fn tls_config() -> Result<Arc<ServerConfig>> {
|
||||||
let mut config = ServerConfig::new(NoClientAuth::new());
|
let mut config = ServerConfig::new(AllowAnonOrSelfsignedClient::new());
|
||||||
|
|
||||||
let cert_chain = load_cert_chain()?;
|
let cert_chain = load_cert_chain()?;
|
||||||
let key = load_key()?;
|
let key = load_key()?;
|
||||||
|
@ -181,7 +191,7 @@ fn load_key() -> Result<PrivateKey> {
|
||||||
let mut keys = BufReader::new(std::fs::File::open("cert/key.pem")?);
|
let mut keys = BufReader::new(std::fs::File::open("cert/key.pem")?);
|
||||||
let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut keys)
|
let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut keys)
|
||||||
.map_err(|_| anyhow!("failed to load key"))?;
|
.map_err(|_| anyhow!("failed to load key"))?;
|
||||||
|
|
||||||
ensure!(!keys.is_empty(), "no key found");
|
ensure!(!keys.is_empty(), "no key found");
|
||||||
|
|
||||||
let key = keys.swap_remove(0);
|
let key = keys.swap_remove(0);
|
||||||
|
@ -196,4 +206,38 @@ pub fn gemini_mime() -> Result<Mime> {
|
||||||
Ok(mime)
|
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<Self> {
|
||||||
|
Arc::new(Self {})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientCertVerifier for AllowAnonOrSelfsignedClient {
|
||||||
|
|
||||||
|
fn client_auth_root_subjects(
|
||||||
|
&self,
|
||||||
|
_: Option<&webpki::DNSName>
|
||||||
|
) -> Option<DistinguishedNames> {
|
||||||
|
Some(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_auth_mandatory(&self, _sni: Option<&webpki::DNSName>) -> Option<bool> {
|
||||||
|
Some(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_client_cert(
|
||||||
|
&self,
|
||||||
|
_: &[Certificate],
|
||||||
|
_: Option<&webpki::DNSName>
|
||||||
|
) -> Result<ClientCertVerified, TLSError> {
|
||||||
|
Ok(ClientCertVerified::assertion())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
26
src/types.rs
26
src/types.rs
|
@ -91,6 +91,20 @@ impl ResponseHeader {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn needs_certificate() -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
status: Status::CLIENT_CERTIFICATE_REQUIRED,
|
||||||
|
meta: Meta::new("No certificate provided")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn not_authorized() -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
status: Status::CERTIFICATE_NOT_AUTHORIZED,
|
||||||
|
meta: Meta::new("Your certificate is not authorized to view this content")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn status(&self) -> &Status {
|
pub fn status(&self) -> &Status {
|
||||||
&self.status
|
&self.status
|
||||||
}
|
}
|
||||||
|
@ -120,6 +134,8 @@ impl Status {
|
||||||
pub const PROXY_REQUEST_REFUSED: Self = Self(53);
|
pub const PROXY_REQUEST_REFUSED: Self = Self(53);
|
||||||
pub const BAD_REQUEST: Self = Self(59);
|
pub const BAD_REQUEST: Self = Self(59);
|
||||||
pub const CLIENT_CERTIFICATE_REQUIRED: Self = Self(60);
|
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 {
|
pub fn code(&self) -> u8 {
|
||||||
self.0
|
self.0
|
||||||
|
@ -237,6 +253,16 @@ impl Response {
|
||||||
Ok(Self::new(header))
|
Ok(Self::new(header))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn needs_certificate() -> Result<Self> {
|
||||||
|
let header = ResponseHeader::needs_certificate()?;
|
||||||
|
Ok(Self::new(header))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn not_authorized() -> Result<Self> {
|
||||||
|
let header = ResponseHeader::not_authorized()?;
|
||||||
|
Ok(Self::new(header))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_body(mut self, body: impl Into<Body>) -> Self {
|
pub fn with_body(mut self, body: impl Into<Body>) -> Self {
|
||||||
self.body = Some(body.into());
|
self.body = Some(body.into());
|
||||||
self
|
self
|
||||||
|
|
Loading…
Reference in a new issue