refactor: rewrite cert::fingerprint once again

This commit is contained in:
DarkCat09 2024-08-15 17:47:43 +04:00
parent 5b43635b62
commit 446bf9f67b
Signed by: DarkCat09
GPG key ID: 0A26CD5B3345D6E3
5 changed files with 138 additions and 108 deletions

View file

@ -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::<fingerprint::Sha256>::new(cert).base64(),
CertFingerprint::new_sha256(cert).base64(),
);
Ok(true)
}

View file

@ -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::<fingerprint::Sha256>::new(cert).base64(),
HashAlgo::Sha512 => CertFingerprint::<fingerprint::Sha512>::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::<fingerprint::Sha256>::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
},
);

View file

@ -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<T: Digest> {
hash: sha2::digest::Output<T>,
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CertFingerprint {
Sha256([u8; SHA256_LEN]),
Sha512([u8; SHA512_LEN]),
Raw(Bytes),
}
impl<T: Digest> CertFingerprint<T> {
/// Generate a TLS cert hash.
///
/// # Examples
/// ```
/// use tokio_gemini::certs::fingerprint::{CertFingerprint, Sha256};
///
/// let hash = CertFingerprint::<Sha256>::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<Self, TryFromSliceError> {
Ok(Self::Sha256(value.try_into()?))
}
pub fn try_from_sha512(value: &[u8]) -> Result<Self, TryFromSliceError> {
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<Self, base64ct::Error> {
let mut buf = [0u8; SHA256_LEN];
b64::decode(value, &mut buf)?;
Ok(Self::Sha256(buf))
}
pub fn try_from_sha512_b64(value: &str) -> Result<Self, base64ct::Error> {
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<u8> = 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<u8> = Vec::new();
b64::encode(&cert, &mut buf).unwrap().to_owned()
}
}
}
}
impl CertFingerprint<Sha256> {
/// 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<Sha512> {
/// 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()
}
}

View file

@ -28,7 +28,6 @@ pub trait SelfsignedCertVerifier: Send + Sync {
/// suggested for using in a [`SelfsignedCertVerifier`] cert storage
/// (like `HashMap<String, SelfsignedCert>`, as a known_hosts parsing result)
pub struct SelfsignedCert {
pub algo: fingerprint::HashAlgo,
pub fingerprint: String,
pub fingerprint: fingerprint::CertFingerprint,
pub expires: u64,
}

View file

@ -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<impl Iterator<Item = (HashAlgo, Bytes)>, LibError> {
) -> Result<impl Iterator<Item = CertFingerprint>, 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<u8>,
// 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
}