acmer 0.0.18

ACME TLS certificates management library
Documentation
use std::path::{Path, PathBuf};

use async_trait::async_trait;
use pem_rfc7468 as pem;
use rustls_pki_types::pem::PemObject;
use tokio::{fs, io, try_join};

use super::{AccountStore, CertStore, Certificate, PrivateKey};

struct FileStore {
    directory: PathBuf,
}

impl FileStore {
    pub fn new(dir: impl AsRef<Path>) -> FileStore {
        Self {
            directory: dir.as_ref().into(),
        }
    }
}

#[async_trait]
impl CertStore for FileStore {
    async fn get_cert(&self, domain: &str) -> io::Result<Option<(PrivateKey, Vec<Certificate>)>> {
        let key = self.directory.join(format!("{domain}.key"));
        let cert = self.directory.join(format!("{domain}.crt"));

        let (key, cert) = try_join!(fs::read(key), fs::read(cert))?;

        let key = PrivateKey::try_from(key).map_err(|err| io::Error::other(err.to_string()))?;

        let cert = Certificate::pem_slice_iter(&cert)
            .map(|cert| cert.map_err(|err| io::Error::other(err.to_string())))
            .collect::<io::Result<Vec<Certificate>>>()?;

        Ok(Some((key, cert)))
    }

    async fn put_cert(
        &self,
        domain: &str,
        key: PrivateKey,
        cert: Vec<Certificate>,
    ) -> io::Result<()> {
        let key_path = self.directory.join(format!("{domain}.key"));
        let cert_path = self.directory.join(format!("{domain}.crt"));

        let cert = cert
            .into_iter()
            .map(|cert| pem::encode_string("CERTIFICATE", pem::LineEnding::default(), &cert))
            .collect::<Result<String, _>>()
            .map_err(|err| io::Error::other(err.to_string()))?;

        try_join!(
            fs::write(key_path, key.secret_der()),
            fs::write(cert_path, cert)
        )?;
        Ok(())
    }
}

#[async_trait]
impl AccountStore for FileStore {
    async fn get_account(&self, directory: &str) -> io::Result<Option<PrivateKey>> {
        let path = self.directory.join(format!("account.{directory}.key"));
        let key = fs::read(path).await?;

        let key = pem::decode_vec(&key)
            .ok()
            .and_then(|(label, key)| match label {
                "PRIVATE KEY" => Some(key),
                _ => None,
            })
            .and_then(|key| PrivateKey::try_from(key).ok());

        Ok(key)
    }

    async fn put_account(&self, directory: &str, key: PrivateKey) -> io::Result<()> {
        let path = self.directory.join(format!("account.{directory}.key"));
        let key = pem::encode_string("PRIVATE KEY", pem::LineEnding::default(), key.secret_der())
            .map_err(|err| io::Error::other(err.to_string()))?;
        fs::write(path, key).await?;
        Ok(())
    }
}

pub struct CertFileStore(FileStore);

impl CertFileStore {
    pub fn new(dir: impl AsRef<Path>) -> Self {
        Self(FileStore::new(dir))
    }
}

#[async_trait]
impl CertStore for CertFileStore {
    async fn get_cert(&self, domain: &str) -> io::Result<Option<(PrivateKey, Vec<Certificate>)>> {
        self.0.get_cert(domain).await
    }

    async fn put_cert(
        &self,
        domain: &str,
        key: PrivateKey,
        cert: Vec<Certificate>,
    ) -> io::Result<()> {
        self.0.put_cert(domain, key, cert).await
    }
}

pub struct AccountFileStore(FileStore);

impl AccountFileStore {
    pub fn new(dir: impl AsRef<Path>) -> Self {
        Self(FileStore::new(dir))
    }
}

#[async_trait]
impl AccountStore for AccountFileStore {
    async fn get_account(&self, directory: &str) -> io::Result<Option<PrivateKey>> {
        self.0.get_account(directory).await
    }

    async fn put_account(&self, directory: &str, key: PrivateKey) -> io::Result<()> {
        self.0.put_account(directory, key).await
    }
}

#[cfg(test)]
mod test {
    use std::env::temp_dir;

    use super::*;

    #[tokio::test]
    async fn test_fs_account_store() {
        let store = AccountFileStore::new(temp_dir());
        let key = papaleguas::PrivateKey::random_ec_key(rand::thread_rng())
            .to_der()
            .map(|key| PrivateKey::try_from(key).unwrap())
            .unwrap();
        store.put_account("123", key.clone_key()).await.unwrap();
        assert_eq!(store.get_account("123").await.unwrap(), Some(key));
    }
}