Rustls is fine but it's hard to change the default "secure" behaviour for Gemini usage. Not that Gemini is unsecure but things like self-signed client certificates should not be such a stupidly hard thing to accept.main
parent
e24031d6c9
commit
237959fb42
@ -1,321 +0,0 @@
|
||||
//! CGI implementation.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::fs;
|
||||
use std::path;
|
||||
use std::process;
|
||||
|
||||
use log::{debug, error};
|
||||
|
||||
/// General CGI configuration.
|
||||
pub struct CgiConfig {
|
||||
pub root: String,
|
||||
}
|
||||
|
||||
/// Return true if this path has executable bits set on Unix systems.
|
||||
///
|
||||
/// If for some reason we can't get the mode information (not on Unix or some error occured),
|
||||
/// return false.
|
||||
fn is_executable(path: &path::Path) -> bool {
|
||||
match fs::metadata(path) {
|
||||
Ok(metadata) => {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = metadata.permissions().mode();
|
||||
mode & 0o111 != 0
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Can't get metadata for \"{}\": {}",
|
||||
path.to_string_lossy(),
|
||||
err
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::server::Connection<'_> {
|
||||
/// Process a client request.
|
||||
///
|
||||
/// If the CGI process returns successfully, return the requested URL with the process output
|
||||
/// so that it can be sent back to the client.
|
||||
///
|
||||
/// If an error occurs outside of the CGI process, return a 3-uple with the URL (if it could be
|
||||
/// parsed correctly), a Gemini error code and an explanation string to provide to the client.
|
||||
pub fn get_response(
|
||||
&self,
|
||||
request: &[u8],
|
||||
) -> Result<(String, Vec<u8>), (Option<String>, u8, &str)> {
|
||||
// Convert the URL to UTF-8.
|
||||
let url_str = std::str::from_utf8(&request[..request.len() - 2])
|
||||
.map_err(|_| (None, 59, "URL is not valid UTF-8"))?;
|
||||
// Parse the URL. The `url` crate normalizes ".." and "/" elements here.
|
||||
let url = url::Url::parse(url_str)
|
||||
.map_err(|_| (Some(url_str.to_string()), 59u8, "Invalid URL"))?;
|
||||
|
||||
// Get the script path, optionally with CGI's "path info".
|
||||
let (script_path, path_info) = self.validate_script_path(&url)?;
|
||||
debug!("Script path: \"{}\"", script_path.to_string_lossy());
|
||||
|
||||
// Define a generic "temp failure" error for any other issue.
|
||||
let cgi_error = (Some(url_str.to_string()), 40, "Temporary failure");
|
||||
|
||||
// Execute script and return its output.
|
||||
let env = self
|
||||
.get_cgi_envs(&url, &script_path.to_string_lossy(), &path_info)
|
||||
.ok_or_else(|| {
|
||||
error!("Can't get required environment variables.");
|
||||
cgi_error.to_owned()
|
||||
})?;
|
||||
let output = process::Command::new(&script_path)
|
||||
.env_clear()
|
||||
.envs(env)
|
||||
.output()
|
||||
.map_err(|err| {
|
||||
error!("Can't execute script: {}", err);
|
||||
cgi_error.to_owned()
|
||||
})?;
|
||||
|
||||
Ok((url_str.to_string(), output.stdout))
|
||||
}
|
||||
|
||||
/// Return a validated script path from the requested URL along with CGI PATH_INFO.
|
||||
///
|
||||
/// A valid path points to an existing, executable file, located within the CGI scripts root.
|
||||
/// If any of these condition fails, log the reason and return an appropriate 3-uple for
|
||||
/// `get_response`.
|
||||
fn validate_script_path(
|
||||
&self,
|
||||
url: &url::Url,
|
||||
) -> Result<(path::PathBuf, Option<String>), (Option<String>, u8, &str)> {
|
||||
// Define a generic "not found" error for most path issues.
|
||||
let not_found = (Some(url.as_str().to_string()), 51, "Not found");
|
||||
|
||||
// Find script path from our CGI root and the request.
|
||||
let mut path = path::PathBuf::from(&self.cgi_config.root);
|
||||
let mut segments = url.path_segments().ok_or_else(|| {
|
||||
error!("Can't get path segments from URL");
|
||||
not_found.to_owned()
|
||||
})?;
|
||||
// We incrementally push path segments after our CGI root to find the first path that
|
||||
// represents an executable file.
|
||||
let mut found_script = false;
|
||||
loop {
|
||||
let segment = segments.next();
|
||||
if segment.is_none() {
|
||||
break;
|
||||
}
|
||||
let decoded_segment = percent_encoding::percent_decode_str(segment.unwrap())
|
||||
.decode_utf8()
|
||||
.map_err(|err| {
|
||||
error!("Path segment decoded into invalid UTF-8: {}", err);
|
||||
not_found.to_owned()
|
||||
})?;
|
||||
path.push(decoded_segment.into_owned());
|
||||
// If that path is not an executable file, continue with the next segment.
|
||||
if path.is_file() && is_executable(&path) {
|
||||
found_script = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found_script {
|
||||
error!("No script found along path \"{}\".", path.to_string_lossy());
|
||||
return Err(not_found);
|
||||
}
|
||||
|
||||
// Collect the remaining segments into the CGI "path info" value.
|
||||
let rem_segments = segments.collect::<Vec<&str>>();
|
||||
let path_info = if rem_segments.len() > 0 {
|
||||
Some(String::from("/") + &rem_segments.join("/"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Just for safety, check that the now-canonicalized path is within the CGI root.
|
||||
if !path.starts_with(&self.cgi_config.root) {
|
||||
debug!(
|
||||
"Script path \"{}\" is outside of CGI root dir \"{}\".",
|
||||
path.to_string_lossy(),
|
||||
self.cgi_config.root
|
||||
);
|
||||
return Err(not_found);
|
||||
}
|
||||
|
||||
Ok((path, path_info))
|
||||
}
|
||||
|
||||
/// Build environment variables for the CGI process.
|
||||
pub fn get_cgi_envs(
|
||||
&self,
|
||||
url: &url::Url,
|
||||
script_path: &str,
|
||||
path_info: &Option<String>,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
// Start the envs vector with common, nothing-to-compute elements.
|
||||
let mut envs = vec![
|
||||
("GATEWAY_INTERFACE", String::from("CGI/1.1")),
|
||||
("REQUEST_METHOD", String::new()),
|
||||
("SERVER_PROTOCOL", String::from("GEMINI")),
|
||||
("SERVER_SOFTWARE", format!("opal/{}", crate_version!())),
|
||||
("GEMINI_DOCUMENT_ROOT", self.cgi_config.root.to_string()),
|
||||
("GEMINI_SCRIPT_FILENAME", script_path.to_string()),
|
||||
("GEMINI_URL", url.to_string()),
|
||||
("GEMINI_URL_PATH", url.path().to_string()),
|
||||
];
|
||||
|
||||
// Next variables must be there but might not be available for some reason: this makes the
|
||||
// whole execution fail.
|
||||
|
||||
let remote_addr = self
|
||||
.socket
|
||||
.peer_addr()
|
||||
.map_err(|err| {
|
||||
error!("Can't get peer address for CGI envs: {}", err);
|
||||
err
|
||||
})
|
||||
.ok()?;
|
||||
envs.push(("REMOTE_ADDR", remote_addr.to_string()));
|
||||
envs.push(("REMOTE_HOST", remote_addr.to_string()));
|
||||
|
||||
let server_port = self
|
||||
.socket
|
||||
.local_addr()
|
||||
.map(|address| address.port())
|
||||
.map_err(|err| {
|
||||
error!("Can't get local address for CGI envs: {}", err);
|
||||
err
|
||||
})
|
||||
.ok()?;
|
||||
envs.push(("SERVER_PORT", server_port.to_string()));
|
||||
|
||||
let root_len = self.cgi_config.root.len();
|
||||
envs.push(("SCRIPT_NAME", script_path[root_len..].to_string()));
|
||||
|
||||
let server_name = self.tls.sni_hostname().or_else(|| {
|
||||
error!("Can't get SNI hostname for SERVER_NAME.");
|
||||
None
|
||||
})?;
|
||||
envs.push(("SERVER_NAME", server_name.to_string()));
|
||||
|
||||
let version = self
|
||||
.tls
|
||||
.protocol_version()
|
||||
.and_then(|v| v.as_str())
|
||||
.or_else(|| {
|
||||
error!("Can't get TLS version.");
|
||||
None
|
||||
})?;
|
||||
envs.push(("TLS_VERSION", version.to_string()));
|
||||
|
||||
let cipher = self
|
||||
.tls
|
||||
.negotiated_cipher_suite()
|
||||
.and_then(|s| s.suite().as_str())
|
||||
.or_else(|| {
|
||||
error!("Can't get TLS negociated cipher suite.");
|
||||
None
|
||||
})?;
|
||||
envs.push(("TLS_CIPHER", cipher.to_string()));
|
||||
|
||||
// Next variables are optional.
|
||||
|
||||
if let Some(path_info) = path_info {
|
||||
let percent_decode = percent_encoding::percent_decode_str(path_info);
|
||||
match percent_decode.decode_utf8() {
|
||||
Ok(path_info) => {
|
||||
envs.push(("PATH_INFO", path_info.to_string()));
|
||||
}
|
||||
Err(err) => {
|
||||
error!("CGI PATH_INFO decoded into invalid UTF-8: {}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(query) = url.query() {
|
||||
envs.push(("QUERY_STRING", query.to_string()));
|
||||
}
|
||||
|
||||
// Variables related to client certificates.
|
||||
if let Some(certs) = self.tls.peer_certificates() {
|
||||
if certs.len() > 0 {
|
||||
envs.push(("AUTH_TYPE", String::from("Certificate")));
|
||||
let der = &certs[0].0;
|
||||
if let Ok((_, cert)) = x509_parser::parse_x509_certificate(der) {
|
||||
envs.push(("REMOTE_USER", get_common_name(cert.subject())));
|
||||
envs.push(("TLS_CLIENT_ISSUER", get_common_name(cert.issuer())));
|
||||
|
||||
let digest = ring::digest::digest(&ring::digest::SHA256, der);
|
||||
let hex_digest = hexlify(digest.as_ref());
|
||||
let client_hash = String::from("SHA256:") + &hex_digest;
|
||||
envs.push(("TLS_CLIENT_HASH", client_hash));
|
||||
|
||||
let not_valid_before = cert.validity().not_before.timestamp().to_string();
|
||||
let not_valid_after = cert.validity().not_after.timestamp().to_string();
|
||||
envs.push(("TLS_CLIENT_NOT_BEFORE", not_valid_before));
|
||||
envs.push(("TLS_CLIENT_NOT_AFTER", not_valid_after));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CGI standard
|
||||
// AUTH_TYPE: OK
|
||||
// CONTENT_LENGTH: not affected
|
||||
// CONTENT_TYPE: not affected
|
||||
// GATEWAY_INTERFACE: OK
|
||||
// PATH_INFO: OK, decoded
|
||||
// PATH_TRANSLATED: TODO useful?
|
||||
// QUERY_STRING: OK still URL-encoded like the standard asks
|
||||
// REMOTE_ADDR: OK
|
||||
// REMOTE_HOST: use REMOTE_ADDR
|
||||
// REMOTE_IDENT: not affected
|
||||
// REMOTE_USER: OK
|
||||
// REQUEST_METHOD: empty string for compatibility
|
||||
// SCRIPT_NAME: OK
|
||||
// SERVER_NAME: OK
|
||||
// SERVER_PORT: OK
|
||||
// SERVER_PROTOCOL: OK
|
||||
// SERVER_SOFTWARE: OK
|
||||
|
||||
// Additionally proposed by gmid
|
||||
// GEMINI_DOCUMENT_ROOT: OK
|
||||
// GEMINI_SCRIPT_FILENAME: OK
|
||||
// GEMINI_URL: OK
|
||||
// GEMINI_URL_PATH: OK
|
||||
// TLS_CLIENT_ISSUER: OK
|
||||
// TLS_CLIENT_HASH: OK
|
||||
// TLS_VERSION: OK
|
||||
// TLS_CIPHER: OK
|
||||
// TLS_CIPHER_STRENGTH: pfffff
|
||||
// TLS_CLIENT_NOT_AFTER: OK but timestamp
|
||||
// TLS_CLIENT_NOT_BEFORE: OK but timestamp
|
||||
|
||||
Some(
|
||||
envs.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_owned()))
|
||||
.collect::<HashMap<String, String>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to get the common name of an x509 name field.
|
||||
///
|
||||
/// If there is no common name or it can't be easily converted into a string, return an empty
|
||||
/// string instead.
|
||||
fn get_common_name(x509name: &x509_parser::x509::X509Name) -> String {
|
||||
x509name
|
||||
.iter_common_name()
|
||||
.next()
|
||||
.and_then(|cn| cn.as_str().ok())
|
||||
.or(Some(""))
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Return an hex-string representing the digest data.
|
||||
fn hexlify(digest: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(digest.len() * 2);
|
||||
for b in digest {
|
||||
write!(&mut s, "{:02x}", b).unwrap();
|
||||
}
|
||||
s
|
||||
}
|
@ -1,263 +0,0 @@
|
||||
//! Server listening loop and connection basics.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::net;
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::{debug, error, info};
|
||||
|
||||
use crate::cgi::CgiConfig;
|
||||
|
||||
/// TCP server, listening for clients opening TLS connections.
|
||||
pub struct Server<'a> {
|
||||
pub config: Arc<rustls::ServerConfig>,
|
||||
pub cgi_config: &'a CgiConfig,
|
||||
listener: mio::net::TcpListener,
|
||||
connections: HashMap<mio::Token, Connection<'a>>,
|
||||
next_id: usize,
|
||||
}
|
||||
|
||||
impl<'a> Server<'a> {
|
||||
/// Create a new Server.
|
||||
pub fn new(
|
||||
listener: mio::net::TcpListener,
|
||||
config: Arc<rustls::ServerConfig>,
|
||||
cgi_config: &'a CgiConfig,
|
||||
) -> Self {
|
||||
Server {
|
||||
config,
|
||||
listener,
|
||||
connections: HashMap::new(),
|
||||
next_id: 2,
|
||||
cgi_config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept incoming client connections forever.
|
||||
pub fn accept(&mut self, registry: &mio::Registry) -> Result<(), io::Error> {
|
||||
loop {
|
||||
match self.listener.accept() {
|
||||
Ok((socket, addr)) => {
|
||||
debug!("Connection from {:?}", addr);
|
||||
let tls = match rustls::ServerConnection::new(Arc::clone(&self.config)) {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
error!("Could not create server connection: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let token = mio::Token(self.next_id);
|
||||
self.next_id += 1;
|
||||
let mut connection = Connection::new(socket, token, tls, self.cgi_config);
|
||||
connection.register(registry);
|
||||
self.connections.insert(token, connection);
|
||||
}
|
||||
Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => return Ok(()),
|
||||
Err(err) => {
|
||||
error!("Error while accepting connection: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pass MIO events to corresponding connections.
|
||||
pub fn handle_event(&mut self, registry: &mio::Registry, event: &mio::event::Event) {
|
||||
let token = event.token();
|
||||
if self.connections.contains_key(&token) {
|
||||
self.connections
|
||||
.get_mut(&token)
|
||||
.unwrap()
|
||||
.ready(registry, event);
|
||||
if self.connections[&token].state == ConnectionState::Closed {
|
||||
self.connections.remove(&token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection state, mostly used for graceful shutdowns.
|
||||
#[derive(PartialEq)]
|
||||
pub enum ConnectionState {
|
||||
Open,
|
||||
Closing,
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// A once open connection; hold the TCP and TLS states, as well as the incoming client data.
|
||||
pub struct Connection<'a> {
|
||||
pub cgi_config: &'a CgiConfig,
|
||||
pub socket: mio::net::TcpStream,
|
||||
pub tls: rustls::ServerConnection,
|
||||
pub token: mio::Token,
|
||||
pub state: ConnectionState,
|
||||
buffer: Vec<u8>,
|
||||
received: usize,
|
||||
}
|
||||
|
||||
impl<'a> Connection<'a> {
|
||||
/// Create a new Connection.
|
||||
fn new(
|
||||
socket: mio::net::TcpStream,
|
||||
token: mio::Token,
|
||||
tls_connection: rustls::ServerConnection,
|
||||
cgi_config: &'a CgiConfig,
|
||||
) -> Self {
|
||||
Connection {
|
||||
socket,
|
||||
token,
|
||||
state: ConnectionState::Open,
|
||||
tls: tls_connection,
|
||||
buffer: vec![0; 1026],
|
||||
received: 0,
|
||||
cgi_config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Process an event.
|
||||
fn ready(&mut self, registry: &mio::Registry, event: &mio::event::Event) {
|
||||
if event.is_readable() {
|
||||
self.read_tls();
|
||||
self.read_plain();
|
||||
}
|
||||
if event.is_writable() {
|
||||
self.write_tls_with_errors();
|
||||
}
|
||||
if self.state == ConnectionState::Closing {
|
||||
if let Err(err) = self.socket.shutdown(net::Shutdown::Both) {
|
||||
error!("Could not properly shutdown socket: {}", err);
|
||||
}
|
||||
self.state = ConnectionState::Closed;
|
||||
registry.deregister(&mut self.socket).unwrap();
|
||||
} else {
|
||||
let event_set = self.event_set();
|
||||
registry
|
||||
.reregister(&mut self.socket, self.token, event_set)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Read data from the TLS tunnel; if enough data is read, new packets are processed and can be
|
||||
/// later read with `read_plain`.
|
||||
fn read_tls(&mut self) {
|
||||
match self.tls.read_tls(&mut self.socket) {
|
||||
Err(err) => {
|
||||
if err.kind() != io::ErrorKind::WouldBlock {
|
||||
error!("TLS read error: {}", err);
|
||||
self.state = ConnectionState::Closing;
|
||||
}
|
||||
return;
|
||||
}
|
||||
Ok(num_bytes) if num_bytes == 0 => {
|
||||
self.state = ConnectionState::Closing;
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
if let Err(err) = self.tls.process_new_packets() {
|
||||
error!("Can't process packet: {}", err);
|
||||
self.write_tls_with_errors();
|
||||
self.state = ConnectionState::Closing;
|
||||
}
|
||||
}
|
||||
|
||||
/// Process packets with incoming data from a client.
|
||||
fn read_plain(&mut self) {
|
||||
if let Ok(io_state) = self.tls.process_new_packets() {
|
||||
let to_read = io_state.plaintext_bytes_to_read();
|
||||
if to_read > 0 {
|
||||
let mut buffer = vec![0u8; to_read];
|
||||
self.tls.reader().read(&mut buffer).unwrap();
|
||||
self.handle_incoming_data(&buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process received client data as a Gemini request; it either is a self-contained
|
||||
fn handle_incoming_data(&mut self, data: &[u8]) {
|
||||
// The connection buffer should never exceed 1026 bytes: 1024 URL bytes plus \r\n.
|
||||
if data.len() + self.received > 1026 {
|
||||
error!("URL queried is longer 1024 bytes, discarding.");
|
||||
self.state = ConnectionState::Closing;
|
||||
return;
|
||||
}
|
||||
// If the URL requested is contained within that single data packet, process it without
|
||||
// copying stuff.
|
||||
if self.received == 0 && data.ends_with(b"\r\n") {
|
||||
self.process_buffer(data);
|
||||
}
|
||||
// Else append received data into the connection buffer and try to process it.
|
||||
else {
|
||||
let buffer_end = self.received + data.len();
|
||||
self.buffer[self.received..buffer_end].copy_from_slice(data);
|
||||
self.received = buffer_end;
|
||||
if self.buffer[..self.received].ends_with(b"\r\n") {
|
||||
self.process_buffer(&self.buffer.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Respond to a client request. Whether the request succeeds or not, a response is sent and
|
||||
/// the connection is closed.
|
||||
fn process_buffer(&mut self, buffer: &[u8]) {
|
||||
// Get appropriate response from either Opal or the CGI process.
|
||||
let response: Vec<u8> = match self.get_response(buffer) {
|
||||
Ok((url, data)) => {
|
||||
info!("\"{}\" → reply {} bytes", url, data.len());
|
||||
data
|
||||
}
|
||||
Err((url, code, meta)) => {
|
||||
info!(
|
||||
"\"{}\" → {} \"{}\"",
|
||||
url.or(Some("<invalid URL>".to_string())).unwrap(),
|
||||
code,
|
||||
meta
|
||||
);
|
||||
format!("{} {}\r\n", code, meta).as_bytes().to_vec()
|
||||
}
|
||||
};
|
||||
// Whether the request succeeded or not, send the response.
|
||||
if let Err(err) = self.tls.writer().write_all(&response) {
|
||||
error!("Error while writing TLS data: {}", err);
|
||||
}
|
||||
// Properly close the connection.
|
||||
self.tls.send_close_notify();
|
||||
self.state = ConnectionState::Closing;
|
||||
}
|
||||
|
||||
/// Write TLS data in the TCP socket.
|
||||
fn write_tls(&mut self) -> io::Result<usize> {
|
||||
self.tls.write_tls(&mut self.socket)
|
||||
}
|
||||
|
||||
/// Call `write_tls` and mark connection as closing on error.
|
||||
fn write_tls_with_errors(&mut self) {
|
||||
if let Err(err) = self.write_tls() {
|
||||
error!("TLS write error after errors: {}", err);
|
||||
self.state = ConnectionState::Closing;
|
||||
}
|
||||
}
|
||||
|
||||
/// Register the connection into the MIO registry using its own token.
|
||||
fn register(&mut self, registry: &mio::Registry) {
|
||||
let event_set = self.event_set();
|
||||
registry
|
||||
.register(&mut self.socket, self.token, event_set)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Return what IO events we're currently waiting for, based on wants_read/wants_write.
|
||||
fn event_set(&self) -> mio::Interest {
|
||||
let r = self.tls.wants_read();
|
||||
let w = self.tls.wants_write();
|
||||
if r && w {
|
||||
mio::Interest::READABLE | mio::Interest::WRITABLE
|
||||
} else if w {
|
||||
mio::Interest::WRITABLE
|
||||
} else {
|
||||
mio::Interest::READABLE
|
||||
}
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
//! Trying desperately to not implement a security colander.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
type SignatureAlgorithms = &'static [&'static webpki::SignatureAlgorithm];
|
||||
|
||||
/// Supported signature verification mechanisms; copied from Rustls source.
|
||||
static SUPPORTED_SIG_ALGS: SignatureAlgorithms = &[
|
||||
&webpki::ECDSA_P256_SHA256,
|
||||
&webpki::ECDSA_P256_SHA384,
|
||||
&webpki::ECDSA_P384_SHA256,
|
||||
&webpki::ECDSA_P384_SHA384,
|
||||
&webpki::ED25519,
|
||||
&webpki::RSA_PSS_2048_8192_SHA256_LEGACY_KEY,
|
||||
&webpki::RSA_PSS_2048_8192_SHA384_LEGACY_KEY,
|
||||
&webpki::RSA_PSS_2048_8192_SHA512_LEGACY_KEY,
|
||||
&webpki::RSA_PKCS1_2048_8192_SHA256,
|
||||
&webpki::RSA_PKCS1_2048_8192_SHA384,
|
||||
&webpki::RSA_PKCS1_2048_8192_SHA512,
|
||||
&webpki::RSA_PKCS1_3072_8192_SHA384,
|
||||
];
|
||||
|
||||
/// A `ClientCertVerifier` for Gemini.
|
||||
///
|
||||
/// Client certificate is optional. When provided, we check that it is valid for use by a client.
|
||||
/// No certificate chain is verified as client certs in Gemini are mostly self-signed anyway.
|
||||
/// Signature verification is left to the default implementation.
|
||||
pub struct GeminiClientCertVerifier {}
|
||||
|
||||
impl GeminiClientCertVerifier {
|
||||
pub fn new() -> Arc<dyn rustls::server::ClientCertVerifier> {
|
||||
Arc::new(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::server::ClientCertVerifier for GeminiClientCertVerifier {
|
||||
/// Make client certificate optional.
|
||||
fn client_auth_mandatory(&self) -> Option<bool> {
|
||||
Some(false)
|
||||
}
|
||||
|
||||
/// Do not provide CA names.
|
||||
fn client_auth_root_subjects(&self) -> Option<rustls::DistinguishedNames> {
|
||||
Some(vec![])
|
||||
}
|
||||
|
||||
/// “Verify” client certificates.
|
||||
///
|
||||
/// Actually do not verify much, mostly that the certificate is well-formed. Like Rustls, we
|
||||
/// rely on the WebPKI crate to verify the certificate. It rejects self-signed client
|
||||
/// certificates early in the verification stage (we can't do much against that), and we ignore
|
||||
/// that error because Gemini clients mostly use self-signed certificates, so we can miss other
|
||||
/// WebPKI verifications errors. We should use a validation process that reports all issues
|
||||
/// found or provide a way to filter acceptable issues, but we can't blame anyone on this…
|
||||
fn verify_client_cert(
|
||||
&self,
|
||||
end_entity: &rustls::Certificate,
|
||||
intermediates: &[rustls::Certificate],
|
||||
now: std::time::SystemTime,
|
||||
) -> Result<rustls::server::ClientCertVerified, rustls::Error> {
|
||||
let cert = webpki::EndEntityCert::try_from(end_entity.0.as_ref())
|
||||
.map_err(|_| rustls::Error::InvalidCertificateEncoding)?;
|
||||
let now = webpki::Time::try_from(now).map_err(|_| rustls::Error::FailedToGetCurrentTime)?;
|
||||
let verified = rustls::server::ClientCertVerified::assertion();
|
||||
match cert.verify_is_valid_tls_client_cert(
|
||||
SUPPORTED_SIG_ALGS,
|
||||
&webpki::TlsClientTrustAnchors(&vec![]),
|
||||
&intermediates
|
||||
.iter()
|
||||
.map(|c| c.0.as_ref())
|
||||
.collect::<Vec<&[u8]>>(),
|
||||
now,
|
||||
) {
|
||||
Ok(()) => Ok(verified),
|
||||
Err(e) => match e {
|
||||
// It's OK for client certs to be self-signed.
|
||||
webpki::Error::CaUsedAsEndEntity => Ok(verified),
|
||||
// Any other error is fatal.
|
||||
_ => Err(rustls::Error::InvalidCertificateData(format!(
|
||||
"invalid client cert: {}",
|
||||
e
|
||||
))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue