diff --git a/Cargo.lock b/Cargo.lock index 975411f..0d8ba78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64ct" version = "1.6.0" @@ -489,6 +495,7 @@ dependencies = [ name = "tokio-gemini" version = "0.4.0" dependencies = [ + "base16ct", "base64ct", "bytes", "dashmap", diff --git a/Cargo.toml b/Cargo.toml index cee330f..2f65bc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ description = "Gemini protocol client and server implementation written in Rust categories = ["network-programming"] [dependencies] +base16ct = "0.2.0" base64ct = "1.6.0" bytes = "1.7.1" dashmap = { version = "6.0.1", optional = true } diff --git a/examples/simple.rs b/examples/simple.rs index 36d4fc2..47ee3bd 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,6 +1,6 @@ use tokio_gemini::{ certs::{ - fingerprint::{generate_fingerprint, Algorithm}, + fingerprint::{self, CertFingerprint}, SelfsignedCertVerifier, }, Client, LibError, @@ -47,7 +47,7 @@ impl SelfsignedCertVerifier for CertVerifier { eprintln!( "Host = {}\nFingerprint = {}", host, - generate_fingerprint(cert, Algorithm::Sha512), + CertFingerprint::::new(cert).base64(), ); Ok(true) } diff --git a/src/certs/file_sscv.rs b/src/certs/file_sscv.rs index 9fba98b..82b60eb 100644 --- a/src/certs/file_sscv.rs +++ b/src/certs/file_sscv.rs @@ -1,4 +1,4 @@ -use std::{io::Write, os::fd::AsFd, sync::Mutex}; +use std::{borrow::Cow, io::Write, os::fd::AsFd, sync::Mutex}; use dashmap::DashMap; use tokio::io::AsyncBufReadExt; @@ -6,7 +6,7 @@ use tokio_rustls::rustls::pki_types::{CertificateDer, UnixTime}; use crate::{ certs::{ - fingerprint::{generate_fingerprint, Algorithm}, + fingerprint::{self, CertFingerprint, HashAlgo}, SelfsignedCert, SelfsignedCertVerifier, }, LibError, @@ -38,11 +38,8 @@ impl FileBasedCertVerifier { // host expires hash-algo fingerprint // Example: // dc09.ru 1722930541 sha512 dGVzdHRlc3R0ZXN0Cg - if let [host, expires, algo, fp] = buf - .split_whitespace() - .take(4) - .collect::>() - .as_slice() + if let [host, expires, algo, fp] = + *buf.split_whitespace().take(4).collect::>() { let expires = if let Ok(num) = expires.parse::() { num @@ -51,9 +48,9 @@ impl FileBasedCertVerifier { continue; }; - let algo = match *algo { - "sha256" => Algorithm::Sha256, - "sha512" => Algorithm::Sha512, + let algo = match algo { + "sha256" => HashAlgo::Sha256, + "sha512" => HashAlgo::Sha512, _ => { eprintln!("Unknown hash algorithm {:?}, skipping", algo); continue; @@ -61,10 +58,10 @@ impl FileBasedCertVerifier { }; map.insert( - (*host).to_owned(), + host.to_owned(), SelfsignedCert { algo, - fingerprint: (*fp).to_owned(), + fingerprint: fp.to_owned(), expires, }, ); @@ -101,7 +98,10 @@ 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 = generate_fingerprint(cert, known_cert.algo); + let this_fp = match known_cert.algo { + HashAlgo::Sha256 => CertFingerprint::::new(cert).base64(), + HashAlgo::Sha512 => CertFingerprint::::new(cert).base64(), + }; if this_fp == known_cert.fingerprint { // current cert hash matches known cert hash eprintln!("Cert for {} matched: {}", &host, &this_fp); @@ -116,7 +116,9 @@ impl SelfsignedCertVerifier for FileBasedCertVerifier { } } else { // host is unknown, generate hash and add to known_hosts - let this_fp = generate_fingerprint(cert, Algorithm::Sha512); + let this_hash = CertFingerprint::::new(cert); + let this_fp = this_hash.base64(); + // TODO: DANE cert check, use this_hash.hex() for this eprintln!( "Warning: updating known_hosts with cert {} for {}", &this_fp, &host, @@ -127,7 +129,7 @@ impl SelfsignedCertVerifier for FileBasedCertVerifier { // because we are not allowed to mutate &self let mut f = std::fs::File::from(self.fd.lock().unwrap().try_clone()?); f.write_all(host.as_bytes())?; - f.write_all(b" 0 sha512 ")?; // TODO after implementing `expires` + f.write_all(b" 0 sha256 ")?; // TODO after implementing `expires` f.write_all(this_fp.as_bytes())?; f.write_all(b"\n")?; Ok::<(), std::io::Error>(()) @@ -139,7 +141,7 @@ impl SelfsignedCertVerifier for FileBasedCertVerifier { self.map.insert( host.to_owned(), SelfsignedCert { - algo: Algorithm::Sha512, + algo: HashAlgo::Sha256, fingerprint: this_fp, expires: 0, // TODO after implementing cert parsing in tokio-gemini }, diff --git a/src/certs/fingerprint.rs b/src/certs/fingerprint.rs index 94f0680..b25f01e 100644 --- a/src/certs/fingerprint.rs +++ b/src/certs/fingerprint.rs @@ -1,49 +1,60 @@ -//! Helpers for TLS cert fingerprint generating +//! TLS cert fingerprint generators -use base64ct::{Base64Unpadded, Encoding}; -use sha2::{Digest, Sha256, Sha512}; +pub use sha2::{Digest, Sha256, Sha512}; + +use base16ct::upper as b16; +use base64ct::{Base64Unpadded as b64, Encoding}; use super::verifier::CertificateDer; +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() -/// Certificate hashing algorithms -/// supported in this library #[derive(Debug, Clone, Copy)] -pub enum Algorithm { +pub enum HashAlgo { Sha256, Sha512, } -/// Generate a fingerprint for the provided certificate -/// using the specified algorithm + base64 -pub fn generate_fingerprint(cert: &CertificateDer, algo: Algorithm) -> String { - match algo { - Algorithm::Sha256 => { - let mut hasher = Sha256::new(); - for chunk in cert.chunks(128) { - hasher.update(chunk); - } - let bin = hasher.finalize(); - let mut buf = [0; SHA256_B64_LEN]; - // Note on unwrap: - // Encoder returns error only if buffer length is insufficient. - // SHA-256 is *always* 256 bits (32 bytes), - // after we apply base64 formula we get 44 bytes in output including padding. - // See also comment near const SHA256_B64_LEN - Base64Unpadded::encode(&bin, &mut buf).unwrap().to_owned() +pub struct CertFingerprint { + hash: sha2::digest::Output, +} + +impl CertFingerprint { + pub fn new(cert: &CertificateDer) -> Self { + let mut hasher = T::new(); + for chunk in cert.chunks(128) { + hasher.update(chunk); } - Algorithm::Sha512 => { - let mut hasher = Sha512::new(); - for chunk in cert.chunks(128) { - hasher.update(chunk); - } - let bin = hasher.finalize(); - let mut buf = [0; SHA512_B64_LEN]; - // Same explanation for unwrap, see above - // SHA-512 is always 512 bits or 64 bytes - Base64Unpadded::encode(&bin, &mut buf).unwrap().to_owned() + CertFingerprint { + hash: hasher.finalize(), } } } + +impl CertFingerprint { + pub fn hex(&self) -> String { + let mut buf = [0u8; SHA256_HEX_LEN]; + b16::encode_str(&self.hash, &mut buf).unwrap().to_owned() + } + + pub fn base64(&self) -> String { + let mut buf = [0u8; SHA256_B64_LEN]; + b64::encode(&self.hash, &mut buf).unwrap().to_owned() + } +} + +impl CertFingerprint { + pub fn hex(&self) -> String { + let mut buf = [0u8; SHA512_HEX_LEN]; + b16::encode_str(&self.hash, &mut buf).unwrap().to_owned() + } + + 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 898888e..8018914 100644 --- a/src/certs/mod.rs +++ b/src/certs/mod.rs @@ -28,7 +28,7 @@ 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: crate::certs::fingerprint::Algorithm, + pub algo: fingerprint::HashAlgo, pub fingerprint: String, pub expires: u64, }