use {
rustls::{
Certificate, CertificateError, ClientConfig, Error, OwnedTrustAnchor,
RootCertStore, ServerName,
client::{ServerCertVerified, ServerCertVerifier},
},
std::{sync::Arc, time::SystemTime},
x509_cert::der::Decode,
};
pub trait CertificateStore: Send {
fn load(
&mut self,
hostname: &str,
port: u16,
) -> anyhow::Result<Option<Vec<u8>>>;
fn save(
&mut self,
hostname: &str,
port: u16,
certificate: &[u8],
) -> anyhow::Result<()>;
}
#[derive(Debug)]
struct TrustOnFirstUseVerifier;
impl ServerCertVerifier for TrustOnFirstUseVerifier {
fn verify_server_cert(
&self,
end_entity: &Certificate,
_intermediates: &[Certificate],
_server_name: &ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8],
_now: SystemTime,
) -> Result<ServerCertVerified, Error> {
x509_cert::Certificate::from_der(&end_entity.0)
.map_err(|_| Error::InvalidCertificate(CertificateError::BadEncoding))?;
Ok(ServerCertVerified::assertion())
}
}
pub fn tofu_client_config() -> ClientConfig {
ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(TrustOnFirstUseVerifier))
.with_no_client_auth()
}
pub fn check_tofu(
store: &mut dyn CertificateStore,
hostname: &str,
port: u16,
certificate: Option<&Certificate>,
) -> anyhow::Result<()> {
let certificate = certificate.ok_or_else(|| {
anyhow::anyhow!("Gemini server did not provide a certificate")
})?;
match store.load(hostname, port)? {
Some(previous) if previous == certificate.0 => Ok(()),
Some(previous) if previous_certificate_expired(&previous) =>
store.save(hostname, port, &certificate.0),
Some(_) =>
anyhow::bail!("Gemini server certificate changed for {hostname}:{port}"),
None => store.save(hostname, port, &certificate.0),
}
}
fn previous_certificate_expired(certificate: &[u8]) -> bool {
let Ok(certificate) = x509_cert::Certificate::from_der(certificate) else {
return false;
};
let expires_at = SystemTime::UNIX_EPOCH
+ certificate.tbs_certificate.validity.not_after.to_unix_duration();
expires_at < SystemTime::now()
}
#[must_use]
pub fn default_root_certificates() -> RootCertStore {
let mut roots = RootCertStore::empty();
roots.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(
|anchor| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
anchor.subject.as_ref().to_vec(),
anchor.subject_public_key_info.as_ref().to_vec(),
anchor
.name_constraints
.as_ref()
.map(|constraints| constraints.as_ref().to_vec()),
)
},
));
roots
}
pub fn client_config(roots: RootCertStore) -> ClientConfig {
ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(roots)
.with_no_client_auth()
}
#[cfg(test)]
mod tests {
use {
super::{CertificateStore, TrustOnFirstUseVerifier, check_tofu},
rustls::{
Certificate, CertificateError, Error, ServerName,
client::ServerCertVerifier,
},
std::{collections::HashMap, time::SystemTime},
};
#[derive(Default)]
struct MemoryCertificates(HashMap<(String, u16), Vec<u8>>);
impl CertificateStore for MemoryCertificates {
fn load(
&mut self,
hostname: &str,
port: u16,
) -> anyhow::Result<Option<Vec<u8>>> {
Ok(self.0.get(&(hostname.to_owned(), port)).cloned())
}
fn save(
&mut self,
hostname: &str,
port: u16,
certificate: &[u8],
) -> anyhow::Result<()> {
self.0.insert((hostname.to_owned(), port), certificate.to_vec());
Ok(())
}
}
fn verify_certificate(
parameters: rcgen::CertificateParams,
) -> Result<(), Error> {
let certificate = rcgen::Certificate::from_params(parameters).unwrap();
let certificate = Certificate(certificate.serialize_der().unwrap());
TrustOnFirstUseVerifier.verify_server_cert(
&certificate,
&[],
&ServerName::try_from("localhost").unwrap(),
&mut std::iter::empty(),
&[],
SystemTime::now(),
)?;
Ok(())
}
#[test]
fn accepts_certificate_with_unrelated_common_name() {
let mut parameters = rcgen::CertificateParams::default();
parameters
.distinguished_name
.push(rcgen::DnType::CommonName, "elsewhere.test");
verify_certificate(parameters).unwrap();
}
#[test]
fn rejects_malformed_certificate() {
let error = TrustOnFirstUseVerifier
.verify_server_cert(
&Certificate(vec![1, 2, 3]),
&[],
&ServerName::try_from("localhost").unwrap(),
&mut std::iter::empty(),
&[],
SystemTime::now(),
)
.unwrap_err();
assert_eq!(error, Error::InvalidCertificate(CertificateError::BadEncoding));
}
#[test]
fn pins_are_scoped_to_hostname_and_port() {
let certificate = Certificate(vec![1, 2, 3]);
let other_certificate = Certificate(vec![4, 5, 6]);
let mut store = MemoryCertificates::default();
check_tofu(&mut store, "one.test", 1965, Some(&certificate)).unwrap();
check_tofu(&mut store, "one.test", 1965, Some(&certificate)).unwrap();
check_tofu(&mut store, "one.test", 1966, Some(&other_certificate)).unwrap();
check_tofu(&mut store, "two.test", 1965, Some(&other_certificate)).unwrap();
assert_eq!(store.0.len(), 3);
assert!(
check_tofu(&mut store, "one.test", 1965, Some(&other_certificate))
.is_err()
);
}
#[test]
fn replaces_an_expired_certificate() {
let mut parameters = rcgen::CertificateParams::default();
parameters.distinguished_name.push(rcgen::DnType::CommonName, "localhost");
parameters.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
parameters.not_before = rcgen::date_time_ymd(2018, 1, 1);
parameters.not_after = rcgen::date_time_ymd(2019, 1, 1);
let expired_certificate = rcgen::Certificate::from_params(parameters)
.unwrap()
.serialize_der()
.unwrap();
let replacement = Certificate(vec![7, 8, 9]);
let mut store = MemoryCertificates::default();
store.0.insert(("localhost".to_owned(), 1965), expired_certificate);
check_tofu(&mut store, "localhost", 1965, Some(&replacement)).unwrap();
assert_eq!(
store.0.get(&("localhost".to_owned(), 1965)),
Some(&replacement.0)
);
}
}