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