use std::sync::{Arc, Mutex, OnceLock};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
use rustls::{DigitallySignedStruct, SignatureScheme};
use tokio_rustls::TlsConnector;
use crate::tofu;
pub(crate) type SeenCell = Arc<Mutex<Option<[u8; 32]>>>;
pub(crate) fn pinning_connector(pinned: Option<[u8; 32]>) -> (TlsConnector, SeenCell) {
let seen: SeenCell = Arc::new(Mutex::new(None));
let provider = Arc::new(rustls::crypto::ring::default_provider());
let config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.expect("ring provides the default protocol versions")
.dangerous()
.with_custom_certificate_verifier(Arc::new(PinningVerifier {
pinned,
seen: Arc::clone(&seen),
}))
.with_no_client_auth();
(TlsConnector::from(Arc::new(config)), seen)
}
pub(crate) fn connector() -> TlsConnector {
static CONFIG: OnceLock<Arc<rustls::ClientConfig>> = OnceLock::new();
let config = CONFIG.get_or_init(|| {
let provider = Arc::new(rustls::crypto::ring::default_provider());
let config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.expect("ring provides the default protocol versions")
.dangerous()
.with_custom_certificate_verifier(Arc::new(AcceptAny))
.with_no_client_auth();
Arc::new(config)
});
TlsConnector::from(config.clone())
}
pub(crate) fn client_connector(
cert_chain: Vec<CertificateDer<'static>>,
key: PrivateKeyDer<'static>,
) -> Result<TlsConnector, rustls::Error> {
let provider = Arc::new(rustls::crypto::ring::default_provider());
let config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.expect("ring provides the default protocol versions")
.dangerous()
.with_custom_certificate_verifier(Arc::new(AcceptAny))
.with_client_auth_cert(cert_chain, key)?;
Ok(TlsConnector::from(Arc::new(config)))
}
const ACCEPTED_SCHEMES: [SignatureScheme; 10] = [
SignatureScheme::RSA_PKCS1_SHA256,
SignatureScheme::RSA_PKCS1_SHA384,
SignatureScheme::RSA_PKCS1_SHA512,
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::ECDSA_NISTP384_SHA384,
SignatureScheme::ECDSA_NISTP521_SHA512,
SignatureScheme::RSA_PSS_SHA256,
SignatureScheme::RSA_PSS_SHA384,
SignatureScheme::RSA_PSS_SHA512,
SignatureScheme::ED25519,
];
#[derive(Debug)]
struct PinningVerifier {
pinned: Option<[u8; 32]>,
seen: SeenCell,
}
impl ServerCertVerifier for PinningVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
let fingerprint = tofu::fingerprint(end_entity.as_ref());
*self.seen.lock().unwrap() = Some(fingerprint);
match self.pinned {
None => Ok(ServerCertVerified::assertion()),
Some(pinned) if pinned == fingerprint => Ok(ServerCertVerified::assertion()),
Some(_) => Err(rustls::Error::General(
"gemini TOFU: certificate fingerprint changed".into(),
)),
}
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
ACCEPTED_SCHEMES.to_vec()
}
}
#[derive(Debug)]
struct AcceptAny;
impl ServerCertVerifier for AcceptAny {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
ACCEPTED_SCHEMES.to_vec()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn verify(verifier: &PinningVerifier, cert: &[u8]) -> Result<(), rustls::Error> {
let der = CertificateDer::from(cert.to_vec());
let name = ServerName::try_from("x.test").unwrap();
verifier
.verify_server_cert(&der, &[], &name, &[], UnixTime::now())
.map(|_| ())
}
#[test]
fn first_contact_accepts_and_records() {
let seen: SeenCell = Arc::new(Mutex::new(None));
let verifier = PinningVerifier {
pinned: None,
seen: Arc::clone(&seen),
};
assert!(verify(&verifier, b"leaf").is_ok());
assert_eq!(*seen.lock().unwrap(), Some(tofu::fingerprint(b"leaf")));
}
#[test]
fn a_matching_pin_accepts() {
let verifier = PinningVerifier {
pinned: Some(tofu::fingerprint(b"leaf")),
seen: Arc::new(Mutex::new(None)),
};
assert!(verify(&verifier, b"leaf").is_ok());
}
#[test]
fn a_changed_cert_is_rejected_but_still_recorded() {
let seen: SeenCell = Arc::new(Mutex::new(None));
let verifier = PinningVerifier {
pinned: Some(tofu::fingerprint(b"old")),
seen: Arc::clone(&seen),
};
assert!(verify(&verifier, b"new").is_err());
assert_eq!(*seen.lock().unwrap(), Some(tofu::fingerprint(b"new")));
}
}