Compare commits

..

4 commits

7 changed files with 110 additions and 12 deletions

View file

@ -1,7 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use tokio_gemini::{ use tokio_gemini::{
certs::{ certs::{
dane, file_sscv::KnownHostsFile, fingerprint::CertFingerprint, SelfsignedCertVerifier, dane, file::sscv::KnownHostsFile, fingerprint::CertFingerprint, SelfsignedCertVerifier,
}, },
dns::DnsClient, dns::DnsClient,
Client, LibError, Client, LibError,

View file

@ -1,3 +1,6 @@
//! Utils for self-signed server certificate verifying
//! using a file with known hosts
use std::{borrow::Cow, os::fd::AsFd, path::Path, sync::Mutex}; use std::{borrow::Cow, os::fd::AsFd, path::Path, sync::Mutex};
use dashmap::DashMap; use dashmap::DashMap;
@ -5,6 +8,10 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufWriter};
use crate::certs::{fingerprint::CertFingerprint, SelfsignedCert}; use crate::certs::{fingerprint::CertFingerprint, SelfsignedCert};
/// Structure holding a known_hosts file descriptor
/// and an in-memory host-to-fingerprint hashmap,
/// providing a handy API to parse such files,
/// to get or store a cert fingerprint
pub struct KnownHostsFile { pub struct KnownHostsFile {
fd: Mutex<std::os::fd::OwnedFd>, fd: Mutex<std::os::fd::OwnedFd>,
map: DashMap<String, SelfsignedCert>, map: DashMap<String, SelfsignedCert>,
@ -89,6 +96,7 @@ impl KnownHostsFile {
Ok(KnownHostsFile { fd, map }) Ok(KnownHostsFile { fd, map })
} }
/// Get a known certificate fingerprint from the in-memory hashmap
pub fn get_known_cert( pub fn get_known_cert(
&self, &self,
host: &str, host: &str,

View file

@ -1,6 +1,7 @@
//! Everything related to TLS certs verification //! Everything related to TLS certs verification
pub mod fingerprint; pub mod fingerprint;
pub mod resolver;
pub mod verifier; pub mod verifier;
#[cfg(feature = "file")] #[cfg(feature = "file")]
@ -9,12 +10,18 @@ pub mod file;
#[cfg(feature = "hickory")] #[cfg(feature = "hickory")]
pub mod dane; pub mod dane;
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use tokio_rustls::rustls::{self, crypto::CryptoProvider};
pub use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime}; pub use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use resolver::InternalCertResolver;
/// Trait for implementing self-signed cert verifiers, /// Trait for implementing self-signed cert verifiers,
/// probably via known_hosts with TOFU policy or DANE verification. /// probably via known_hosts with TOFU policy or DANE verification.
/// It is recommended to use helpers from file_sscv. /// It is recommended to use helpers from [`file::sscv`]
#[async_trait] #[async_trait]
pub trait SelfsignedCertVerifier: Send + Sync { pub trait SelfsignedCertVerifier: Send + Sync {
async fn verify( async fn verify(
@ -33,3 +40,23 @@ pub struct SelfsignedCert {
pub fingerprint: fingerprint::CertFingerprint, pub fingerprint: fingerprint::CertFingerprint,
pub expires: u64, pub expires: u64,
} }
/// Trait for implementing authentication cert resolvers,
/// choosing the right TLS client cert for the given host and path.
/// It is recommended to use helpers from [`file::accr`]
#[async_trait]
pub trait AuthCertResolver: Send + Sync {
async fn load(&self, host: &str, port: u16, path: &str) -> Option<Arc<InternalCertResolver>>;
}
pub(crate) fn get_crypto_provider() -> Arc<CryptoProvider> {
if let Some(provider) = CryptoProvider::get_default() {
provider.clone()
} else {
let provider = rustls::crypto::ring::default_provider();
// unwrap: checked above that default not set
provider.install_default().unwrap();
// unwrap: default has been just installed
CryptoProvider::get_default().unwrap().clone()
}
}

40
src/certs/resolver.rs Normal file
View file

@ -0,0 +1,40 @@
use std::sync::Arc;
use tokio_rustls::rustls::{client::ResolvesClientCert, sign};
use webpki::types::{CertificateDer, PrivateKeyDer};
use crate::LibError;
#[derive(Debug)]
pub struct InternalCertResolver(Arc<sign::CertifiedKey>);
impl InternalCertResolver {
pub fn new(
chain: Vec<CertificateDer<'static>>,
key: PrivateKeyDer<'static>,
) -> Result<Self, LibError> {
let provider = crate::certs::get_crypto_provider();
let private_key = provider.key_provider.load_private_key(key)?;
Ok(InternalCertResolver(Arc::new(sign::CertifiedKey::new(
chain,
private_key,
))))
}
}
impl ResolvesClientCert for InternalCertResolver {
fn resolve(
&self,
_root_hint_subjects: &[&[u8]],
_sigschemes: &[tokio_rustls::rustls::SignatureScheme],
) -> Option<Arc<sign::CertifiedKey>> {
Some(self.0.clone())
}
#[inline]
fn has_certs(&self) -> bool {
true
}
}

View file

@ -10,10 +10,7 @@ use crate::{
#[cfg(feature = "hickory")] #[cfg(feature = "hickory")]
use crate::dns::DnsClient; use crate::dns::DnsClient;
use tokio_rustls::{ use tokio_rustls::rustls::{self, SupportedProtocolVersion};
rustls::{self, SupportedProtocolVersion},
TlsConnector,
};
/// Builder for creating configured [`Client`] instance /// Builder for creating configured [`Client`] instance
pub struct ClientBuilder { pub struct ClientBuilder {
@ -58,7 +55,8 @@ impl ClientBuilder {
let tls_config = tls_config.with_no_client_auth(); let tls_config = tls_config.with_no_client_auth();
Client { Client {
connector: TlsConnector::from(Arc::new(tls_config)), tls_config: Arc::new(tls_config),
cs_resolver: None,
ss_verifier: self.ss_verifier, ss_verifier: self.ss_verifier,
#[cfg(feature = "hickory")] #[cfg(feature = "hickory")]
dns: self.dns, dns: self.dns,

View file

@ -16,7 +16,7 @@ use hickory_client::rr::IntoName;
use std::net::SocketAddr; use std::net::SocketAddr;
use crate::{ use crate::{
certs::{SelfsignedCertVerifier, ServerName}, certs::{AuthCertResolver, SelfsignedCertVerifier, ServerName},
error::*, error::*,
into_url::IntoUrl, into_url::IntoUrl,
status::*, status::*,
@ -34,7 +34,8 @@ use tokio_rustls::{client::TlsStream, rustls, TlsConnector};
pub type ThisResponse = Response<BufReader<TlsStream<TcpStream>>>; pub type ThisResponse = Response<BufReader<TlsStream<TcpStream>>>;
pub struct Client { pub struct Client {
pub(crate) connector: TlsConnector, pub(crate) tls_config: Arc<rustls::ClientConfig>,
pub(crate) cs_resolver: Option<Arc<dyn AuthCertResolver>>,
pub(crate) ss_verifier: Option<Arc<dyn SelfsignedCertVerifier>>, pub(crate) ss_verifier: Option<Arc<dyn SelfsignedCertVerifier>>,
#[cfg(feature = "hickory")] #[cfg(feature = "hickory")]
pub(crate) dns: Option<DnsClient>, pub(crate) dns: Option<DnsClient>,
@ -139,10 +140,34 @@ impl Client {
.map_err(|_| InvalidUrl::ConvertError)? .map_err(|_| InvalidUrl::ConvertError)?
.to_owned(); .to_owned();
let connector = {
let cert = if let Some(cs_chooser) = &self.cs_resolver {
cs_chooser.load(host, port, "/").await // TODO path
} else {
None
};
let config = if let Some(cert) = cert {
// clone the underlying rustls::ClientConfig (not just copy Arc pointer)
let mut new_config = (*self.tls_config).clone();
// `cert` is actually an instance of `certs::resolver::InternalCertResolver`
new_config.client_auth_cert_resolver = cert;
Arc::new(new_config)
} else {
// Arc of config without client cert is stored in Client
// to avoid additional heap allocations on each request
// (new Arc-alloc only if auth cert is present, see the line above)
self.tls_config.clone()
};
TlsConnector::from(config)
};
// TCP connection // TCP connection
let stream = self.try_connect(host, port).await?; let stream = self.try_connect(host, port).await?;
// TLS connection via tokio-rustls // TLS connection via tokio-rustls
let stream = self.connector.connect(domain, stream).await?; let stream = connector.connect(domain, stream).await?;
// certificate verification // certificate verification
if let Some(ssv) = &self.ss_verifier { if let Some(ssv) = &self.ss_verifier {

View file

@ -27,7 +27,7 @@ fn check_parser() {
assert_eq!(status.second_digit(), 0u8); assert_eq!(status.second_digit(), 0u8);
} }
assert_eq!(resp.is_ok(), true); assert!(resp.is_ok());
assert_eq!(resp.message(), "text/gemini"); assert_eq!(resp.message(), "text/gemini");
@ -54,7 +54,7 @@ fn check_real_site() {
.block_on(client.request("gemini://geminiprotocol.net/docs")) .block_on(client.request("gemini://geminiprotocol.net/docs"))
.unwrap(); .unwrap();
assert_eq!(resp.is_ok(), true); // check if redirection is processed correctly assert!(resp.is_ok()); // check if redirection is processed correctly
{ {
let mime = resp.mime().unwrap(); let mime = resp.mime().unwrap();