From 446bf9f67b4ce5aa76a3e80cda36f6b3f78dfd8b Mon Sep 17 00:00:00 2001 From: DarkCat09 Date: Thu, 15 Aug 2024 17:47:43 +0400 Subject: [PATCH] refactor: rewrite cert::fingerprint once again --- examples/simple.rs | 7 +- src/certs/file_sscv.rs | 47 +++++------- src/certs/fingerprint.rs | 162 +++++++++++++++++++++++++-------------- src/certs/mod.rs | 3 +- src/dns.rs | 27 +++---- 5 files changed, 138 insertions(+), 108 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index 47ee3bd..b652577 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,8 +1,5 @@ use tokio_gemini::{ - certs::{ - fingerprint::{self, CertFingerprint}, - SelfsignedCertVerifier, - }, + certs::{fingerprint::CertFingerprint, SelfsignedCertVerifier}, Client, LibError, }; @@ -47,7 +44,7 @@ impl SelfsignedCertVerifier for CertVerifier { eprintln!( "Host = {}\nFingerprint = {}", host, - CertFingerprint::::new(cert).base64(), + CertFingerprint::new_sha256(cert).base64(), ); Ok(true) } diff --git a/src/certs/file_sscv.rs b/src/certs/file_sscv.rs index 8a4ae27..460c37b 100644 --- a/src/certs/file_sscv.rs +++ b/src/certs/file_sscv.rs @@ -5,10 +5,7 @@ use tokio::io::AsyncBufReadExt; use tokio_rustls::rustls::pki_types::{CertificateDer, UnixTime}; use crate::{ - certs::{ - fingerprint::{self, CertFingerprint, HashAlgo}, - SelfsignedCert, SelfsignedCertVerifier, - }, + certs::{fingerprint::CertFingerprint, SelfsignedCert, SelfsignedCertVerifier}, LibError, }; @@ -48,20 +45,27 @@ impl FileBasedCertVerifier { continue; }; - let algo = match algo { - "sha256" => HashAlgo::Sha256, - "sha512" => HashAlgo::Sha512, + let fp = match algo { + "sha256" => CertFingerprint::try_from_sha256_b64(fp), + "sha512" => CertFingerprint::try_from_sha512_b64(fp), _ => { eprintln!("Unknown hash algorithm {:?}, skipping", algo); continue; } }; + let fp = match fp { + Ok(fp) => fp, + Err(e) => { + eprintln!("Fingerprint decoding error: {:?}", e); + continue; + } + }; + map.insert( host.to_owned(), SelfsignedCert { - algo, - fingerprint: fp.to_owned(), + fingerprint: fp, expires, }, ); @@ -98,25 +102,15 @@ impl SelfsignedCertVerifier for FileBasedCertVerifier { if let Some(known_cert) = self.map.get(host) { // if host is found in known_hosts, compare certs - let this_fp = match known_cert.algo { - HashAlgo::Sha256 => CertFingerprint::::new(cert).base64(), - HashAlgo::Sha512 => CertFingerprint::::new(cert).base64(), + let this_fp = match known_cert.fingerprint { + CertFingerprint::Sha256(_) => CertFingerprint::new_sha256(cert), + CertFingerprint::Sha512(_) => CertFingerprint::new_sha512(cert), + _ => unreachable!(), }; - if this_fp == known_cert.fingerprint { - // current cert hash matches known cert hash - eprintln!("Cert for {} matched: {}", &host, &this_fp); - Ok(true) - } else { - // TODO (after implementing `expires`) update cert if known is expired - eprintln!( - "Error: certs do not match! Possibly MitM attack.\nKnown FP: {}\nGot: {}", - &known_cert.fingerprint, &this_fp, - ); - Ok(false) - } + Ok(this_fp == known_cert.fingerprint) } else { // host is unknown, generate hash and add to known_hosts - let this_hash = CertFingerprint::::new(cert); + let this_hash = CertFingerprint::new_sha256(cert); let this_fp = this_hash.base64(); // TODO: DANE cert check, use this_hash.hex() for this eprintln!( @@ -141,8 +135,7 @@ impl SelfsignedCertVerifier for FileBasedCertVerifier { self.map.insert( host.to_owned(), SelfsignedCert { - algo: HashAlgo::Sha256, - fingerprint: this_fp, + fingerprint: this_hash, expires: 0, // TODO after implementing cert parsing in tokio-gemini }, ); diff --git a/src/certs/fingerprint.rs b/src/certs/fingerprint.rs index 164feba..c0f8df2 100644 --- a/src/certs/fingerprint.rs +++ b/src/certs/fingerprint.rs @@ -1,5 +1,8 @@ //! TLS cert fingerprint generators +use std::array::TryFromSliceError; + +use bytes::Bytes; pub use sha2::{Digest, Sha256, Sha512}; use base16ct::upper as b16; @@ -7,77 +10,118 @@ use base64ct::{Base64Unpadded as b64, Encoding}; use super::verifier::CertificateDer; +pub const SHA256_LEN: usize = 32; // 256 / 8 +pub const SHA512_LEN: usize = 64; // 512 / 8 + +#[allow(clippy::inconsistent_digit_grouping)] pub const SHA256_HEX_LEN: usize = 64_; // (256 / 8) * 2 pub const SHA512_HEX_LEN: usize = 128; // (512 / 8) * 2 pub const SHA256_B64_LEN: usize = 44; // 4 * ((256 / 8) as f64 / 3 as f64).ceil() pub const SHA512_B64_LEN: usize = 88; // 4 * ((512 / 8) as f64 / 3 as f64).ceil() -/// Supported hashing algorithms -#[derive(Debug, Clone, Copy)] -pub enum HashAlgo { - Sha256, - Sha512, - /// Do not hash, compare the whole cert - Raw, -} - -/// Structure holding a TLS cert hash -/// and providing bin2text methods, +/// Enum holding a TLS cert hash or raw cert bytes. +/// Provides hex (base16) and base64 bin-to-text methods, /// mostly for use in [`crate::certs::SelfsignedCertVerifier`] -pub struct CertFingerprint { - hash: sha2::digest::Output, +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CertFingerprint { + Sha256([u8; SHA256_LEN]), + Sha512([u8; SHA512_LEN]), + Raw(Bytes), } -impl CertFingerprint { - /// Generate a TLS cert hash. - /// - /// # Examples - /// ``` - /// use tokio_gemini::certs::fingerprint::{CertFingerprint, Sha256}; - /// - /// let hash = CertFingerprint::::new(rustls_cert); - /// let fingerprint = hash.base64(); - /// ``` - pub fn new(cert: &CertificateDer) -> Self { - let mut hasher = T::new(); +impl CertFingerprint { + pub fn new_sha256(cert: &CertificateDer) -> Self { + let mut hasher = Sha256::new(); for chunk in cert.chunks(128) { hasher.update(chunk); } - CertFingerprint { - hash: hasher.finalize(), + Self::Sha256(hasher.finalize().into()) + } + + pub fn new_sha512(cert: &CertificateDer) -> Self { + let mut hasher = Sha512::new(); + for chunk in cert.chunks(128) { + hasher.update(chunk); + } + Self::Sha512(hasher.finalize().into()) + } + + #[inline] + pub fn new_raw(cert: &CertificateDer) -> Self { + // TODO: looks like an absolutely inefficient solution + Self::Raw(Bytes::copy_from_slice(cert.as_ref())) + } +} + +impl CertFingerprint { + pub fn try_from_sha256(value: &[u8]) -> Result { + Ok(Self::Sha256(value.try_into()?)) + } + + pub fn try_from_sha512(value: &[u8]) -> Result { + Ok(Self::Sha512(value.try_into()?)) + } + + #[inline] + pub fn from_raw(value: &[u8]) -> Self { + Self::Raw(Bytes::copy_from_slice(value)) + } + + pub fn try_from_sha256_b64(value: &str) -> Result { + let mut buf = [0u8; SHA256_LEN]; + b64::decode(value, &mut buf)?; + Ok(Self::Sha256(buf)) + } + + pub fn try_from_sha512_b64(value: &str) -> Result { + let mut buf = [0u8; SHA512_LEN]; + b64::decode(value, &mut buf)?; + Ok(Self::Sha512(buf)) + } +} + +impl CertFingerprint { + pub fn hex(&self) -> String { + match self { + Self::Sha256(hash) => { + let mut buf = [0u8; SHA256_HEX_LEN]; + b16::encode_str(hash.as_slice(), &mut buf) + // Note on `unwrap`: + // encoder returns an error only if an output buffer is too small, + // but we exactly know the required size (it's fixed for hashes). + // See also comments near `const SHA*_LEN` for formulas. + .unwrap() + .to_owned() + } + Self::Sha512(hash) => { + let mut buf = [0u8; SHA512_HEX_LEN]; + b16::encode_str(hash.as_slice(), &mut buf) + .unwrap() + .to_owned() + } + Self::Raw(cert) => { + let mut buf: Vec = Vec::new(); + b16::encode_str(&cert, &mut buf).unwrap().to_owned() + } + } + } + + pub fn base64(&self) -> String { + match self { + Self::Sha256(hash) => { + let mut buf = [0u8; SHA256_B64_LEN]; + // Same note on `unwrap`, see above + b64::encode(hash.as_slice(), &mut buf).unwrap().to_owned() + } + Self::Sha512(hash) => { + let mut buf = [0u8; SHA512_B64_LEN]; + b64::encode(hash.as_slice(), &mut buf).unwrap().to_owned() + } + Self::Raw(cert) => { + let mut buf: Vec = Vec::new(); + b64::encode(&cert, &mut buf).unwrap().to_owned() + } } } } - -impl CertFingerprint { - /// Encode the TLS cert SHA-256 hash as HEX (base16). - /// Resulting string is 64 bytes length. - pub fn hex(&self) -> String { - let mut buf = [0u8; SHA256_HEX_LEN]; - b16::encode_str(&self.hash, &mut buf).unwrap().to_owned() - } - - /// Encode the TLS cert SHA-256 hash as base64. - /// Resulting string is 44 bytes length. - pub fn base64(&self) -> String { - let mut buf = [0u8; SHA256_B64_LEN]; - b64::encode(&self.hash, &mut buf).unwrap().to_owned() - } -} - -impl CertFingerprint { - /// Encode the TLS cert SHA-512 hash as HEX (base16). - /// Resulting string is 128 bytes length. - pub fn hex(&self) -> String { - let mut buf = [0u8; SHA512_HEX_LEN]; - b16::encode_str(&self.hash, &mut buf).unwrap().to_owned() - } - - /// Encode the TLS cert SHA-512 hash as base64. - /// Resulting string is 88 bytes length. - pub fn base64(&self) -> String { - let mut buf = [0u8; SHA512_B64_LEN]; - b64::encode(&self.hash, &mut buf).unwrap().to_owned() - } -} diff --git a/src/certs/mod.rs b/src/certs/mod.rs index 8018914..fafe98f 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -28,7 +28,6 @@ pub trait SelfsignedCertVerifier: Send + Sync { /// suggested for using in a [`SelfsignedCertVerifier`] cert storage /// (like `HashMap`, as a known_hosts parsing result) pub struct SelfsignedCert { - pub algo: fingerprint::HashAlgo, - pub fingerprint: String, + pub fingerprint: fingerprint::CertFingerprint, pub expires: u64, } diff --git a/src/dns.rs b/src/dns.rs index b10cb39..ce74922 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -1,6 +1,5 @@ use std::net::IpAddr; -use bytes::Bytes; use hickory_client::{ client::{AsyncClient, ClientHandle}, proto::iocompat::AsyncIoTokioAsStd, @@ -12,7 +11,7 @@ use hickory_client::{ }; use tokio::net::ToSocketAddrs; -use crate::{certs::fingerprint::HashAlgo, LibError}; +use crate::{certs::fingerprint::CertFingerprint, LibError}; pub struct DnsClient(AsyncClient); @@ -74,7 +73,7 @@ impl DnsClient { &mut self, domain: &str, port: u16, - ) -> Result, LibError> { + ) -> Result, LibError> { let answers = self .0 .query( @@ -91,18 +90,16 @@ impl DnsClient { if tlsa.cert_usage() == CertUsage::DomainIssued && tlsa.selector() == Selector::Spki { - let hash_algo = match tlsa.matching() { - Matching::Sha256 => HashAlgo::Sha256, - Matching::Sha512 => HashAlgo::Sha512, - Matching::Raw => HashAlgo::Raw, - _ => { - return None; - } - }; - // TODO: optimize? - // if tlsa.cert_data() returned inner Vec, - // i could do this with zero-copy - Some((hash_algo, Bytes::copy_from_slice(tlsa.cert_data()))) + match tlsa.matching() { + Matching::Sha256 => CertFingerprint::try_from_sha256(tlsa.cert_data()) + .map(Some) + .unwrap_or(None), + Matching::Sha512 => CertFingerprint::try_from_sha512(tlsa.cert_data()) + .map(Some) + .unwrap_or(None), + Matching::Raw => Some(CertFingerprint::from_raw(tlsa.cert_data())), + _ => None, + } } else { None }