use std::path::Path;
use std::sync::Arc;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use sha2::{Digest, Sha256};
use tokio_rustls::TlsConnector;
use crate::error::{Result, SyncwebError};
pub const RELAY_ALPN: &[u8] = super::protocol::PROTOCOL_NAME;
const CERT_FILE: &str = "relay-cert.der";
const KEY_FILE: &str = "relay-key.der";
#[derive(Debug)]
pub struct RelayCerts {
cert_der: CertificateDer<'static>,
key_der: PrivateKeyDer<'static>,
relay_id: [u8; 32],
}
impl RelayCerts {
pub(crate) fn load_or_create(data_dir: &Path) -> Result<Self> {
std::fs::create_dir_all(data_dir)?;
let cert_path = data_dir.join(CERT_FILE);
let key_path = data_dir.join(KEY_FILE);
if let (Ok(cert), Ok(key)) = (std::fs::read(&cert_path), std::fs::read(&key_path)) {
return Self::from_der(cert, key);
}
let generated = Self::generate()?;
std::fs::write(&cert_path, generated.cert_der().as_ref())?;
std::fs::write(&key_path, generated.key_der_for_storage())?;
Ok(generated)
}
fn from_der(cert_bytes: Vec<u8>, key_bytes: Vec<u8>) -> Result<Self> {
let key_der = PrivateKeyDer::try_from(key_bytes)
.map_err(|error| SyncwebError::RelayDecode(format!("stored relay key is invalid: {error}")))?;
let relay_id = relay_id_from_cert(&cert_bytes);
Ok(Self {
cert_der: CertificateDer::from(cert_bytes),
key_der,
relay_id,
})
}
fn generate() -> Result<Self> {
use rcgen::{CertificateParams, KeyPair, PKCS_ECDSA_P256_SHA256};
let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)
.map_err(|error| SyncwebError::RelayDecode(format!("relay key generation failed: {error}")))?;
let mut params =
CertificateParams::new(Vec::new()).map_err(|error| SyncwebError::RelayDecode(error.to_string()))?;
params.distinguished_name.push(rcgen::DnType::CommonName, "syncweb");
params.is_ca = rcgen::IsCa::NoCa;
params.key_usages = vec![rcgen::KeyUsagePurpose::DigitalSignature];
let certificate = params
.self_signed(&key_pair)
.map_err(|error| SyncwebError::RelayDecode(format!("relay certificate signing failed: {error}")))?;
let cert_der = certificate.der().as_ref().to_vec();
let key_der = key_pair.serialize_der();
Self::from_der(cert_der, key_der)
}
#[must_use]
pub const fn relay_id(&self) -> &[u8; 32] {
&self.relay_id
}
#[must_use]
pub const fn cert_der(&self) -> &CertificateDer<'static> {
&self.cert_der
}
#[must_use]
pub const fn key_der(&self) -> &PrivateKeyDer<'static> {
&self.key_der
}
fn key_der_for_storage(&self) -> &[u8] {
self.key_der.secret_der()
}
}
fn relay_id_from_cert(cert_der: &[u8]) -> [u8; 32] {
Sha256::digest(cert_der).into()
}
pub fn server_name_for(host: &str) -> Result<ServerName<'static>> {
if let Ok(address) = host.parse::<std::net::IpAddr>() {
return Ok(ServerName::IpAddress(address.into()));
}
ServerName::try_from(host.to_owned()).map_err(|error| SyncwebError::RelayUnreachable {
reasons: format!("{host} is not a usable relay host: {error}"),
})
}
pub fn tls_connector(certs: &RelayCerts) -> Result<TlsConnector> {
ensure_provider_installed();
let tls_error = |error: rustls::Error| SyncwebError::RelayUnreachable {
reasons: format!("relay TLS setup failed: {error}"),
};
let mut config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])
.map_err(tls_error)?
.dangerous()
.with_custom_certificate_verifier(Arc::new(RelayServerVerifier))
.with_client_auth_cert(vec![certs.cert_der().clone()], certs.key_der().clone_key())
.map_err(tls_error)?;
config.alpn_protocols = vec![RELAY_ALPN.to_vec()];
Ok(TlsConnector::from(Arc::new(config)))
}
pub fn ensure_provider_installed() {
if rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
}
#[derive(Debug)]
struct RelayServerVerifier;
impl rustls::client::danger::ServerCertVerifier for RelayServerVerifier {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls12_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
rustls::crypto::ring::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_round_trips_on_disk() {
let dir = std::env::temp_dir().join(format!("syncweb-relay-cert-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let first = RelayCerts::load_or_create(&dir).expect("create");
let second = RelayCerts::load_or_create(&dir).expect("reload");
assert_eq!(first.relay_id(), second.relay_id());
assert_eq!(first.cert_der(), second.cert_der());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn connector_builds_for_generated_and_reloaded_certs() {
let dir = std::env::temp_dir().join(format!("syncweb-relay-cert-{}-2", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let certs = RelayCerts::load_or_create(&dir).expect("create");
tls_connector(&certs).expect("connector");
let reloaded = RelayCerts::load_or_create(&dir).expect("reload");
tls_connector(&reloaded).expect("connector for reloaded");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn server_name_from_host() {
let ip = server_name_for("127.0.0.1").expect("ip");
assert!(matches!(ip, ServerName::IpAddress(_)));
let dns = server_name_for("relay.example.com").expect("dns");
assert!(matches!(dns, ServerName::DnsName(_)));
assert!(server_name_for("not a host").is_err());
}
#[test]
fn relay_id_is_sha256_of_der() {
let certs = RelayCerts::load_or_create(&std::env::temp_dir()).expect("create");
let expected: [u8; 32] = Sha256::digest(certs.cert_der().as_ref()).into();
assert_eq!(certs.relay_id(), &expected);
}
}