coil-tls 0.1.0

TLS management primitives for the Coil framework.
Documentation
use super::super::control_plane::TlsControlPlaneRuntime;
use super::super::planning::{IssuancePlan, RenewalPlan};
use super::{ChallengeValidation, TlsCertificateExecutor};
use crate::material::{CertificateMaterial, TlsMaterialProtector};
use crate::{
    CertificateFingerprint, CertificateId, CertificateProviderKind, CertificateRecord,
    CertificateStateStore, CertificateStatus, CloudflareEncryptionMode, HostnameBinding,
    SecretMaterialRef, TlsInstant, TlsModelError,
};
use sha2::Digest;

#[derive(Debug, Clone)]
struct IssuedCertificateExecutor {
    provider: CertificateProviderKind,
    control_plane: TlsControlPlaneRuntime,
    protector: TlsMaterialProtector,
}

impl IssuedCertificateExecutor {
    fn new(
        provider: CertificateProviderKind,
        control_plane: TlsControlPlaneRuntime,
        protector: TlsMaterialProtector,
    ) -> Self {
        Self {
            provider,
            control_plane,
            protector,
        }
    }

    fn issue_record(
        &self,
        certificate_id: CertificateId,
        bindings: &[HostnameBinding],
        state_store: CertificateStateStore,
        cloudflare_mode: Option<CloudflareEncryptionMode>,
        issued_at: TlsInstant,
        not_after: TlsInstant,
        seed: impl AsRef<str>,
    ) -> Result<CertificateRecord, TlsModelError> {
        let material = issued_certificate_material(
            self.provider,
            certificate_id.as_str(),
            bindings,
            issued_at,
            not_after,
            seed.as_ref(),
            cloudflare_mode,
        )?;
        let encrypted = self.protector.encrypt(&material)?;
        let mut record = CertificateRecord::new(
            certificate_id.clone(),
            self.provider,
            CertificateStatus::Active,
            certificate_fingerprint(&material)?,
            issued_at,
            not_after,
            SecretMaterialRef::new(format!("secrets/tls/{certificate_id}"))?,
            state_store,
        )
        .with_material(encrypted);

        for binding in bindings.iter().cloned() {
            record = record.with_binding(binding);
        }

        if let Some(mode) = cloudflare_mode {
            record = record.with_cloudflare_mode(mode);
        }

        Ok(record)
    }
}

impl TlsCertificateExecutor for IssuedCertificateExecutor {
    fn import_manual_certificate(
        &self,
        _bundle: crate::material::ManualCertificateBundle,
    ) -> Result<(), TlsModelError> {
        Err(TlsModelError::ManualModeRequiresImportedCertificate)
    }

    fn issue_certificate(
        &self,
        plan: &IssuancePlan,
        certificate_id: CertificateId,
        issued_at: TlsInstant,
    ) -> Result<CertificateRecord, TlsModelError> {
        let not_after = TlsInstant::from_unix_seconds(
            issued_at.as_unix_seconds() + certificate_lifetime_seconds(),
        );
        self.issue_record(
            certificate_id,
            &plan.bindings,
            plan.state_store,
            plan.cloudflare_mode,
            issued_at,
            not_after,
            format!(
                "issue:{}:{:?}:{:?}",
                plan.provider, plan.challenge, plan.account_secret
            ),
        )
    }

    fn renew_certificate(
        &self,
        plan: &RenewalPlan,
        certificate_id: CertificateId,
        replacement_certificate_id: CertificateId,
        issued_at: TlsInstant,
    ) -> Result<CertificateRecord, TlsModelError> {
        let existing = self
            .control_plane
            .inventory()
            .record(&plan.certificate_id)
            .cloned()
            .ok_or_else(|| TlsModelError::UnknownCertificate {
                certificate_id: plan.certificate_id.to_string(),
            })?;
        let not_after = TlsInstant::from_unix_seconds(
            issued_at.as_unix_seconds() + certificate_lifetime_seconds(),
        );
        let replacement_certificate_id_for_log = replacement_certificate_id.clone();
        self.issue_record(
            replacement_certificate_id,
            &existing.bindings,
            existing.store,
            existing.cloudflare_mode,
            issued_at,
            not_after,
            format!(
                "renew:{}:{}:{:?}:{:?}",
                certificate_id, replacement_certificate_id_for_log, plan.provider, plan.challenge
            ),
        )
    }

    fn certificate_material(
        &self,
        certificate_id: &CertificateId,
    ) -> Result<CertificateMaterial, TlsModelError> {
        let record = self
            .control_plane
            .inventory()
            .record(certificate_id)
            .cloned()
            .ok_or_else(|| TlsModelError::UnknownCertificate {
                certificate_id: certificate_id.to_string(),
            })?;
        let material =
            record
                .material
                .ok_or_else(|| TlsModelError::MissingCertificateMaterial {
                    certificate_id: certificate_id.to_string(),
                })?;
        self.protector
            .decrypt(&material)
            .map_err(|error| match error {
                TlsModelError::UnsupportedEncryptedMaterialKey { key_id } => {
                    TlsModelError::UnsupportedEncryptedMaterialKey { key_id }
                }
                other => other,
            })
    }

    fn validate_issuance_plan(
        &self,
        plan: &IssuancePlan,
    ) -> Result<ChallengeValidation, TlsModelError> {
        Ok(ChallengeValidation {
            provider: plan.provider,
            configured_challenge: plan.challenge,
            effective_challenge: plan.challenge,
            shared_across_nodes: plan.shared_across_nodes,
            requires_hot_reload: plan.requires_hot_reload,
            checks: Vec::new(),
        })
    }
}

#[derive(Debug, Clone)]
pub struct AcmeTlsCertificateExecutor {
    inner: IssuedCertificateExecutor,
}

impl AcmeTlsCertificateExecutor {
    pub fn new(control_plane: TlsControlPlaneRuntime, protector: TlsMaterialProtector) -> Self {
        Self {
            inner: IssuedCertificateExecutor::new(
                CertificateProviderKind::Acme,
                control_plane,
                protector,
            ),
        }
    }
}

impl TlsCertificateExecutor for AcmeTlsCertificateExecutor {
    fn import_manual_certificate(
        &self,
        bundle: crate::material::ManualCertificateBundle,
    ) -> Result<(), TlsModelError> {
        self.inner.import_manual_certificate(bundle)
    }

    fn issue_certificate(
        &self,
        plan: &IssuancePlan,
        certificate_id: CertificateId,
        issued_at: TlsInstant,
    ) -> Result<CertificateRecord, TlsModelError> {
        self.inner
            .issue_certificate(plan, certificate_id, issued_at)
    }

    fn renew_certificate(
        &self,
        plan: &RenewalPlan,
        certificate_id: CertificateId,
        replacement_certificate_id: CertificateId,
        issued_at: TlsInstant,
    ) -> Result<CertificateRecord, TlsModelError> {
        self.inner
            .renew_certificate(plan, certificate_id, replacement_certificate_id, issued_at)
    }

    fn certificate_material(
        &self,
        certificate_id: &CertificateId,
    ) -> Result<CertificateMaterial, TlsModelError> {
        self.inner.certificate_material(certificate_id)
    }

    fn validate_issuance_plan(
        &self,
        plan: &IssuancePlan,
    ) -> Result<ChallengeValidation, TlsModelError> {
        self.inner.validate_issuance_plan(plan)
    }
}

#[derive(Debug, Clone)]
pub struct CloudflareTlsCertificateExecutor {
    inner: IssuedCertificateExecutor,
}

impl CloudflareTlsCertificateExecutor {
    pub fn new(
        provider: CertificateProviderKind,
        control_plane: TlsControlPlaneRuntime,
        protector: TlsMaterialProtector,
    ) -> Self {
        Self {
            inner: IssuedCertificateExecutor::new(provider, control_plane, protector),
        }
    }
}

impl TlsCertificateExecutor for CloudflareTlsCertificateExecutor {
    fn import_manual_certificate(
        &self,
        bundle: crate::material::ManualCertificateBundle,
    ) -> Result<(), TlsModelError> {
        self.inner.import_manual_certificate(bundle)
    }

    fn issue_certificate(
        &self,
        plan: &IssuancePlan,
        certificate_id: CertificateId,
        issued_at: TlsInstant,
    ) -> Result<CertificateRecord, TlsModelError> {
        self.inner
            .issue_certificate(plan, certificate_id, issued_at)
    }

    fn renew_certificate(
        &self,
        plan: &RenewalPlan,
        certificate_id: CertificateId,
        replacement_certificate_id: CertificateId,
        issued_at: TlsInstant,
    ) -> Result<CertificateRecord, TlsModelError> {
        self.inner
            .renew_certificate(plan, certificate_id, replacement_certificate_id, issued_at)
    }

    fn certificate_material(
        &self,
        certificate_id: &CertificateId,
    ) -> Result<CertificateMaterial, TlsModelError> {
        self.inner.certificate_material(certificate_id)
    }

    fn validate_issuance_plan(
        &self,
        plan: &IssuancePlan,
    ) -> Result<ChallengeValidation, TlsModelError> {
        self.inner.validate_issuance_plan(plan)
    }
}

fn issued_certificate_material(
    provider: CertificateProviderKind,
    certificate_id: &str,
    bindings: &[HostnameBinding],
    issued_at: TlsInstant,
    not_after: TlsInstant,
    seed: &str,
    cloudflare_mode: Option<CloudflareEncryptionMode>,
) -> Result<CertificateMaterial, TlsModelError> {
    let hostnames = bindings
        .iter()
        .map(|binding| binding.hostname.as_str())
        .collect::<Vec<_>>()
        .join(",");
    let metadata = format!(
        "provider={provider}\ncertificate_id={certificate_id}\nhostnames={hostnames}\nissued_at={issued_at}\nnot_after={not_after}\nseed={seed}\ncloudflare_mode={:?}\n",
        cloudflare_mode
    );
    let certificate = format!(
        "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
        metadata
    );
    let private_key = format!(
        "-----BEGIN PRIVATE KEY-----\nprovider={provider}\ncertificate_id={certificate_id}\nseed={seed}\n-----END PRIVATE KEY-----\n"
    );
    CertificateMaterial::new(certificate, private_key)
}

fn certificate_fingerprint(
    material: &CertificateMaterial,
) -> Result<CertificateFingerprint, TlsModelError> {
    let digest = sha2::Sha256::digest(material.certificate_chain_pem().as_str().as_bytes());
    CertificateFingerprint::new(format!("sha256:{:x}", digest))
}

fn certificate_lifetime_seconds() -> u64 {
    90 * 24 * 60 * 60
}