refactor: rewrite self-signed cert verifier
This commit is contained in:
parent
4314df372b
commit
e042a139bf
11 changed files with 294 additions and 272 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -879,6 +879,7 @@ dependencies = [
|
|||
name = "tokio-gemini"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base16ct",
|
||||
"base64ct",
|
||||
"bytes",
|
||||
|
|
|
@ -24,6 +24,7 @@ tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring
|
|||
|
||||
dashmap = { version = "6.0.1", optional = true }
|
||||
hickory-client = { version = "0.24.1", optional = true }
|
||||
async-trait = "0.1.81"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] }
|
||||
|
|
109
examples/main.rs
109
examples/main.rs
|
@ -1,11 +1,13 @@
|
|||
use tokio_gemini::{
|
||||
certs::{file_sscv::FileBasedCertVerifier, insecure::AllowAllCertVerifier},
|
||||
certs::{
|
||||
dane, file_sscv::KnownHostsFile, fingerprint::CertFingerprint, SelfsignedCertVerifier,
|
||||
},
|
||||
dns::DnsClient,
|
||||
Client, LibError,
|
||||
};
|
||||
|
||||
//
|
||||
// cargo add tokio_gemini -F file-sscv,hickory
|
||||
// cargo add tokio_gemini -F file-sscv,dane
|
||||
// cargo add tokio -F macros,rt-multi-thread,io-util,fs
|
||||
//
|
||||
|
||||
|
@ -13,12 +15,6 @@ const USAGE: &str = "-k\t\tinsecure mode (trust all certs)
|
|||
-d <DNS server addr>\tuse custom DNS for resolving & DANE
|
||||
-h\t\tshow help";
|
||||
|
||||
struct Config {
|
||||
insecure: bool,
|
||||
dns: Option<String>,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), LibError> {
|
||||
let config = parse_args();
|
||||
|
@ -68,11 +64,11 @@ fn parse_args() -> Config {
|
|||
"-k" => config.insecure = true,
|
||||
"-d" => expected_dns = true,
|
||||
"-h" => {
|
||||
println!("{}", USAGE);
|
||||
eprintln!("{}", USAGE);
|
||||
std::process::exit(0);
|
||||
}
|
||||
url => {
|
||||
println!("URL: {}", url);
|
||||
eprintln!("URL: {}", url);
|
||||
config.url = url.to_owned();
|
||||
break;
|
||||
}
|
||||
|
@ -80,7 +76,7 @@ fn parse_args() -> Config {
|
|||
}
|
||||
|
||||
if expected_dns {
|
||||
println!("{}", USAGE);
|
||||
eprintln!("{}", USAGE);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
|
@ -94,21 +90,94 @@ async fn build_client(config: &Config) -> Result<Client, LibError> {
|
|||
None
|
||||
};
|
||||
|
||||
let known_hosts = KnownHostsFile::parse_file("known_hosts").await?;
|
||||
let verifier = CertVerifier {
|
||||
known_hosts,
|
||||
dns: dns.clone(),
|
||||
};
|
||||
|
||||
let client = tokio_gemini::Client::builder();
|
||||
|
||||
let client = if config.insecure {
|
||||
client.with_custom_verifier(AllowAllCertVerifier::yes_i_know_what_i_am_doing())
|
||||
client.dangerous_with_no_verifier()
|
||||
} else {
|
||||
client.with_selfsigned_cert_verifier(
|
||||
FileBasedCertVerifier::init("known_hosts", dns.clone()).await?,
|
||||
)
|
||||
client.with_selfsigned_cert_verifier(verifier)
|
||||
};
|
||||
|
||||
let client = if let Some(dns) = dns {
|
||||
client.with_dns_client(dns)
|
||||
} else {
|
||||
client
|
||||
};
|
||||
let client = client.maybe_with_dns_client(dns);
|
||||
|
||||
Ok(client.build())
|
||||
}
|
||||
|
||||
struct Config {
|
||||
insecure: bool,
|
||||
dns: Option<String>,
|
||||
url: String,
|
||||
}
|
||||
|
||||
struct CertVerifier {
|
||||
known_hosts: KnownHostsFile,
|
||||
dns: Option<DnsClient>,
|
||||
}
|
||||
|
||||
impl SelfsignedCertVerifier for CertVerifier {
|
||||
async fn verify<'c>(
|
||||
&self,
|
||||
cert: &'c tokio_gemini::certs::CertificateDer<'c>,
|
||||
host: &str,
|
||||
port: u16,
|
||||
) -> Result<bool, tokio_gemini::LibError> {
|
||||
if let Some(known) = self.known_hosts.get_known_cert(host) {
|
||||
// if found in known_hosts, just compare certs
|
||||
Ok(known.fingerprint.hash_and_compare(cert))
|
||||
} else {
|
||||
// otherwise, generate a hash and add to known_hosts
|
||||
let hash = if let Some(dns) = &self.dns {
|
||||
// if DNS client is configured, try verifying the cert
|
||||
// via DANE instead of blindly trusting
|
||||
match dane::dane(&dns, cert, host, port).await {
|
||||
Ok(hash) => hash, // use the fingerprint matched with TLSA record
|
||||
Err(LibError::HostLookupError) => {
|
||||
// no TLSA record found -- server admin haven't set it up
|
||||
eprintln!(
|
||||
"TLSA not configured for tcp:{}:{}, trusting on first use",
|
||||
host, port,
|
||||
);
|
||||
// just generate a hash for this cert
|
||||
CertFingerprint::new_sha256(cert)
|
||||
}
|
||||
Err(e) => {
|
||||
// some other problem (e.g. DNS server rejected the request),
|
||||
// we shouldn't continue
|
||||
eprintln!("DANE verification failed: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("DANE disabled");
|
||||
// just generate a hash for this cert
|
||||
CertFingerprint::new_sha256(cert)
|
||||
};
|
||||
|
||||
let fingerprint = hash.base64();
|
||||
let fptype = hash.fingerprint_type_str();
|
||||
eprintln!(
|
||||
"Warning: adding trusted cert for {} with FP {}",
|
||||
host, &fingerprint,
|
||||
);
|
||||
|
||||
// adding the cert hash to trusted
|
||||
// can be done simplier:
|
||||
// self.known_hosts.add_trusted_cert(host, hash).await.unwrap...
|
||||
self.known_hosts.add_cert_to_hashmap(host, hash);
|
||||
self.known_hosts
|
||||
.add_cert_to_file(host, &fingerprint, fptype)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Cert saved in-memory, unable to write to file: {:?}", e);
|
||||
});
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,17 +33,18 @@ async fn main() -> Result<(), LibError> {
|
|||
struct CertVerifier;
|
||||
|
||||
impl SelfsignedCertVerifier for CertVerifier {
|
||||
fn verify(
|
||||
async fn verify<'c>(
|
||||
&self,
|
||||
cert: &tokio_gemini::certs::CertificateDer,
|
||||
cert: &'c tokio_gemini::certs::CertificateDer<'c>,
|
||||
host: &str,
|
||||
_now: tokio_gemini::certs::UnixTime,
|
||||
) -> Result<bool, tokio_rustls::rustls::Error> {
|
||||
// For real verification example with known_hosts file
|
||||
port: u16,
|
||||
) -> Result<bool, tokio_gemini::LibError> {
|
||||
// For real verification example with known_hosts and DANE
|
||||
// see examples/main.rs
|
||||
eprintln!(
|
||||
"Host = {}\nFingerprint = {}",
|
||||
"Host = {}:{}\nFingerprint = {}",
|
||||
host,
|
||||
port,
|
||||
CertFingerprint::new_sha256(cert).base64(),
|
||||
);
|
||||
Ok(true)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
//! Custom verifier for Rustls accepting any TLS cert
|
||||
//! (usually called "insecure mode")
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio_rustls::rustls::{
|
||||
self,
|
||||
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
|
@ -9,28 +11,28 @@ use tokio_rustls::rustls::{
|
|||
|
||||
/// Custom verifier for Rustls accepting any TLS certificate
|
||||
#[derive(Debug)]
|
||||
pub struct AllowAllCertVerifier(std::sync::Arc<CryptoProvider>);
|
||||
pub struct AllowAllCertVerifier(Arc<CryptoProvider>);
|
||||
|
||||
impl AllowAllCertVerifier {
|
||||
/// Constructor for this verifier.
|
||||
/// Use only if you know what you are doing.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// let client = tokio_gemini::Client::builder()
|
||||
/// .with_custom_verifier(AllowAllCertVerifier::yes_i_know_what_i_am_doing())
|
||||
/// .build()
|
||||
/// ```
|
||||
pub fn yes_i_know_what_i_am_doing() -> Self {
|
||||
/// Constructor for this verifier
|
||||
pub fn new() -> Self {
|
||||
AllowAllCertVerifier(
|
||||
CryptoProvider::get_default()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| std::sync::Arc::new(rustls::crypto::ring::default_provider())),
|
||||
.unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<CryptoProvider>> for AllowAllCertVerifier {
|
||||
#[inline]
|
||||
fn from(value: Arc<CryptoProvider>) -> Self {
|
||||
AllowAllCertVerifier(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerCertVerifier for AllowAllCertVerifier {
|
||||
#[inline]
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
||||
|
@ -42,6 +44,7 @@ impl ServerCertVerifier for AllowAllCertVerifier {
|
|||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
|
@ -56,6 +59,7 @@ impl ServerCertVerifier for AllowAllCertVerifier {
|
|||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
|
@ -70,6 +74,7 @@ impl ServerCertVerifier for AllowAllCertVerifier {
|
|||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
self.0.signature_verification_algorithms.supported_schemes()
|
||||
}
|
54
src/certs/dane.rs
Normal file
54
src/certs/dane.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use crate::{
|
||||
certs::{fingerprint::CertFingerprint, CertificateDer},
|
||||
dns::DnsClient,
|
||||
LibError,
|
||||
};
|
||||
|
||||
pub async fn dane<'d>(
|
||||
dns: &DnsClient,
|
||||
cert: &CertificateDer<'d>,
|
||||
host: &str,
|
||||
port: u16,
|
||||
) -> Result<CertFingerprint, LibError> {
|
||||
let mut dns = dns.clone();
|
||||
|
||||
let mut sha256: Option<CertFingerprint> = None;
|
||||
let mut sha512: Option<CertFingerprint> = None;
|
||||
|
||||
for tlsa_fp in dns.query_tlsa(host, port).await? {
|
||||
match tlsa_fp {
|
||||
CertFingerprint::Sha256(_) => {
|
||||
if sha256.is_none() {
|
||||
sha256 = Some(CertFingerprint::new_sha256(cert));
|
||||
}
|
||||
let this_fp = sha256.as_ref().unwrap();
|
||||
if this_fp == &tlsa_fp {
|
||||
return Ok(sha256.unwrap());
|
||||
}
|
||||
}
|
||||
CertFingerprint::Sha512(_) => {
|
||||
if sha512.is_none() {
|
||||
sha512 = Some(CertFingerprint::new_sha512(cert));
|
||||
}
|
||||
let this_fp = sha512.as_ref().unwrap();
|
||||
if this_fp == &tlsa_fp {
|
||||
return Ok(sha512.unwrap());
|
||||
}
|
||||
}
|
||||
CertFingerprint::Raw(_) => {
|
||||
let this_fp = CertFingerprint::new_raw(cert);
|
||||
if this_fp == tlsa_fp {
|
||||
return Ok(CertFingerprint::new_sha256(cert));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sha256) = sha256 {
|
||||
Ok(sha256)
|
||||
} else if let Some(sha512) = sha512 {
|
||||
Ok(sha512)
|
||||
} else {
|
||||
Ok(CertFingerprint::new_sha256(cert))
|
||||
}
|
||||
}
|
|
@ -1,35 +1,25 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
io::{BufWriter, Write},
|
||||
os::fd::AsFd,
|
||||
path::Path,
|
||||
sync::Mutex,
|
||||
};
|
||||
use std::{borrow::Cow, os::fd::AsFd, path::Path, sync::Mutex};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio_rustls::rustls::pki_types::{CertificateDer, UnixTime};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufWriter};
|
||||
|
||||
use crate::{
|
||||
certs::{fingerprint::CertFingerprint, SelfsignedCert, SelfsignedCertVerifier},
|
||||
LibError,
|
||||
};
|
||||
use crate::certs::{fingerprint::CertFingerprint, SelfsignedCert};
|
||||
|
||||
#[cfg(feature = "hickory")]
|
||||
use crate::dns::DnsClient;
|
||||
|
||||
pub struct FileBasedCertVerifier {
|
||||
pub struct KnownHostsFile {
|
||||
fd: Mutex<std::os::fd::OwnedFd>,
|
||||
map: DashMap<String, SelfsignedCert>,
|
||||
#[cfg(feature = "hickory")]
|
||||
dns: Option<DnsClient>,
|
||||
}
|
||||
|
||||
impl FileBasedCertVerifier {
|
||||
pub async fn init(
|
||||
path: impl AsRef<Path>,
|
||||
#[cfg(feature = "hickory")] dns: Option<DnsClient>,
|
||||
) -> Result<Self, LibError> {
|
||||
impl KnownHostsFile {
|
||||
/// Read or create a known_hosts file at the given path.
|
||||
/// Format of known_hosts is: _(fields are separated by any space sequence)_
|
||||
/// ```
|
||||
/// #host expires algo base64 fingerprint
|
||||
/// dc09.ru 1722930541 sha512 dGVzdHRlc3R0ZXN0Cg
|
||||
/// ```
|
||||
pub async fn parse_file(path: impl AsRef<Path>) -> std::io::Result<KnownHostsFile> {
|
||||
// TODO: remove eprintln!()s
|
||||
|
||||
let map = DashMap::new();
|
||||
|
||||
if tokio::fs::try_exists(&path).await? {
|
||||
|
@ -45,10 +35,6 @@ impl FileBasedCertVerifier {
|
|||
break;
|
||||
}
|
||||
|
||||
// Format:
|
||||
// host <space> expires <space> hash-algo <space> fingerprint
|
||||
// Example:
|
||||
// dc09.ru 1722930541 sha512 dGVzdHRlc3R0ZXN0Cg
|
||||
if let [host, expires, algo, fp] =
|
||||
*buf.split_whitespace().take(4).collect::<Cow<[&str]>>()
|
||||
{
|
||||
|
@ -91,34 +77,42 @@ impl FileBasedCertVerifier {
|
|||
}
|
||||
|
||||
let fd = Mutex::new(
|
||||
std::fs::OpenOptions::new()
|
||||
tokio::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(path)?
|
||||
.open(path)
|
||||
.await?
|
||||
.as_fd()
|
||||
.try_clone_to_owned()?,
|
||||
);
|
||||
|
||||
Ok(FileBasedCertVerifier {
|
||||
fd,
|
||||
map,
|
||||
#[cfg(feature = "hickory")]
|
||||
dns,
|
||||
})
|
||||
Ok(KnownHostsFile { fd, map })
|
||||
}
|
||||
}
|
||||
|
||||
impl FileBasedCertVerifier {
|
||||
pub fn add_trusted_cert(
|
||||
pub fn get_known_cert(
|
||||
&self,
|
||||
host: &str,
|
||||
hash: CertFingerprint,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let fp = hash.base64();
|
||||
let ft = hash.fingerprint_type_str();
|
||||
// TODO: remove eprintln!()
|
||||
eprintln!("Warning: adding {} cert with FP {}", &host, &fp);
|
||||
) -> Option<dashmap::mapref::one::Ref<String, SelfsignedCert>> {
|
||||
self.map.get(host)
|
||||
}
|
||||
|
||||
/// Add a new entry to known_hosts (both to the in-memory hashmap and to the file).
|
||||
/// Should be called only after the TLS cert is verified for validity (including DANE).
|
||||
/// Exactly the same as `add_cert_to_hashmap` + `add_cert_to_file`.
|
||||
pub async fn add_trusted_cert(&self, host: &str, hash: CertFingerprint) -> std::io::Result<()> {
|
||||
let fp = hash.base64();
|
||||
let fptype = hash.fingerprint_type_str();
|
||||
|
||||
self.add_cert_to_hashmap(host, hash);
|
||||
|
||||
self.add_cert_to_file(host, &fp, fptype).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a new trusted cert only to the in-memory hashmap,
|
||||
/// do not write to the known_hosts file.
|
||||
pub fn add_cert_to_hashmap(&self, host: &str, hash: CertFingerprint) {
|
||||
self.map.insert(
|
||||
host.to_owned(),
|
||||
SelfsignedCert {
|
||||
|
@ -126,117 +120,35 @@ impl FileBasedCertVerifier {
|
|||
expires: 0, // TODO after implementing cert parsing in tokio-gemini
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Write a new trusted cert's fingerprint to the known_hosts file.
|
||||
/// - `fp` is a TLS cert hash in base64 (see [`CertFingerprint::base64`]),
|
||||
/// - `fptype` is a name of hashing algorithm (see [`CertFingerprint::fingerprint_type_str`]).
|
||||
/// The certificate will not be trusted in the current session unless you call `add_cert_to_hashmap`,
|
||||
/// so use this function only if you need modularity, otherwise just use `add_trusted_cert`.
|
||||
pub async fn add_cert_to_file(
|
||||
&self,
|
||||
host: &str,
|
||||
fp: &str,
|
||||
fptype: &str,
|
||||
) -> std::io::Result<()> {
|
||||
// trick with cloning file descriptor
|
||||
// because we are not allowed to mutate &self
|
||||
let f = std::fs::File::from(self.fd.lock().unwrap().try_clone()?);
|
||||
let f = tokio::fs::File::from(std::fs::File::from(self.fd.lock().unwrap().try_clone()?));
|
||||
let mut bw = BufWriter::new(f);
|
||||
bw.write_all(host.as_bytes())?;
|
||||
bw.write_all(b" 0 ")?; // TODO after implementing `expires`
|
||||
bw.write_all(ft.as_bytes())?;
|
||||
bw.write_all(b" ")?;
|
||||
bw.write_all(fp.as_bytes())?;
|
||||
bw.write_all(b"\n")?;
|
||||
bw.flush()?;
|
||||
|
||||
bw.write_all(host.as_bytes()).await?;
|
||||
bw.write_all(b" 0 ").await?; // TODO after implementing `expires`
|
||||
|
||||
bw.write_all(fptype.as_bytes()).await?;
|
||||
bw.write_all(b" ").await?;
|
||||
|
||||
bw.write_all(fp.as_bytes()).await?;
|
||||
bw.write_all(b"\n").await?;
|
||||
|
||||
bw.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "hickory")]
|
||||
pub fn dane(
|
||||
&self,
|
||||
cert: &CertificateDer,
|
||||
host: &str,
|
||||
port: u16,
|
||||
) -> Result<CertFingerprint, LibError> {
|
||||
let mut dns = if let Some(dns) = &self.dns {
|
||||
dns.clone()
|
||||
} else {
|
||||
return Err(LibError::HostLookupError);
|
||||
};
|
||||
|
||||
let rt = tokio::runtime::Handle::try_current()?;
|
||||
|
||||
let mut sha256: Option<CertFingerprint> = None;
|
||||
let mut sha512: Option<CertFingerprint> = None;
|
||||
|
||||
for tlsa_fp in rt.block_on(dns.query_tlsa(host, port))? {
|
||||
match tlsa_fp {
|
||||
CertFingerprint::Sha256(_) => {
|
||||
if sha256.is_none() {
|
||||
sha256 = Some(CertFingerprint::new_sha256(cert));
|
||||
}
|
||||
let this_fp = sha256.as_ref().unwrap();
|
||||
if this_fp == &tlsa_fp {
|
||||
return Ok(sha256.unwrap());
|
||||
}
|
||||
}
|
||||
CertFingerprint::Sha512(_) => {
|
||||
if sha512.is_none() {
|
||||
sha512 = Some(CertFingerprint::new_sha512(cert));
|
||||
}
|
||||
let this_fp = sha512.as_ref().unwrap();
|
||||
if this_fp == &tlsa_fp {
|
||||
return Ok(sha512.unwrap());
|
||||
}
|
||||
}
|
||||
CertFingerprint::Raw(_) => {
|
||||
let this_fp = CertFingerprint::new_raw(cert);
|
||||
if this_fp == tlsa_fp {
|
||||
return Ok(CertFingerprint::new_sha256(cert));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sha256) = sha256 {
|
||||
Ok(sha256)
|
||||
} else if let Some(sha512) = sha512 {
|
||||
Ok(sha512)
|
||||
} else {
|
||||
Ok(CertFingerprint::new_sha256(cert))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelfsignedCertVerifier for FileBasedCertVerifier {
|
||||
fn verify(
|
||||
&self,
|
||||
cert: &CertificateDer,
|
||||
host: &str,
|
||||
_now: UnixTime,
|
||||
) -> Result<bool, tokio_rustls::rustls::Error> {
|
||||
//
|
||||
// TODO: remove eprintln!()s
|
||||
//
|
||||
|
||||
if let Some(known_cert) = self.map.get(host) {
|
||||
// if host is found in known_hosts, compare certs
|
||||
let this_hash = match known_cert.fingerprint {
|
||||
CertFingerprint::Sha256(_) => CertFingerprint::new_sha256(cert),
|
||||
CertFingerprint::Sha512(_) => CertFingerprint::new_sha512(cert),
|
||||
CertFingerprint::Raw(_) => CertFingerprint::new_raw(cert),
|
||||
};
|
||||
Ok(this_hash == known_cert.fingerprint)
|
||||
} else {
|
||||
// host is unknown, generate hash and add to known_hosts
|
||||
#[cfg(feature = "hickory")]
|
||||
let this_hash = match self.dane(cert, host, 1965) {
|
||||
Ok(hash) => hash,
|
||||
Err(e) => {
|
||||
eprintln!("DANE verification failed: {:?}", e);
|
||||
CertFingerprint::new_sha256(cert)
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "hickory"))]
|
||||
let this_hash = CertFingerprint::new_sha256(cert);
|
||||
|
||||
self.add_trusted_cert(host, this_hash).unwrap_or_else(|e| {
|
||||
eprintln!("Unable to add new cert: {:?}", e);
|
||||
});
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ pub use sha2::{Digest, Sha256, Sha512};
|
|||
use base16ct::upper as b16;
|
||||
use base64ct::{Base64Unpadded as b64, Encoding};
|
||||
|
||||
use super::verifier::CertificateDer;
|
||||
use super::CertificateDer;
|
||||
|
||||
pub const SHA256_LEN: usize = 32; // 256 / 8
|
||||
pub const SHA512_LEN: usize = 64; // 512 / 8
|
||||
|
@ -127,6 +127,15 @@ impl CertFingerprint {
|
|||
}
|
||||
|
||||
impl CertFingerprint {
|
||||
pub fn hash_and_compare(&self, cert: &CertificateDer) -> bool {
|
||||
let hash = match self {
|
||||
Self::Sha256(_) => Self::new_sha256(cert),
|
||||
Self::Sha512(_) => Self::new_sha512(cert),
|
||||
Self::Raw(_) => Self::new_raw(cert),
|
||||
};
|
||||
*self == hash
|
||||
}
|
||||
|
||||
pub fn fingerprint_type_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Sha256(_) => "sha256",
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
//! Everything related to TLS certs verification
|
||||
|
||||
pub mod allow_all;
|
||||
pub mod fingerprint;
|
||||
pub mod insecure;
|
||||
|
||||
#[cfg(feature = "file-sscv")]
|
||||
pub mod file_sscv;
|
||||
|
||||
pub(crate) mod verifier;
|
||||
#[cfg(feature = "hickory")]
|
||||
pub mod dane;
|
||||
|
||||
use async_trait::async_trait;
|
||||
pub use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
|
||||
use tokio_rustls::rustls;
|
||||
|
||||
/// Trait for implementing self-signed cert verifiers
|
||||
/// like [`file_sscv::FileBasedCertVerifier`]
|
||||
/// (probably via known_hosts with TOFU policy or DANE verification)
|
||||
/// Trait for implementing self-signed cert verifiers,
|
||||
/// probably via known_hosts with TOFU policy or DANE verification.
|
||||
/// It is recommended to use helpers from file_sscv.
|
||||
#[async_trait]
|
||||
pub trait SelfsignedCertVerifier: Send + Sync {
|
||||
fn verify(
|
||||
async fn verify<'c>(
|
||||
&self,
|
||||
cert: &CertificateDer,
|
||||
cert: &'c CertificateDer<'c>,
|
||||
host: &str,
|
||||
now: UnixTime,
|
||||
) -> Result<bool, rustls::Error>;
|
||||
port: u16,
|
||||
// now: UnixTime,
|
||||
) -> Result<bool, crate::LibError>;
|
||||
}
|
||||
|
||||
/// Structure holding a cert fingerprint and expiry date,
|
||||
|
|
|
@ -3,20 +3,21 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
certs::{verifier::CustomCertVerifier, SelfsignedCertVerifier},
|
||||
certs::{allow_all::AllowAllCertVerifier, SelfsignedCertVerifier},
|
||||
Client,
|
||||
};
|
||||
|
||||
#[cfg(feature = "hickory")]
|
||||
use crate::dns::DnsClient;
|
||||
|
||||
use tokio_rustls::rustls::{self, client::danger::ServerCertVerifier, SupportedProtocolVersion};
|
||||
use tokio_rustls::{
|
||||
rustls::{self, SupportedProtocolVersion},
|
||||
TlsConnector,
|
||||
};
|
||||
|
||||
/// Builder for creating configured [`Client`] instance
|
||||
pub struct ClientBuilder {
|
||||
root_certs: rustls::RootCertStore,
|
||||
ss_verifier: Option<Box<dyn SelfsignedCertVerifier>>,
|
||||
custom_verifier: Option<Arc<dyn ServerCertVerifier + 'static>>,
|
||||
ss_verifier: Option<Arc<dyn SelfsignedCertVerifier>>,
|
||||
tls_versions: Option<&'static [&'static SupportedProtocolVersion]>,
|
||||
#[cfg(feature = "hickory")]
|
||||
dns: Option<DnsClient>,
|
||||
|
@ -34,9 +35,7 @@ impl ClientBuilder {
|
|||
/// no cert verifiers and default TLS versions.
|
||||
pub fn new() -> Self {
|
||||
ClientBuilder {
|
||||
root_certs: rustls::RootCertStore::empty(),
|
||||
ss_verifier: None,
|
||||
custom_verifier: None,
|
||||
tls_versions: None,
|
||||
#[cfg(feature = "hickory")]
|
||||
dns: None,
|
||||
|
@ -55,30 +54,19 @@ impl ClientBuilder {
|
|||
} else {
|
||||
rustls::DEFAULT_VERSIONS
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let tls_config = if let Some(cv) = self.custom_verifier {
|
||||
tls_config.dangerous().with_custom_certificate_verifier(cv)
|
||||
} else if let Some(ssv) = self.ss_verifier {
|
||||
tls_config
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(CustomCertVerifier {
|
||||
provider: provider.clone(),
|
||||
ss_verifier: ssv,
|
||||
}))
|
||||
} else {
|
||||
tls_config.with_root_certificates(self.root_certs)
|
||||
};
|
||||
.unwrap()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(AllowAllCertVerifier::from(provider)));
|
||||
|
||||
// TODO
|
||||
let tls_config = tls_config.with_no_client_auth();
|
||||
|
||||
#[cfg(feature = "hickory")]
|
||||
if let Some(dns) = self.dns {
|
||||
return Client::from((tls_config, dns));
|
||||
Client {
|
||||
connector: TlsConnector::from(Arc::new(tls_config)),
|
||||
ss_verifier: self.ss_verifier,
|
||||
#[cfg(feature = "hickory")]
|
||||
dns: self.dns,
|
||||
}
|
||||
|
||||
Client::from(tls_config)
|
||||
}
|
||||
|
||||
/// Limit the supported TLS versions list to the specified ones.
|
||||
|
@ -99,25 +87,28 @@ impl ClientBuilder {
|
|||
mut self,
|
||||
ss_verifier: impl SelfsignedCertVerifier + 'static,
|
||||
) -> Self {
|
||||
self.ss_verifier = Some(Box::new(ss_verifier));
|
||||
self.ss_verifier = Some(Arc::new(ss_verifier));
|
||||
self
|
||||
}
|
||||
|
||||
/// Include a custom TLS cert verifier implementing rustls' [`ServerCertVerifier`].
|
||||
/// Normally need to be used only for [`crate::certs::insecure::AllowAllCertVerifier`].
|
||||
/// Note: the webpki verifier and a self-signed cert verifier are not called
|
||||
/// when a custom verifier is set.
|
||||
pub fn with_custom_verifier(
|
||||
mut self,
|
||||
custom_verifier: impl ServerCertVerifier + 'static,
|
||||
) -> Self {
|
||||
self.custom_verifier = Some(Arc::new(custom_verifier));
|
||||
/// Disable TLS cert verification.
|
||||
/// Use only if you definitely know what you are doing.
|
||||
// TODO: will make sense after implementing a typestate pattern
|
||||
pub fn dangerous_with_no_verifier(self) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a custom DNS client for resolving IPs.
|
||||
#[cfg(feature = "hickory")]
|
||||
pub fn with_dns_client(mut self, dns: DnsClient) -> Self {
|
||||
self.dns = Some(dns);
|
||||
self
|
||||
}
|
||||
|
||||
/// Same as [`Builder::with_dns_client`], but accepts Option.
|
||||
#[cfg(feature = "hickory")]
|
||||
pub fn maybe_with_dns_client(mut self, dns: Option<DnsClient>) -> Self {
|
||||
self.dns = dns;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,11 @@ use hickory_client::rr::IntoName;
|
|||
#[cfg(feature = "hickory")]
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use crate::{error::*, status::*};
|
||||
use crate::{
|
||||
certs::{SelfsignedCertVerifier, ServerName},
|
||||
error::*,
|
||||
status::*,
|
||||
};
|
||||
use builder::ClientBuilder;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
@ -21,45 +25,18 @@ use tokio::{
|
|||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
};
|
||||
use tokio_rustls::{
|
||||
rustls::{self, pki_types},
|
||||
TlsConnector,
|
||||
};
|
||||
use tokio_rustls::TlsConnector;
|
||||
use url::Url;
|
||||
|
||||
pub struct Client {
|
||||
connector: TlsConnector,
|
||||
pub(crate) connector: TlsConnector,
|
||||
pub(crate) ss_verifier: Option<Arc<dyn SelfsignedCertVerifier>>,
|
||||
#[cfg(feature = "hickory")]
|
||||
dns: Option<DnsClient>,
|
||||
}
|
||||
|
||||
impl From<rustls::ClientConfig> for Client {
|
||||
/// Create a Client from a Rustls config.
|
||||
#[inline]
|
||||
fn from(config: rustls::ClientConfig) -> Self {
|
||||
Client {
|
||||
connector: TlsConnector::from(Arc::new(config)),
|
||||
#[cfg(feature = "hickory")]
|
||||
dns: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hickory")]
|
||||
impl From<(rustls::ClientConfig, DnsClient)> for Client {
|
||||
/// Create a Client from a Rustls config and
|
||||
/// a DnsClient instance as a custom resolver.
|
||||
#[inline]
|
||||
fn from(value: (rustls::ClientConfig, DnsClient)) -> Self {
|
||||
Client {
|
||||
connector: TlsConnector::from(Arc::new(value.0)),
|
||||
dns: Some(value.1),
|
||||
}
|
||||
}
|
||||
pub(crate) dns: Option<DnsClient>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a Client with a customized configuration,
|
||||
/// Construct a Client with a customized configuration,
|
||||
/// see [`ClientBuilder`] methods.
|
||||
pub fn builder() -> ClientBuilder {
|
||||
ClientBuilder::new()
|
||||
|
@ -117,7 +94,7 @@ impl Client {
|
|||
host: &str,
|
||||
port: u16,
|
||||
) -> Result<Response, LibError> {
|
||||
let domain = pki_types::ServerName::try_from(host)
|
||||
let domain = ServerName::try_from(host)
|
||||
.map_err(|_| InvalidUrl::ConvertError)?
|
||||
.to_owned();
|
||||
|
||||
|
@ -170,7 +147,7 @@ impl Client {
|
|||
Ok(Response::new(status, message, stream))
|
||||
}
|
||||
|
||||
pub async fn try_connect(&self, host: &str, port: u16) -> Result<TcpStream, LibError> {
|
||||
async fn try_connect(&self, host: &str, port: u16) -> Result<TcpStream, LibError> {
|
||||
let mut last_err: Option<std::io::Error> = None;
|
||||
|
||||
#[cfg(feature = "hickory")]
|
||||
|
|
Loading…
Reference in a new issue