Compare commits

...

4 commits

8 changed files with 110 additions and 166 deletions

10
Cargo.lock generated
View file

@ -890,7 +890,6 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls 0.26.0", "tokio-rustls 0.26.0",
"url", "url",
"webpki-roots",
] ]
[[package]] [[package]]
@ -1112,15 +1111,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "webpki-roots"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View file

@ -21,7 +21,6 @@ url = "2.5.2"
tokio = { version = "1.39.2", features = ["io-util", "net"] } tokio = { version = "1.39.2", features = ["io-util", "net"] }
tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring"] } tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring"] }
webpki-roots = { version = "0.26.3", optional = true }
dashmap = { version = "6.0.1", optional = true } dashmap = { version = "6.0.1", optional = true }
hickory-client = { version = "0.24.1", optional = true } hickory-client = { version = "0.24.1", optional = true }
@ -30,9 +29,9 @@ hickory-client = { version = "0.24.1", optional = true }
tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] }
[features] [features]
webpki = ["dep:webpki-roots"]
file-sscv = ["dep:dashmap", "tokio/fs"] file-sscv = ["dep:dashmap", "tokio/fs"]
dane = ["hickory"]
hickory = ["dep:hickory-client"] hickory = ["dep:hickory-client"]
hickory-dot = ["hickory", "hickory-client/dns-over-rustls"] hickory-dot = ["hickory", "hickory-client/dns-over-rustls"]
hickory-doh = ["hickory", "hickory-client/dns-over-https-rustls"] hickory-doh = ["hickory", "hickory-client/dns-over-https-rustls"]
@ -46,9 +45,4 @@ path = "examples/simple.rs"
[[example]] [[example]]
name = "main" name = "main"
path = "examples/main.rs" path = "examples/main.rs"
required-features = ["file-sscv"] required-features = ["file-sscv", "hickory"]
[[example]]
name = "dane"
path = "examples/dane.rs"
required-features = ["hickory"]

View file

@ -1,16 +0,0 @@
use tokio_gemini::dns::DnsClient;
#[tokio::main]
async fn main() {
let mut client = DnsClient::init(("1.1.1.1", 53)).await.unwrap();
println!(
"{}",
client
.query_tlsa("torproject.org", 443)
.await
.unwrap()
.next()
.unwrap()
.hex()
);
}

View file

@ -1,38 +1,30 @@
use tokio_gemini::certs::{file_sscv::FileBasedCertVerifier, insecure::AllowAllCertVerifier}; use tokio_gemini::{
certs::{file_sscv::FileBasedCertVerifier, insecure::AllowAllCertVerifier},
dns::DnsClient,
Client, LibError,
};
// //
// cargo add tokio_gemini -F file-sscv // cargo add tokio_gemini -F file-sscv,hickory
// cargo add tokio -F macros,rt-multi-thread,io-util,fs // cargo add tokio -F macros,rt-multi-thread,io-util,fs
// //
const USAGE: &str = "-k\t\tinsecure mode (trust all certs)
-d <DNS server addr>\tuse custom DNS for resolving & DANE
-h\t\tshow help";
struct Config {
insecure: bool,
dns: Option<String>,
url: String,
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), tokio_gemini::LibError> { async fn main() -> Result<(), LibError> {
let mut args = std::env::args(); let config = parse_args();
let mut insecure = false; let client = build_client(&config).await?;
let mut url = "gemini://geminiprotocol.net/".to_owned();
_ = args.next(); // skip exe path
if let Some(arg) = args.next() {
if arg == "-k" {
insecure = true;
if let Some(arg) = args.next() {
url = arg;
}
} else {
url = arg;
}
}
let client = if insecure { let mut resp = client.request(&config.url).await?;
tokio_gemini::Client::builder()
.with_custom_verifier(AllowAllCertVerifier::yes_i_know_what_i_am_doing())
.build()
} else {
tokio_gemini::Client::builder()
.with_selfsigned_cert_verifier(FileBasedCertVerifier::init("known_hosts").await?)
.build()
};
let mut resp = client.request(&url).await?;
{ {
let status_code = resp.status().status_code(); let status_code = resp.status().status_code();
@ -57,3 +49,66 @@ async fn main() -> Result<(), tokio_gemini::LibError> {
Ok(()) Ok(())
} }
fn parse_args() -> Config {
let mut config = Config {
insecure: false,
dns: None,
url: "gemini://geminiprotocol.net/".to_owned(),
};
let mut expected_dns = false;
for arg in std::env::args().skip(1) {
match arg.as_str() {
dns if expected_dns => {
config.dns = Some(dns.to_owned());
expected_dns = false;
}
"-k" => config.insecure = true,
"-d" => expected_dns = true,
"-h" => {
println!("{}", USAGE);
std::process::exit(0);
}
url => {
println!("URL: {}", url);
config.url = url.to_owned();
break;
}
}
}
if expected_dns {
println!("{}", USAGE);
std::process::exit(0);
}
config
}
async fn build_client(config: &Config) -> Result<Client, LibError> {
let dns = if let Some(addr) = &config.dns {
Some(DnsClient::init(addr).await?)
} else {
None
};
let client = tokio_gemini::Client::builder();
let client = if config.insecure {
client.with_custom_verifier(AllowAllCertVerifier::yes_i_know_what_i_am_doing())
} else {
client.with_selfsigned_cert_verifier(
FileBasedCertVerifier::init("known_hosts", dns.clone()).await?,
)
};
let client = if let Some(dns) = dns {
client.with_dns_client(dns)
} else {
client
};
Ok(client.build())
}

View file

@ -15,15 +15,21 @@ use crate::{
LibError, LibError,
}; };
#[cfg(feature = "hickory")]
use crate::dns::DnsClient;
pub struct FileBasedCertVerifier { pub struct FileBasedCertVerifier {
fd: Mutex<std::os::fd::OwnedFd>, fd: Mutex<std::os::fd::OwnedFd>,
map: DashMap<String, SelfsignedCert>, map: DashMap<String, SelfsignedCert>,
#[cfg(feature = "hickory")] #[cfg(feature = "hickory")]
dns: Option<crate::dns::DnsClient>, dns: Option<DnsClient>,
} }
impl FileBasedCertVerifier { impl FileBasedCertVerifier {
pub async fn init(path: impl AsRef<Path>) -> Result<Self, LibError> { pub async fn init(
path: impl AsRef<Path>,
#[cfg(feature = "hickory")] dns: Option<DnsClient>,
) -> Result<Self, LibError> {
let map = DashMap::new(); let map = DashMap::new();
if tokio::fs::try_exists(&path).await? { if tokio::fs::try_exists(&path).await? {
@ -97,7 +103,7 @@ impl FileBasedCertVerifier {
fd, fd,
map, map,
#[cfg(feature = "hickory")] #[cfg(feature = "hickory")]
dns: None, dns,
}) })
} }
} }

View file

@ -15,8 +15,6 @@ use tokio_rustls::rustls::{
pub struct CustomCertVerifier { pub struct CustomCertVerifier {
pub(crate) provider: Arc<rustls::crypto::CryptoProvider>, pub(crate) provider: Arc<rustls::crypto::CryptoProvider>,
pub(crate) webpki_verifier: Option<Arc<rustls::client::WebPkiServerVerifier>>,
pub(crate) ss_allowed: bool,
pub(crate) ss_verifier: Box<dyn SelfsignedCertVerifier>, pub(crate) ss_verifier: Box<dyn SelfsignedCertVerifier>,
} }
@ -29,50 +27,18 @@ impl ServerCertVerifier for CustomCertVerifier {
_ocsp_response: &[u8], _ocsp_response: &[u8],
now: UnixTime, now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> { ) -> Result<ServerCertVerified, rustls::Error> {
// if webpki CA certs enabled // TODO: certificate validation (domain, expiry, etc.)
#[cfg(feature = "webpki")]
if let Some(wv) = &self.webpki_verifier { if self
match wv.verify_server_cert( .ss_verifier
end_entity, .verify(end_entity, server_name.to_str().as_ref(), now)?
_intermediates, {
server_name, Ok(ServerCertVerified::assertion())
_ocsp_response, } else {
now, Err(rustls::Error::InvalidCertificate(
) { rustls::CertificateError::ApplicationVerificationFailure,
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( fn verify_tls12_signature(
@ -112,10 +78,6 @@ impl ServerCertVerifier for CustomCertVerifier {
impl std::fmt::Debug for CustomCertVerifier { impl std::fmt::Debug for CustomCertVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(f, "CustomCertVerifier {{ provider: {:?} }}", self.provider)
f,
"CustomCertVerifier {{ provider: {:?}, webpki_verifier: {:?} }}",
self.provider, self.webpki_verifier
)
} }
} }

View file

@ -12,9 +12,6 @@ use crate::dns::DnsClient;
use tokio_rustls::rustls::{self, client::danger::ServerCertVerifier, SupportedProtocolVersion}; use tokio_rustls::rustls::{self, client::danger::ServerCertVerifier, SupportedProtocolVersion};
#[cfg(feature = "webpki")]
use tokio_rustls::rustls::{client::WebPkiServerVerifier, pki_types::TrustAnchor};
/// Builder for creating configured [`Client`] instance /// Builder for creating configured [`Client`] instance
pub struct ClientBuilder { pub struct ClientBuilder {
root_certs: rustls::RootCertStore, root_certs: rustls::RootCertStore,
@ -63,33 +60,10 @@ impl ClientBuilder {
let tls_config = if let Some(cv) = self.custom_verifier { let tls_config = if let Some(cv) = self.custom_verifier {
tls_config.dangerous().with_custom_certificate_verifier(cv) tls_config.dangerous().with_custom_certificate_verifier(cv)
} else if let Some(ssv) = self.ss_verifier { } else if let Some(ssv) = self.ss_verifier {
let webpki_verifier = {
#[cfg(feature = "webpki")]
if !self.root_certs.is_empty() {
Some(
WebPkiServerVerifier::builder_with_provider(
Arc::new(self.root_certs),
provider.clone(),
)
.build()
// panics only if roots are empty (that is checked above)
// or CRLs couldn't be parsed (we didn't provide any)
.unwrap(),
)
} else {
None
}
#[cfg(not(feature = "webpki"))]
None
};
tls_config tls_config
.dangerous() .dangerous()
.with_custom_certificate_verifier(Arc::new(CustomCertVerifier { .with_custom_certificate_verifier(Arc::new(CustomCertVerifier {
provider: provider.clone(), provider: provider.clone(),
webpki_verifier,
ss_allowed: true,
ss_verifier: ssv, ss_verifier: ssv,
})) }))
} else { } else {
@ -117,27 +91,6 @@ impl ClientBuilder {
self self
} }
/// Include webpki trust anchors.
/// Not recommended (useless) as most Gemini capsules use self-signed
/// TLS certs and properly configured TOFU policy is enough.
#[cfg(feature = "webpki")]
pub fn with_webpki_roots(mut self) -> Self {
self.root_certs
.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
self
}
/// Include custom trust anchors.
/// Not recommended (useless), see note for [`ClientBuilder::with_webpki_roots`].
#[cfg(feature = "webpki")]
pub fn with_custom_roots(
mut self,
iter: impl IntoIterator<Item = TrustAnchor<'static>>,
) -> Self {
self.root_certs.extend(iter);
self
}
/// Include a self-signed cert verifier. /// Include a self-signed cert verifier.
/// If you only need a known_hosts file, consider using /// If you only need a known_hosts file, consider using
/// [`crate::certs::file_sscv::FileBasedCertVerifier`], /// [`crate::certs::file_sscv::FileBasedCertVerifier`],

View file

@ -7,16 +7,14 @@ pub use response::Response;
#[cfg(feature = "hickory")] #[cfg(feature = "hickory")]
use crate::dns::DnsClient; use crate::dns::DnsClient;
#[cfg(feature = "hickory")] #[cfg(feature = "hickory")]
use hickory_client::rr::IntoName; use hickory_client::rr::IntoName;
#[cfg(feature = "hickory")]
use std::net::SocketAddr;
use crate::{error::*, status::*}; use crate::{error::*, status::*};
use builder::ClientBuilder; use builder::ClientBuilder;
#[cfg(feature = "hickory")]
use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use tokio::{ use tokio::{
@ -49,6 +47,8 @@ impl From<rustls::ClientConfig> for Client {
#[cfg(feature = "hickory")] #[cfg(feature = "hickory")]
impl From<(rustls::ClientConfig, DnsClient)> for Client { impl From<(rustls::ClientConfig, DnsClient)> for Client {
/// Create a Client from a Rustls config and
/// a DnsClient instance as a custom resolver.
#[inline] #[inline]
fn from(value: (rustls::ClientConfig, DnsClient)) -> Self { fn from(value: (rustls::ClientConfig, DnsClient)) -> Self {
Client { Client {