tokio_rustls_acme/
acme.rs

1use std::sync::Arc;
2
3use crate::https_helper::{https, HttpsRequestError, Method, Response};
4use crate::jose::{key_authorization_sha256, sign, JoseError};
5use base64::engine::general_purpose::URL_SAFE_NO_PAD;
6use base64::Engine;
7use rcgen::{CustomExtension, Error as RcgenError, PKCS_ECDSA_P256_SHA256};
8use ring::error::{KeyRejected, Unspecified};
9use ring::rand::SystemRandom;
10use ring::signature::{EcdsaKeyPair, EcdsaSigningAlgorithm, ECDSA_P256_SHA256_FIXED_SIGNING};
11use rustls::{crypto::ring::sign::any_ecdsa_type, sign::CertifiedKey};
12use rustls::{
13    pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer},
14    ClientConfig,
15};
16use serde::{Deserialize, Serialize};
17use serde_json::json;
18use thiserror::Error;
19
20pub const LETS_ENCRYPT_STAGING_DIRECTORY: &str =
21    "https://acme-staging-v02.api.letsencrypt.org/directory";
22pub const LETS_ENCRYPT_PRODUCTION_DIRECTORY: &str =
23    "https://acme-v02.api.letsencrypt.org/directory";
24pub const ACME_TLS_ALPN_NAME: &[u8] = b"acme-tls/1";
25
26#[derive(Debug)]
27pub struct Account {
28    pub key_pair: EcdsaKeyPair,
29    pub directory: Directory,
30    pub kid: String,
31}
32
33static ALG: &EcdsaSigningAlgorithm = &ECDSA_P256_SHA256_FIXED_SIGNING;
34
35impl Account {
36    pub fn generate_key_pair() -> Vec<u8> {
37        let rng = SystemRandom::new();
38        let pkcs8 = EcdsaKeyPair::generate_pkcs8(ALG, &rng).unwrap();
39        pkcs8.as_ref().to_vec()
40    }
41    pub async fn create<'a, S, I>(
42        client_config: &Arc<ClientConfig>,
43        directory: Directory,
44        contact: I,
45    ) -> Result<Self, AcmeError>
46    where
47        S: AsRef<str> + 'a,
48        I: IntoIterator<Item = &'a S>,
49    {
50        let key_pair = Self::generate_key_pair();
51        Self::create_with_keypair(client_config, directory, contact, &key_pair).await
52    }
53    pub async fn create_with_keypair<'a, S, I>(
54        client_config: &Arc<ClientConfig>,
55        directory: Directory,
56        contact: I,
57        key_pair: &[u8],
58    ) -> Result<Self, AcmeError>
59    where
60        S: AsRef<str> + 'a,
61        I: IntoIterator<Item = &'a S>,
62    {
63        let key_pair = EcdsaKeyPair::from_pkcs8(ALG, key_pair, &SystemRandom::new())?;
64        let contact: Vec<&'a str> = contact.into_iter().map(AsRef::<str>::as_ref).collect();
65        let payload = json!({
66            "termsOfServiceAgreed": true,
67            "contact": contact,
68        })
69        .to_string();
70        let body = sign(
71            &key_pair,
72            None,
73            directory.nonce(client_config).await?,
74            &directory.new_account,
75            &payload,
76        )?;
77        let response = https(
78            client_config,
79            &directory.new_account,
80            Method::Post,
81            Some(body),
82        )
83        .await?;
84        let kid = get_header(&response, "Location")?;
85        Ok(Account {
86            key_pair,
87            kid,
88            directory,
89        })
90    }
91    async fn request(
92        &self,
93        client_config: &Arc<ClientConfig>,
94        url: impl AsRef<str>,
95        payload: &str,
96    ) -> Result<(Option<String>, String), AcmeError> {
97        let body = sign(
98            &self.key_pair,
99            Some(&self.kid),
100            self.directory.nonce(client_config).await?,
101            url.as_ref(),
102            payload,
103        )?;
104        let response = https(client_config, url.as_ref(), Method::Post, Some(body)).await?;
105        let location = get_header(&response, "Location").ok();
106        let body = response.text().await.map_err(HttpsRequestError::from)?;
107        log::debug!("response: {:?}", body);
108        Ok((location, body))
109    }
110    pub async fn new_order(
111        &self,
112        client_config: &Arc<ClientConfig>,
113        domains: Vec<String>,
114    ) -> Result<(String, Order), AcmeError> {
115        let domains: Vec<Identifier> = domains.into_iter().map(Identifier::Dns).collect();
116        let payload = format!("{{\"identifiers\":{}}}", serde_json::to_string(&domains)?);
117        let response = self
118            .request(client_config, &self.directory.new_order, &payload)
119            .await?;
120        let url = response.0.ok_or(AcmeError::MissingHeader("Location"))?;
121        let order = serde_json::from_str(&response.1)?;
122        Ok((url, order))
123    }
124    pub async fn auth(
125        &self,
126        client_config: &Arc<ClientConfig>,
127        url: impl AsRef<str>,
128    ) -> Result<Auth, AcmeError> {
129        let payload = "".to_string();
130        let response = self.request(client_config, url, &payload).await?;
131        Ok(serde_json::from_str(&response.1)?)
132    }
133    pub async fn challenge(
134        &self,
135        client_config: &Arc<ClientConfig>,
136        url: impl AsRef<str>,
137    ) -> Result<(), AcmeError> {
138        self.request(client_config, &url, "{}").await?;
139        Ok(())
140    }
141    pub async fn order(
142        &self,
143        client_config: &Arc<ClientConfig>,
144        url: impl AsRef<str>,
145    ) -> Result<Order, AcmeError> {
146        let response = self.request(client_config, &url, "").await?;
147        Ok(serde_json::from_str(&response.1)?)
148    }
149    pub async fn finalize(
150        &self,
151        client_config: &Arc<ClientConfig>,
152        url: impl AsRef<str>,
153        csr: Vec<u8>,
154    ) -> Result<Order, AcmeError> {
155        let payload = format!("{{\"csr\":\"{}\"}}", URL_SAFE_NO_PAD.encode(csr),);
156        let response = self.request(client_config, &url, &payload).await?;
157        Ok(serde_json::from_str(&response.1)?)
158    }
159    pub async fn certificate(
160        &self,
161        client_config: &Arc<ClientConfig>,
162        url: impl AsRef<str>,
163    ) -> Result<String, AcmeError> {
164        Ok(self.request(client_config, &url, "").await?.1)
165    }
166    pub fn tls_alpn_01<'a>(
167        &self,
168        challenges: &'a [Challenge],
169        domain: String,
170    ) -> Result<(&'a Challenge, CertifiedKey), AcmeError> {
171        let challenge = challenges
172            .iter()
173            .find(|c| c.typ == ChallengeType::TlsAlpn01);
174
175        let challenge = match challenge {
176            Some(challenge) => challenge,
177            None => return Err(AcmeError::NoTlsAlpn01Challenge),
178        };
179        let mut params = rcgen::CertificateParams::new(vec![domain])?;
180        let key_auth = key_authorization_sha256(&self.key_pair, &challenge.token)?;
181        params.custom_extensions = vec![CustomExtension::new_acme_identifier(key_auth.as_ref())];
182
183        let key_pair = rcgen::KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
184        let cert = params.self_signed(&key_pair)?;
185
186        let pk_bytes = key_pair.serialize_der();
187        let pk_der: PrivatePkcs8KeyDer = pk_bytes.into();
188        let pk_der: PrivateKeyDer = pk_der.into();
189        let pk = any_ecdsa_type(&pk_der).unwrap();
190        let certified_key = CertifiedKey::new(vec![cert.der().clone()], pk);
191        Ok((challenge, certified_key))
192    }
193}
194
195#[derive(Debug, Clone, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct Directory {
198    pub new_nonce: String,
199    pub new_account: String,
200    pub new_order: String,
201}
202
203impl Directory {
204    pub async fn discover(
205        client_config: &Arc<ClientConfig>,
206        url: impl AsRef<str>,
207    ) -> Result<Self, AcmeError> {
208        let response = https(client_config, url, Method::Get, None).await?;
209        let body = response.bytes().await.map_err(HttpsRequestError::from)?;
210
211        Ok(serde_json::from_slice(&body)?)
212    }
213    pub async fn nonce(&self, client_config: &Arc<ClientConfig>) -> Result<String, AcmeError> {
214        let response = &https(client_config, &self.new_nonce.as_str(), Method::Head, None).await?;
215        get_header(response, "replay-nonce")
216    }
217}
218
219#[derive(Debug, Deserialize, Eq, PartialEq)]
220pub enum ChallengeType {
221    #[serde(rename = "http-01")]
222    Http01,
223    #[serde(rename = "dns-01")]
224    Dns01,
225    #[serde(rename = "tls-alpn-01")]
226    TlsAlpn01,
227}
228
229#[derive(Debug, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct Order {
232    #[serde(flatten)]
233    pub status: OrderStatus,
234    pub authorizations: Vec<String>,
235    pub finalize: String,
236    pub error: Option<Problem>,
237}
238
239#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
240#[serde(tag = "status", rename_all = "camelCase")]
241pub enum OrderStatus {
242    Pending,
243    Ready,
244    Valid { certificate: String },
245    Invalid,
246    Processing,
247}
248
249#[derive(Debug, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct Auth {
252    pub status: AuthStatus,
253    pub identifier: Identifier,
254    pub challenges: Vec<Challenge>,
255}
256
257#[derive(Debug, Deserialize)]
258#[serde(rename_all = "camelCase")]
259pub enum AuthStatus {
260    Pending,
261    Valid,
262    Invalid,
263    Revoked,
264    Expired,
265    Deactivated,
266}
267
268#[derive(Clone, Debug, Serialize, Deserialize)]
269#[serde(tag = "type", content = "value", rename_all = "camelCase")]
270pub enum Identifier {
271    Dns(String),
272}
273
274#[derive(Debug, Deserialize)]
275pub struct Challenge {
276    #[serde(rename = "type")]
277    pub typ: ChallengeType,
278    pub url: String,
279    pub token: String,
280    pub error: Option<Problem>,
281}
282
283#[derive(Clone, Debug, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct Problem {
286    #[serde(rename = "type")]
287    pub typ: Option<String>,
288    pub detail: Option<String>,
289}
290
291#[derive(Error, Debug)]
292pub enum AcmeError {
293    #[error("io error: {0}")]
294    Io(#[from] std::io::Error),
295    #[error("certificate generation error: {0}")]
296    Rcgen(#[from] RcgenError),
297    #[error("JOSE error: {0}")]
298    Jose(#[from] JoseError),
299    #[error("JSON error: {0}")]
300    Json(#[from] serde_json::Error),
301    #[error("http request error: {0}")]
302    HttpRequest(#[from] HttpsRequestError),
303    #[error("invalid key pair: {0}")]
304    KeyRejected(#[from] KeyRejected),
305    #[error("crypto error: {0}")]
306    Crypto(#[from] Unspecified),
307    #[error("acme service response is missing {0} header")]
308    MissingHeader(&'static str),
309    #[error("no tls-alpn-01 challenge found")]
310    NoTlsAlpn01Challenge,
311}
312
313fn get_header(response: &Response, header: &'static str) -> Result<String, AcmeError> {
314    let h = response
315        .headers()
316        .get_all(header)
317        .iter()
318        .last()
319        .and_then(|v| v.to_str().ok())
320        .map(|s| s.to_string());
321    match h {
322        None => Err(AcmeError::MissingHeader(header)),
323        Some(value) => Ok(value),
324    }
325}