async_acme/
cache.rs

1/*! Ways to cache account data and certificates.
2
3A default implementation for `AsRef<Path>` (`Sting`, `OsString`, `PathBuf`, ...)
4allows the use of a local directory as cache.
5Note that the files contain private keys.
6*/
7
8use crate::B64_URL_SAFE_NO_PAD;
9use async_trait::async_trait;
10use base64::Engine;
11use std::{
12    io::{Error as IoError, ErrorKind},
13    path::Path,
14};
15
16#[cfg(feature = "use_async_std")]
17use async_std::{
18    fs::{create_dir_all as cdall, read, OpenOptions},
19    io::WriteExt,
20    os::unix::fs::OpenOptionsExt,
21};
22#[cfg(feature = "use_tokio")]
23use tokio::{
24    fs::{create_dir_all, read, OpenOptions},
25    io::AsyncWriteExt,
26};
27
28use crate::crypto::sha256_hasher;
29
30/// Trait to define a custom location/mechanism to cache account data and certificates.
31#[async_trait]
32pub trait AcmeCache {
33    /// The error type returned from the functions on this trait.
34    type Error: CacheError;
35
36    /// Returns the previously written data for `contacts`, if any. This
37    /// function should return `None` instead of erroring if data was not
38    /// previously written for `contacts`.
39    async fn read_account(&self, contacts: &[&str]) -> Result<Option<Vec<u8>>, Self::Error>;
40
41    /// Writes `data` for `contacts`. The data being written is unique for the
42    /// combined list of `contacts`.
43    ///
44    /// # Errors
45    ///
46    /// Returns an error when `data` was unable to be written successfully.
47    async fn write_account(&self, contacts: &[&str], data: &[u8]) -> Result<(), Self::Error>;
48
49    /// Writes a certificate retrieved from `Acme`. The parameters are:
50    ///
51    /// ## Parameters
52    ///
53    /// * `domains`: the list of domains included in the certificate.
54    /// * `directory_url`: the Url of the `Acme` directory that this certificate
55    ///   was issued form.
56    /// * `key_pem`: the private key, encoded in PEM format.
57    /// * `certificate_pem`: the certificate chain, encoded in PEM format.
58    ///
59    /// ## Errors
60    ///
61    /// Returns an error when the certificate was unable to be written
62    /// sucessfully.
63    async fn write_certificate(
64        &self,
65        domains: &[String],
66        directory_url: &str,
67        key_pem: &str,
68        certificate_pem: &str,
69    ) -> Result<(), Self::Error>;
70}
71
72#[async_trait]
73impl<P> AcmeCache for P
74where
75    P: AsRef<Path> + Send + Sync,
76{
77    type Error = IoError;
78
79    async fn read_account(&self, contacts: &[&str]) -> Result<Option<Vec<u8>>, Self::Error> {
80        let file = cached_key_file_name(contacts);
81        let mut path = self.as_ref().to_path_buf();
82        path.push(file);
83        match read(path).await {
84            Ok(content) => Ok(Some(content)),
85            Err(err) => match err.kind() {
86                ErrorKind::NotFound => Ok(None),
87                _ => Err(err),
88            },
89        }
90    }
91
92    async fn write_account(&self, contacts: &[&str], contents: &[u8]) -> Result<(), Self::Error> {
93        let mut path = self.as_ref().to_path_buf();
94        create_dir_all(&path).await?;
95        path.push(cached_key_file_name(contacts));
96        Ok(write(path, contents).await?)
97    }
98
99    async fn write_certificate(
100        &self,
101        domains: &[String],
102        directory_url: &str,
103        key_pem: &str,
104        certificate_pem: &str,
105    ) -> Result<(), Self::Error> {
106        let hash = {
107            let mut ctx = sha256_hasher();
108            for domain in domains {
109                ctx.update(domain.as_ref());
110                ctx.update(&[0])
111            }
112            // cache is specific to a particular ACME API URL
113            ctx.update(directory_url.as_bytes());
114            B64_URL_SAFE_NO_PAD.encode(ctx.finish())
115        };
116        let file = AsRef::<Path>::as_ref(self).join(&format!("cached_cert_{}", hash));
117        let content = format!("{}\n{}", key_pem, certificate_pem);
118        write(&file, &content).await?;
119        Ok(())
120    }
121}
122
123/// An error that can be returned from an [`AcmeCache`].
124pub trait CacheError: std::error::Error + Send + Sync + 'static {}
125
126impl<T> CacheError for T where T: std::error::Error + Send + Sync + 'static {}
127
128#[cfg(feature = "use_async_std")]
129async fn create_dir_all(a: impl AsRef<Path>) -> Result<(), IoError> {
130    let p = a.as_ref();
131    let p = <&async_std::path::Path>::from(p);
132    cdall(p).await
133}
134
135#[cfg(not(any(feature = "use_tokio", feature = "use_async_std")))]
136async fn create_dir_all(_a: impl AsRef<Path>) -> Result<(), IoError> {
137    Err(IoError::new(
138        ErrorKind::NotFound,
139        "no async backend selected",
140    ))
141}
142#[cfg(not(any(feature = "use_tokio", feature = "use_async_std")))]
143async fn read(_a: impl AsRef<Path>) -> Result<Vec<u8>, IoError> {
144    Err(IoError::new(
145        ErrorKind::NotFound,
146        "no async backend selected",
147    ))
148}
149#[cfg(not(any(feature = "use_tokio", feature = "use_async_std")))]
150async fn write(_a: impl AsRef<Path>, _c: impl AsRef<[u8]>) -> Result<(), IoError> {
151    Err(IoError::new(
152        ErrorKind::NotFound,
153        "no async backend selected",
154    ))
155}
156#[cfg(any(feature = "use_tokio", feature = "use_async_std"))]
157async fn write(file_path: impl AsRef<Path>, content: impl AsRef<[u8]>) -> Result<(), IoError> {
158    let mut file = OpenOptions::new();
159    file.write(true).create(true).truncate(true);
160    #[cfg(unix)]
161    file.mode(0o600); //user: R+W
162    let mut buffer = file.open(file_path.as_ref()).await?;
163    buffer.write_all(content.as_ref()).await?;
164    Ok(())
165}
166
167fn cached_key_file_name(contact: &[&str]) -> String {
168    let mut ctx = sha256_hasher();
169    for el in contact {
170        ctx.update(el.as_ref());
171        ctx.update(&[0])
172    }
173    let hash = B64_URL_SAFE_NO_PAD.encode(ctx.finish());
174    format!("cached_account_{}", hash)
175}