refactor: rewrite cert::fingerprint once again
This commit is contained in:
parent
5b43635b62
commit
446bf9f67b
5 changed files with 138 additions and 108 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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<Sha256> {
|
||||
/// Encode the TLS cert SHA-256 hash as HEX (base16).
|
||||
/// Resulting string is 64 bytes length.
|
||||
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(&self.hash, &mut buf).unwrap().to_owned()
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
Self::Sha512(hash) => {
|
||||
let mut buf = [0u8; SHA512_HEX_LEN];
|
||||
b16::encode_str(&self.hash, &mut buf).unwrap().to_owned()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode the TLS cert SHA-512 hash as base64.
|
||||
/// Resulting string is 88 bytes length.
|
||||
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(&self.hash, &mut buf).unwrap().to_owned()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
25
src/dns.rs
25
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<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;
|
||||
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,
|
||||
}
|
||||
};
|
||||
// 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())))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue