diff --git a/Cargo.lock b/Cargo.lock index 7acdd2f..1a42696 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,21 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bytes" version = "1.6.1" @@ -50,6 +65,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -65,6 +109,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -280,6 +334,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "socket2" version = "0.5.7" @@ -348,8 +413,10 @@ dependencies = [ name = "tokio-gemini" version = "0.1.0" dependencies = [ + "base64ct", "mime", "num_enum", + "sha2", "tokio", "tokio-rustls", "url", @@ -395,6 +462,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -433,6 +506,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index b7a9173..607b167 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,10 @@ description = "Gemini protocol client & server written in Rust with Tokio" categories = ["network-programming"] [dependencies] +base64ct = "1.6.0" mime = "0.3.17" num_enum = "0.7.3" +sha2 = "0.10.8" tokio = { version = "1.39.2", features = ["io-util", "net"] } tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring"] } url = "2.5.2" diff --git a/src/certs/fingerprint.rs b/src/certs/fingerprint.rs new file mode 100644 index 0000000..8a89ad9 --- /dev/null +++ b/src/certs/fingerprint.rs @@ -0,0 +1,38 @@ +use base64ct::{Base64Unpadded, Encoding}; +use sha2::{Digest, Sha256, Sha512}; + +use super::verifier::CertificateDer; + +const SHA256_B64_LEN: usize = 44; // 4 * ((256 / 8) as f64 / 3 as f64).ceil() +const SHA512_B64_LEN: usize = 88; // 4 * ((512 / 8) as f64 / 3 as f64).ceil() + +pub enum Algorithm { + Sha256, + Sha512, +} + +pub fn generate_fingerprint( + cert: &CertificateDer, + algo: Algorithm, +) -> Result { + 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]; + Base64Unpadded::encode(&bin, &mut buf).map(|hash| hash.to_owned()) + } + 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]; + Base64Unpadded::encode(&bin, &mut buf).map(|hash| hash.to_owned()) + } + } +} diff --git a/src/certs/insecure.rs b/src/certs/insecure.rs new file mode 100644 index 0000000..754f1d6 --- /dev/null +++ b/src/certs/insecure.rs @@ -0,0 +1,59 @@ +use tokio_rustls::rustls::{ + self, + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + crypto::CryptoProvider, +}; + +#[derive(Debug)] +pub struct AllowAllCertVerifier(CryptoProvider); + +impl AllowAllCertVerifier { + fn yes_i_know_what_i_am_doing(provider: CryptoProvider) -> Self { + AllowAllCertVerifier(provider) + } +} + +impl ServerCertVerifier for AllowAllCertVerifier { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &rustls::pki_types::CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &rustls::pki_types::CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } +} diff --git a/src/certs/mod.rs b/src/certs/mod.rs new file mode 100644 index 0000000..2013ac7 --- /dev/null +++ b/src/certs/mod.rs @@ -0,0 +1,3 @@ +pub mod fingerprint; +pub mod insecure; +pub mod verifier; diff --git a/src/certs/verifier.rs b/src/certs/verifier.rs new file mode 100644 index 0000000..93ef818 --- /dev/null +++ b/src/certs/verifier.rs @@ -0,0 +1,122 @@ +pub use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime}; + +use tokio_rustls::rustls::{ + self, + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, +}; + +pub trait SelfsignedCertVerifier: Send + Sync { + fn verify( + &self, + cert: &CertificateDer, + host: &str, + now: UnixTime, + ) -> Result; +} + +pub struct SelfsignedCert { + algo: super::fingerprint::Algorithm, + fingerprint: String, + expires: u64, +} + +pub struct CustomCertVerifier { + provider: rustls::crypto::CryptoProvider, + webpki_verifier: Option, + ss_allowed: bool, + ss_verifier: dyn SelfsignedCertVerifier, +} + +impl ServerCertVerifier for CustomCertVerifier { + fn verify_server_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + server_name: &ServerName<'_>, + ocsp_response: &[u8], + now: UnixTime, + ) -> Result { + // if webpki CA certs enabled + if let Some(wv) = &self.webpki_verifier { + match wv.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now) + { + Ok(verified) => { + return Ok(verified); + } + Err( + e @ rustls::Error::InvalidCertificate(rustls::CertificateError::UnknownIssuer), + ) => { + if !self.ss_allowed { + return Err(e); + } + // go ahead, verify as self-signed + } + Err(e) => { + // any other error, probably related to invalid cert + return Err(e); + } + } + } + + // TODO: certificate validation when webpki_verifier is not used + + // if self-signed certs enabled + if self.ss_allowed { + // TODO: check if expired or provide handy API to check it + // (probably with rustls-webpki's webpki::Cert) + if self + .ss_verifier + .verify(end_entity, &server_name.to_str().as_ref(), now)? + { + return Ok(ServerCertVerified::assertion()); + } + } + + // both disabled (shouldn't happen) + Err(rustls::Error::UnsupportedNameType) // not sure if chosen correct enum item + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self.provider.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self.provider.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.provider + .signature_verification_algorithms + .supported_schemes() + } +} + +impl std::fmt::Debug for CustomCertVerifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "CustomCertVerifier {{ provider: {:?}, webpki_verifier: {:?} }}", + self.provider, self.webpki_verifier + ) + } +} diff --git a/src/lib.rs b/src/lib.rs index 9af7c17..e6f456e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod certs; pub mod client; pub mod error; pub mod response;