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::{
|
use tokio_gemini::{
|
||||||
certs::{
|
certs::{fingerprint::CertFingerprint, SelfsignedCertVerifier},
|
||||||
fingerprint::{self, CertFingerprint},
|
|
||||||
SelfsignedCertVerifier,
|
|
||||||
},
|
|
||||||
Client, LibError,
|
Client, LibError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -47,7 +44,7 @@ impl SelfsignedCertVerifier for CertVerifier {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Host = {}\nFingerprint = {}",
|
"Host = {}\nFingerprint = {}",
|
||||||
host,
|
host,
|
||||||
CertFingerprint::<fingerprint::Sha256>::new(cert).base64(),
|
CertFingerprint::new_sha256(cert).base64(),
|
||||||
);
|
);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,7 @@ use tokio::io::AsyncBufReadExt;
|
||||||
use tokio_rustls::rustls::pki_types::{CertificateDer, UnixTime};
|
use tokio_rustls::rustls::pki_types::{CertificateDer, UnixTime};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
certs::{
|
certs::{fingerprint::CertFingerprint, SelfsignedCert, SelfsignedCertVerifier},
|
||||||
fingerprint::{self, CertFingerprint, HashAlgo},
|
|
||||||
SelfsignedCert, SelfsignedCertVerifier,
|
|
||||||
},
|
|
||||||
LibError,
|
LibError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,20 +45,27 @@ impl FileBasedCertVerifier {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let algo = match algo {
|
let fp = match algo {
|
||||||
"sha256" => HashAlgo::Sha256,
|
"sha256" => CertFingerprint::try_from_sha256_b64(fp),
|
||||||
"sha512" => HashAlgo::Sha512,
|
"sha512" => CertFingerprint::try_from_sha512_b64(fp),
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("Unknown hash algorithm {:?}, skipping", algo);
|
eprintln!("Unknown hash algorithm {:?}, skipping", algo);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let fp = match fp {
|
||||||
|
Ok(fp) => fp,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Fingerprint decoding error: {:?}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
map.insert(
|
map.insert(
|
||||||
host.to_owned(),
|
host.to_owned(),
|
||||||
SelfsignedCert {
|
SelfsignedCert {
|
||||||
algo,
|
fingerprint: fp,
|
||||||
fingerprint: fp.to_owned(),
|
|
||||||
expires,
|
expires,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -98,25 +102,15 @@ impl SelfsignedCertVerifier for FileBasedCertVerifier {
|
||||||
|
|
||||||
if let Some(known_cert) = self.map.get(host) {
|
if let Some(known_cert) = self.map.get(host) {
|
||||||
// if host is found in known_hosts, compare certs
|
// if host is found in known_hosts, compare certs
|
||||||
let this_fp = match known_cert.algo {
|
let this_fp = match known_cert.fingerprint {
|
||||||
HashAlgo::Sha256 => CertFingerprint::<fingerprint::Sha256>::new(cert).base64(),
|
CertFingerprint::Sha256(_) => CertFingerprint::new_sha256(cert),
|
||||||
HashAlgo::Sha512 => CertFingerprint::<fingerprint::Sha512>::new(cert).base64(),
|
CertFingerprint::Sha512(_) => CertFingerprint::new_sha512(cert),
|
||||||
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
if this_fp == known_cert.fingerprint {
|
Ok(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)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// host is unknown, generate hash and add to known_hosts
|
// 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();
|
let this_fp = this_hash.base64();
|
||||||
// TODO: DANE cert check, use this_hash.hex() for this
|
// TODO: DANE cert check, use this_hash.hex() for this
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
@ -141,8 +135,7 @@ impl SelfsignedCertVerifier for FileBasedCertVerifier {
|
||||||
self.map.insert(
|
self.map.insert(
|
||||||
host.to_owned(),
|
host.to_owned(),
|
||||||
SelfsignedCert {
|
SelfsignedCert {
|
||||||
algo: HashAlgo::Sha256,
|
fingerprint: this_hash,
|
||||||
fingerprint: this_fp,
|
|
||||||
expires: 0, // TODO after implementing cert parsing in tokio-gemini
|
expires: 0, // TODO after implementing cert parsing in tokio-gemini
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
//! TLS cert fingerprint generators
|
//! TLS cert fingerprint generators
|
||||||
|
|
||||||
|
use std::array::TryFromSliceError;
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
pub use sha2::{Digest, Sha256, Sha512};
|
pub use sha2::{Digest, Sha256, Sha512};
|
||||||
|
|
||||||
use base16ct::upper as b16;
|
use base16ct::upper as b16;
|
||||||
|
@ -7,77 +10,118 @@ use base64ct::{Base64Unpadded as b64, Encoding};
|
||||||
|
|
||||||
use super::verifier::CertificateDer;
|
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 SHA256_HEX_LEN: usize = 64_; // (256 / 8) * 2
|
||||||
pub const SHA512_HEX_LEN: usize = 128; // (512 / 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 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()
|
pub const SHA512_B64_LEN: usize = 88; // 4 * ((512 / 8) as f64 / 3 as f64).ceil()
|
||||||
|
|
||||||
/// Supported hashing algorithms
|
/// Enum holding a TLS cert hash or raw cert bytes.
|
||||||
#[derive(Debug, Clone, Copy)]
|
/// Provides hex (base16) and base64 bin-to-text methods,
|
||||||
pub enum HashAlgo {
|
|
||||||
Sha256,
|
|
||||||
Sha512,
|
|
||||||
/// Do not hash, compare the whole cert
|
|
||||||
Raw,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Structure holding a TLS cert hash
|
|
||||||
/// and providing bin2text methods,
|
|
||||||
/// mostly for use in [`crate::certs::SelfsignedCertVerifier`]
|
/// mostly for use in [`crate::certs::SelfsignedCertVerifier`]
|
||||||
pub struct CertFingerprint<T: Digest> {
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
hash: sha2::digest::Output<T>,
|
pub enum CertFingerprint {
|
||||||
|
Sha256([u8; SHA256_LEN]),
|
||||||
|
Sha512([u8; SHA512_LEN]),
|
||||||
|
Raw(Bytes),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Digest> CertFingerprint<T> {
|
impl CertFingerprint {
|
||||||
/// Generate a TLS cert hash.
|
pub fn new_sha256(cert: &CertificateDer) -> Self {
|
||||||
///
|
let mut hasher = Sha256::new();
|
||||||
/// # 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();
|
|
||||||
for chunk in cert.chunks(128) {
|
for chunk in cert.chunks(128) {
|
||||||
hasher.update(chunk);
|
hasher.update(chunk);
|
||||||
}
|
}
|
||||||
CertFingerprint {
|
Self::Sha256(hasher.finalize().into())
|
||||||
hash: hasher.finalize(),
|
}
|
||||||
|
|
||||||
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ pub trait SelfsignedCertVerifier: Send + Sync {
|
||||||
/// suggested for using in a [`SelfsignedCertVerifier`] cert storage
|
/// suggested for using in a [`SelfsignedCertVerifier`] cert storage
|
||||||
/// (like `HashMap<String, SelfsignedCert>`, as a known_hosts parsing result)
|
/// (like `HashMap<String, SelfsignedCert>`, as a known_hosts parsing result)
|
||||||
pub struct SelfsignedCert {
|
pub struct SelfsignedCert {
|
||||||
pub algo: fingerprint::HashAlgo,
|
pub fingerprint: fingerprint::CertFingerprint,
|
||||||
pub fingerprint: String,
|
|
||||||
pub expires: u64,
|
pub expires: u64,
|
||||||
}
|
}
|
||||||
|
|
27
src/dns.rs
27
src/dns.rs
|
@ -1,6 +1,5 @@
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use bytes::Bytes;
|
|
||||||
use hickory_client::{
|
use hickory_client::{
|
||||||
client::{AsyncClient, ClientHandle},
|
client::{AsyncClient, ClientHandle},
|
||||||
proto::iocompat::AsyncIoTokioAsStd,
|
proto::iocompat::AsyncIoTokioAsStd,
|
||||||
|
@ -12,7 +11,7 @@ use hickory_client::{
|
||||||
};
|
};
|
||||||
use tokio::net::ToSocketAddrs;
|
use tokio::net::ToSocketAddrs;
|
||||||
|
|
||||||
use crate::{certs::fingerprint::HashAlgo, LibError};
|
use crate::{certs::fingerprint::CertFingerprint, LibError};
|
||||||
|
|
||||||
pub struct DnsClient(AsyncClient);
|
pub struct DnsClient(AsyncClient);
|
||||||
|
|
||||||
|
@ -74,7 +73,7 @@ impl DnsClient {
|
||||||
&mut self,
|
&mut self,
|
||||||
domain: &str,
|
domain: &str,
|
||||||
port: u16,
|
port: u16,
|
||||||
) -> Result<impl Iterator<Item = (HashAlgo, Bytes)>, LibError> {
|
) -> Result<impl Iterator<Item = CertFingerprint>, LibError> {
|
||||||
let answers = self
|
let answers = self
|
||||||
.0
|
.0
|
||||||
.query(
|
.query(
|
||||||
|
@ -91,18 +90,16 @@ impl DnsClient {
|
||||||
if tlsa.cert_usage() == CertUsage::DomainIssued
|
if tlsa.cert_usage() == CertUsage::DomainIssued
|
||||||
&& tlsa.selector() == Selector::Spki
|
&& tlsa.selector() == Selector::Spki
|
||||||
{
|
{
|
||||||
let hash_algo = match tlsa.matching() {
|
match tlsa.matching() {
|
||||||
Matching::Sha256 => HashAlgo::Sha256,
|
Matching::Sha256 => CertFingerprint::try_from_sha256(tlsa.cert_data())
|
||||||
Matching::Sha512 => HashAlgo::Sha512,
|
.map(Some)
|
||||||
Matching::Raw => HashAlgo::Raw,
|
.unwrap_or(None),
|
||||||
_ => {
|
Matching::Sha512 => CertFingerprint::try_from_sha512(tlsa.cert_data())
|
||||||
return None;
|
.map(Some)
|
||||||
}
|
.unwrap_or(None),
|
||||||
};
|
Matching::Raw => Some(CertFingerprint::from_raw(tlsa.cert_data())),
|
||||||
// TODO: optimize?
|
_ => None,
|
||||||
// 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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue