soth-mitm 0.3.0

Rust intercepting proxy crate with deterministic handler/event contracts for SOTH.
Documentation
impl MitmCertificateStore {
    pub fn new(config: CertificateAuthorityConfig) -> Result<Self, TlsConfigError> {
        config.validate()?;
        let ca = load_or_generate_ca_material(&config)?;
        let state = CertStoreState {
            ca,
            leaf_cache: HashMap::new(),
            cache_lru: VecDeque::new(),
            ca_created_at: SystemTime::now(),
        };
        Ok(Self {
            config,
            state: Mutex::new(state),
            cache_hits: AtomicU64::new(0),
            cache_misses: AtomicU64::new(0),
            leaves_issued: AtomicU64::new(0),
            ca_rotations: AtomicU64::new(0),
        })
    }

    pub fn server_config_for_host(&self, host: &str) -> Result<IssuedServerConfig, TlsConfigError> {
        self.server_config_for_host_with_http2(host, false)
    }

    pub fn server_config_for_host_with_http2(
        &self,
        host: &str,
        http2_enabled: bool,
    ) -> Result<IssuedServerConfig, TlsConfigError> {
        let normalized_host = normalize_host(host);
        let cache_key = format!(
            "{}|h2={}",
            normalized_host,
            if http2_enabled { 1 } else { 0 }
        );
        let mut state = self.state.lock();
        self.maybe_rotate_locked(&mut state)?;

        if let Some((server_config, leaf_cert_der, leaf_identity)) =
            state.leaf_cache.get(&cache_key).map(|cached| {
                (
                    Arc::clone(&cached.server_config),
                    cached.leaf_cert_der.clone(),
                    cached.leaf_identity.clone(),
                )
            })
        {
            touch_lru(&mut state.cache_lru, &cache_key);
            self.cache_hits.fetch_add(1, Ordering::Relaxed);
            return Ok(IssuedServerConfig {
                server_config,
                cache_status: LeafCacheStatus::Hit,
                leaf_cert_der,
                leaf_identity,
            });
        }

        self.cache_misses.fetch_add(1, Ordering::Relaxed);
        let (server_config, leaf_cert_der, leaf_identity) =
            issue_leaf_server_config(
                &state.ca,
                &normalized_host,
                http2_enabled,
                self.config.downstream_cert_profile,
            )?;
        self.leaves_issued.fetch_add(1, Ordering::Relaxed);

        if self.config.leaf_cert_cache_capacity > 0 {
            if state.leaf_cache.len() >= self.config.leaf_cert_cache_capacity {
                evict_lru_entry(&mut state);
            }
            state.leaf_cache.insert(
                cache_key.clone(),
                CachedLeaf {
                    server_config: Arc::clone(&server_config),
                    leaf_cert_der: leaf_cert_der.clone(),
                    leaf_identity: leaf_identity.clone(),
                },
            );
            touch_lru(&mut state.cache_lru, &cache_key);
        }

        Ok(IssuedServerConfig {
            server_config,
            cache_status: LeafCacheStatus::Miss,
            leaf_cert_der,
            leaf_identity,
        })
    }

    pub fn force_rotate(&self) -> Result<(), TlsConfigError> {
        let mut state = self.state.lock();
        self.rotate_locked(&mut state)
    }

    pub fn metrics_snapshot(&self) -> CertStoreMetricsSnapshot {
        CertStoreMetricsSnapshot {
            cache_hits: self.cache_hits.load(Ordering::Relaxed),
            cache_misses: self.cache_misses.load(Ordering::Relaxed),
            leaves_issued: self.leaves_issued.load(Ordering::Relaxed),
            ca_rotations: self.ca_rotations.load(Ordering::Relaxed),
        }
    }

    pub fn ca_certificate_pem(&self) -> Result<String, TlsConfigError> {
        let state = self.state.lock();
        Ok(state.ca.cert_pem.clone())
    }

    fn maybe_rotate_locked(&self, state: &mut CertStoreState) -> Result<(), TlsConfigError> {
        let Some(rotate_after_seconds) = self.config.ca_rotate_after_seconds else {
            return Ok(());
        };

        let elapsed = state
            .ca_created_at
            .elapsed()
            .unwrap_or_else(|_| std::time::Duration::from_secs(0));
        if elapsed.as_secs() >= rotate_after_seconds {
            self.rotate_locked(state)?;
        }
        Ok(())
    }

    fn rotate_locked(&self, state: &mut CertStoreState) -> Result<(), TlsConfigError> {
        let next_ca = generate_ca_material(&self.config)?;
        persist_ca_material_if_configured(&self.config, &next_ca)?;

        state.ca = next_ca;
        state.ca_created_at = SystemTime::now();
        state.leaf_cache.clear();
        state.cache_lru.clear();
        self.ca_rotations.fetch_add(1, Ordering::Relaxed);
        Ok(())
    }
}

fn load_or_generate_ca_material(
    config: &CertificateAuthorityConfig,
) -> Result<CaMaterial, TlsConfigError> {
    match (&config.ca_cert_pem_path, &config.ca_key_pem_path) {
        (Some(ca_cert_path), Some(ca_key_path)) => {
            let cert_exists = Path::new(ca_cert_path).exists();
            let key_exists = Path::new(ca_key_path).exists();

            match (cert_exists, key_exists) {
                (true, true) => load_ca_material(ca_cert_path, ca_key_path, config),
                (false, false) => {
                    let generated = generate_ca_material(config)?;
                    persist_ca_material(ca_cert_path, ca_key_path, &generated)?;
                    Ok(generated)
                }
                _ => Err(TlsConfigError::InvalidConfiguration(
                    "CA cert and key files must both exist or both be absent".to_string(),
                )),
            }
        }
        (None, None) => generate_ca_material(config),
        _ => Err(TlsConfigError::InvalidConfiguration(
            "ca_cert_pem_path and ca_key_pem_path must be set together".to_string(),
        )),
    }
}

fn generate_ca_material(config: &CertificateAuthorityConfig) -> Result<CaMaterial, TlsConfigError> {
    let ca_key = KeyPair::generate()?;
    let ca_key_pem = ca_key.serialize_pem();
    let ca_params = build_ca_params(config);
    let ca_cert = ca_params.self_signed(&ca_key)?;
    let cert_pem = ca_cert.pem();
    let cert_der = ca_cert.der().clone();
    let issuer = Issuer::new(ca_params, ca_key);

    Ok(CaMaterial {
        issuer,
        cert_pem,
        cert_der,
        key_pem: ca_key_pem,
    })
}

fn load_ca_material(
    ca_cert_path: &str,
    ca_key_path: &str,
    _config: &CertificateAuthorityConfig,
) -> Result<CaMaterial, TlsConfigError> {
    let cert_pem = fs::read_to_string(ca_cert_path)?;
    let key_pem = fs::read_to_string(ca_key_path)?;
    certificate_store_openssl::validate_ca_material_with_openssl(ca_cert_path, &cert_pem, &key_pem)?;
    let cert_der = CertificateDer::from_pem_slice(cert_pem.as_bytes()).map_err(|error| {
        TlsConfigError::InvalidConfiguration(format!(
            "failed to parse CA certificate PEM from {ca_cert_path}: {error}"
        ))
    })?;
    let ca_key = KeyPair::from_pem(&key_pem)?;
    let issuer = Issuer::from_ca_cert_der(&cert_der, ca_key).map_err(|error| {
        TlsConfigError::InvalidConfiguration(format!(
            "failed to parse issuer metadata from CA certificate {ca_cert_path}: {error}"
        ))
    })?;

    Ok(CaMaterial {
        issuer,
        cert_pem,
        cert_der,
        key_pem,
    })
}
fn persist_ca_material_if_configured(
    config: &CertificateAuthorityConfig,
    ca: &CaMaterial,
) -> Result<(), TlsConfigError> {
    if let (Some(ca_cert_path), Some(ca_key_path)) =
        (&config.ca_cert_pem_path, &config.ca_key_pem_path)
    {
        persist_ca_material(ca_cert_path, ca_key_path, ca)?;
    }
    Ok(())
}

fn persist_ca_material(
    ca_cert_path: &str,
    ca_key_path: &str,
    ca: &CaMaterial,
) -> Result<(), TlsConfigError> {
    ensure_parent_exists(ca_cert_path)?;
    ensure_parent_exists(ca_key_path)?;

    fs::write(ca_cert_path, ca.cert_pem.as_bytes())?;
    write_key_file_restricted(ca_key_path, ca.key_pem.as_bytes())?;
    Ok(())
}

fn write_key_file_restricted(path: &str, key_pem: &[u8]) -> Result<(), TlsConfigError> {
    let path = std::path::Path::new(path);
    #[cfg(unix)]
    {
        use std::io::Write;
        use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
        let mut file = fs::OpenOptions::new()
            .create(true)
            .truncate(true)
            .write(true)
            .mode(0o600)
            .open(path)?;
        file.write_all(key_pem)?;
        file.flush()?;
        fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
        Ok(())
    }
    #[cfg(not(unix))]
    {
        fs::write(path, key_pem)?;
        Ok(())
    }
}

fn ensure_parent_exists(path: &str) -> Result<(), TlsConfigError> {
    if let Some(parent) = Path::new(path).parent() {
        if !parent.as_os_str().is_empty() {
            fs::create_dir_all(parent)?;
        }
    }
    Ok(())
}

fn issue_leaf_server_config(
    ca: &CaMaterial,
    host: &str,
    http2_enabled: bool,
    downstream_cert_profile: DownstreamCertProfile,
) -> Result<
    (
        Arc<ServerConfig>,
        CertificateDer<'static>,
        IssuedLeafIdentity,
    ),
    TlsConfigError,
> {
    let leaf_params = build_leaf_params(host)?;
    let leaf_key = generate_leaf_key_pair(downstream_cert_profile)?;
    let leaf_key_der = PrivatePkcs8KeyDer::from(leaf_key.serialize_der());
    let leaf_cert = leaf_params.signed_by(&leaf_key, &ca.issuer)?;
    let leaf_cert_der = leaf_cert.der().clone();
    let leaf_cert_pem = leaf_cert.pem();
    let leaf_key_pem = leaf_key.serialize_pem();

    let chain = vec![leaf_cert_der.clone(), ca.cert_der.clone()];
    let private_key = PrivateKeyDer::from(leaf_key_der);

    let mut server_config = ServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(chain, private_key)?;
    server_config.alpn_protocols = configured_http_alpn_protocols(http2_enabled);

    Ok((
        Arc::new(server_config),
        leaf_cert_der,
        IssuedLeafIdentity {
            leaf_cert_pem,
            leaf_key_pem,
            ca_cert_pem: ca.cert_pem.clone(),
        },
    ))
}

fn build_ca_params(config: &CertificateAuthorityConfig) -> CertificateParams {
    let mut params = CertificateParams::default();
    params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
    params.use_authority_key_identifier_extension = true;
    params.key_usages = vec![
        KeyUsagePurpose::DigitalSignature,
        KeyUsagePurpose::KeyCertSign,
        KeyUsagePurpose::CrlSign,
    ];

    let mut distinguished_name = DistinguishedName::new();
    distinguished_name.push(DnType::CommonName, config.ca_common_name.clone());
    distinguished_name.push(DnType::OrganizationName, config.ca_organization.clone());
    params.distinguished_name = distinguished_name;
    params
}

fn build_leaf_params(host: &str) -> Result<CertificateParams, TlsConfigError> {
    let mut params = CertificateParams::new(Vec::<String>::new())?;
    params.use_authority_key_identifier_extension = true;
    params.is_ca = IsCa::NoCa;
    params.key_usages = vec![
        KeyUsagePurpose::DigitalSignature,
        KeyUsagePurpose::KeyEncipherment,
    ];
    params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];

    let mut distinguished_name = DistinguishedName::new();
    distinguished_name.push(DnType::CommonName, host.to_string());
    params.distinguished_name = distinguished_name;

    if let Ok(ip) = host.parse::<IpAddr>() {
        params.subject_alt_names.push(SanType::IpAddress(ip));
    } else {
        params
            .subject_alt_names
            .push(SanType::DnsName(host.try_into()?));
    }

    Ok(params)
}

fn generate_leaf_key_pair(
    downstream_cert_profile: DownstreamCertProfile,
) -> Result<KeyPair, TlsConfigError> {
    match downstream_cert_profile {
        DownstreamCertProfile::Modern => KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
            .or_else(|_| KeyPair::generate())
            .map_err(Into::into),
        DownstreamCertProfile::Compat => KeyPair::generate_for(&rcgen::PKCS_RSA_SHA256)
            .or_else(|_| KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256))
            .or_else(|_| KeyPair::generate())
            .map_err(Into::into),
    }
}

fn normalize_host(host: &str) -> String {
    match host.parse::<IpAddr>() {
        Ok(_) => host.to_string(),
        Err(_) => host.to_ascii_lowercase(),
    }
}

fn touch_lru(lru: &mut VecDeque<String>, key: &str) {
    if let Some(position) = lru.iter().position(|entry| entry == key) {
        lru.remove(position);
    }
    lru.push_back(key.to_string());
}

fn evict_lru_entry(state: &mut CertStoreState) {
    if let Some(oldest) = state.cache_lru.pop_front() {
        state.leaf_cache.remove(&oldest);
    }
}