Merge pull request #3 from Alch-Emi/certificates

Expose client certificates to the user
This commit is contained in:
panicbit 2020-11-13 23:57:20 +01:00 committed by GitHub
commit d917f06368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 160 additions and 5 deletions

View File

@ -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"

63
examples/certificates.rs Normal file
View 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| 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<RwLock<HashMap<CertBytes, String>>>, request: Request) -> BoxFuture<'static, Result<Response>> {
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()
}

View File

@ -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<A: ToSocketAddrs> Builder<A> {
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<Arc<ServerConfig>> {
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<PrivateKey> {
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<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())
}
}

View File

@ -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<String>,
certificate: Option<Certificate>,
}
impl Request {
pub fn from_uri(mut uri: URIReference<'static>) -> Result<Self> {
pub fn from_uri(uri: URIReference<'static>) -> Result<Self> {
Self::with_certificate(uri, None)
}
pub fn with_certificate(
mut uri: URIReference<'static>,
certificate: Option<Certificate>
) -> Result<Self> {
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<Certificate>) {
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<Self> {
Ok(Self {
status: Status::CLIENT_CERTIFICATE_REQUIRED,
meta: Meta::new("No certificate provided")?,
})
}
pub fn certificate_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 {
&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<Self> {
let header = ResponseHeader::client_certificate_required()?;
Ok(Self::new(header))
}
pub fn certificate_not_authorized() -> Result<Self> {
let header = ResponseHeader::certificate_not_authorized()?;
Ok(Self::new(header))
}
pub fn with_body(mut self, body: impl Into<Body>) -> Self {
self.body = Some(body.into());
self