Skip to main content

acme/
types.rs

1use std::fmt;
2
3use crypto::{Hasher, p256::PublicKey, sha2::Sha256};
4use reqwest::Response;
5// use rustls_pki_types::CertificateDer;
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9// use ureq::Response;
10
11/// Error type for instant-acme
12#[derive(Debug, Error)]
13pub enum Error {
14    /// An JSON problem as returned by the ACME server
15    ///
16    /// RFC 8555 uses problem documents as described in RFC 7807.
17    #[error(transparent)]
18    Api(#[from] Problem),
19    /// Failed to base64-decode data
20    #[error("base64 decoding failed: {0}")]
21    Base64(#[from] base64::DecodeError),
22    /// Failed from cryptographic operations
23    #[error("cryptographic operation failed")]
24    Crypto,
25    /// Failed to instantiate a private key
26    #[error("invalid key bytes")]
27    CryptoKey,
28    /// HTTP request failure
29    #[error("HTTP request failure: {0}")]
30    Http(#[from] reqwest::Error),
31    /// HTTP IO failure
32    #[error("HTTP IO failure: {0}")]
33    HttpIo(#[from] std::io::Error),
34    /// Failed to (de)serialize a JSON object
35    #[error("failed to (de)serialize JSON: {0}")]
36    Json(#[from] serde_json::Error),
37    /// Miscellaneous errors
38    #[error("missing data: {0}")]
39    Str(&'static str),
40}
41
42impl From<&'static str> for Error {
43    fn from(s: &'static str) -> Self {
44        Error::Str(s)
45    }
46}
47
48/// ACME account credentials
49///
50/// This opaque type contains the account ID, the private key data and the
51/// server URLs from the relevant ACME server. This can be used to serialize
52/// the account credentials to a file or secret manager and restore the
53/// account from persistent storage.
54#[derive(Deserialize, Serialize, Clone)]
55pub struct AccountCredentials {
56    pub(crate) id: String,
57    /// Stored in DER, serialized as base64
58    #[serde(with = "pkcs8_serde")]
59    pub(crate) key_pkcs8: Vec<u8>,
60    pub(crate) directory: Option<String>,
61    pub(crate) urls: Option<DirectoryUrls>,
62}
63
64mod pkcs8_serde {
65    use std::fmt;
66
67    use serde::{Deserializer, Serializer, de};
68
69    pub(crate) fn serialize<S>(key_pkcs8: &[u8], serializer: S) -> Result<S::Ok, S::Error>
70    where
71        S: Serializer,
72    {
73        let encoded = base64::encode(key_pkcs8.as_ref(), base64::Alphabet::UrlNoPadding);
74        serializer.serialize_str(&encoded)
75    }
76
77    pub(crate) fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
78        struct Visitor;
79
80        impl<'de> de::Visitor<'de> for Visitor {
81            type Value = Vec<u8>;
82
83            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84                formatter.write_str("a base64-encoded PKCS#8 private key")
85            }
86
87            fn visit_str<E>(self, v: &str) -> Result<Vec<u8>, E>
88            where
89                E: de::Error,
90            {
91                base64::decode(v, base64::Alphabet::UrlNoPadding).map_err(de::Error::custom)
92            }
93        }
94
95        deserializer.deserialize_str(Visitor)
96    }
97}
98
99/// An RFC 7807 problem document as returned by the ACME server
100#[derive(Clone, Debug, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct Problem {
103    /// One of an enumerated list of problem types
104    ///
105    /// See <https://datatracker.ietf.org/doc/html/rfc8555#section-6.7>
106    pub r#type: Option<String>,
107    /// A human-readable explanation of the problem
108    pub detail: Option<String>,
109    /// The HTTP status code returned for this response
110    pub status: Option<u16>,
111}
112
113impl Problem {
114    pub(crate) async fn check<T: DeserializeOwned>(rsp: Response) -> Result<T, Error> {
115        rsp.json().await.map_err(Error::Http)
116    }
117
118    pub(crate) async fn from_response(rsp: Response) -> Result<Vec<u8>, Error> {
119        let status = rsp.status();
120        let body = rsp.bytes().await?;
121
122        if (100..=399).contains(&status.as_u16()) {
123            return Ok(body.to_vec());
124        }
125
126        Err(serde_json::from_slice::<Problem>(&body)?.into())
127    }
128}
129
130impl fmt::Display for Problem {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        f.write_str("API error")?;
133        if let Some(detail) = &self.detail {
134            write!(f, ": {detail}")?;
135        }
136
137        if let Some(r#type) = &self.r#type {
138            write!(f, " ({})", r#type)?;
139        }
140
141        Ok(())
142    }
143}
144
145impl std::error::Error for Problem {}
146
147#[derive(Debug, Serialize)]
148pub(crate) struct FinalizeRequest {
149    csr: String,
150}
151
152impl FinalizeRequest {
153    pub(crate) fn new(csr_der: &[u8]) -> Self {
154        Self {
155            csr: base64::encode(csr_der, base64::Alphabet::UrlNoPadding),
156        }
157    }
158}
159
160#[derive(Debug, Serialize)]
161pub(crate) struct Header<'a> {
162    pub(crate) alg: SigningAlgorithm,
163    #[serde(flatten)]
164    pub(crate) key: KeyOrKeyId<'a>,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub(crate) nonce: Option<&'a str>,
167    pub(crate) url: &'a str,
168}
169
170#[derive(Debug, Serialize)]
171pub(crate) enum KeyOrKeyId<'a> {
172    #[serde(rename = "jwk")]
173    Key(Jwk),
174    #[serde(rename = "kid")]
175    KeyId(&'a str),
176}
177
178impl<'a> KeyOrKeyId<'a> {
179    pub(crate) fn from_key(key: &PublicKey) -> KeyOrKeyId<'static> {
180        KeyOrKeyId::Key(Jwk::new(key))
181    }
182}
183
184#[derive(Debug, Serialize)]
185pub(crate) struct Jwk {
186    alg: SigningAlgorithm,
187    crv: &'static str,
188    kty: &'static str,
189    r#use: &'static str,
190    x: String,
191    y: String,
192}
193
194impl Jwk {
195    pub(crate) fn new(key: &PublicKey) -> Self {
196        let bytes = key.to_bytes();
197        let (x, y) = bytes[1..].split_at(32);
198        Self {
199            alg: SigningAlgorithm::Es256,
200            crv: "P-256",
201            kty: "EC",
202            r#use: "sig",
203            x: base64::encode(x, base64::Alphabet::UrlNoPadding),
204            y: base64::encode(y, base64::Alphabet::UrlNoPadding),
205        }
206    }
207
208    pub(crate) fn thumb_sha256(key: &PublicKey) -> Result<[u8; 32], serde_json::Error> {
209        let jwk = Self::new(key);
210        let hash = Sha256::hash(&serde_json::to_vec(&JwkThumb {
211            crv: jwk.crv,
212            kty: jwk.kty,
213            x: &jwk.x,
214            y: &jwk.y,
215        })?);
216        let mut out = [0u8; 32];
217        out.copy_from_slice(&hash.as_ref()[..32]);
218        Ok(out)
219    }
220}
221
222#[derive(Debug, Serialize)]
223struct JwkThumb<'a> {
224    crv: &'a str,
225    kty: &'a str,
226    x: &'a str,
227    y: &'a str,
228}
229
230/// An ACME challenge as described in RFC 8555 (section 7.1.5)
231///
232/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.5>
233#[derive(Debug, Deserialize)]
234pub struct Challenge {
235    /// Type of challenge
236    pub r#type: ChallengeType,
237    /// Challenge identifier
238    pub url: String,
239    /// Token for this challenge
240    pub token: String,
241    /// Current status
242    pub status: ChallengeStatus,
243    /// Potential error state
244    pub error: Option<Problem>,
245}
246
247/// Contents of an ACME order as described in RFC 8555 (section 7.1.3)
248///
249/// The order identity will usually be represented by an [Order](crate::Order).
250///
251/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3>
252#[derive(Debug, Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct OrderState {
255    /// Current status
256    pub status: OrderStatus,
257    /// Authorization URLs for this order
258    ///
259    /// There should be one authorization per identifier in the order.
260    pub authorizations: Vec<String>,
261    /// Potential error state
262    pub error: Option<Problem>,
263    /// A finalization URL, to be used once status becomes `Ready`
264    pub finalize: String,
265    /// The certificate URL, which becomes available after finalization
266    pub certificate: Option<String>,
267}
268
269/// Input data for [Order](crate::Order) creation
270///
271/// To be passed into [Account::new_order()](crate::Account::new_order()).
272#[derive(Debug, Serialize)]
273#[serde(rename_all = "camelCase")]
274pub struct NewOrder<'a> {
275    /// Identifiers to be included in the order
276    pub identifiers: &'a [Identifier],
277}
278
279// /// Payload for a certificate revocation request
280// /// Defined in <https://datatracker.ietf.org/doc/html/rfc8555#section-7.6>
281// #[derive(Debug)]
282// pub struct RevocationRequest<'a> {
283//     /// The certificate to revoke
284//     pub certificate: &'a CertificateDer<'a>,
285//     /// Reason for revocation
286//     pub reason: Option<RevocationReason>,
287// }
288
289// impl<'a> Serialize for RevocationRequest<'a> {
290//     fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
291//         let base64 = BASE64_URL_SAFE_NO_PAD.encode(self.certificate);
292//         let mut map = serializer.serialize_map(Some(2))?;
293//         map.serialize_entry("certificate", &base64)?;
294//         if let Some(reason) = &self.reason {
295//             map.serialize_entry("reason", reason)?;
296//         }
297//         map.end()
298//     }
299// }
300
301/// The reason for a certificate revocation
302/// Defined in <https://datatracker.ietf.org/doc/html/rfc5280#section-5.3.1>
303#[allow(missing_docs)]
304#[derive(Debug, Clone)]
305#[repr(u8)]
306pub enum RevocationReason {
307    Unspecified = 0,
308    KeyCompromise = 1,
309    CaCompromise = 2,
310    AffiliationChanged = 3,
311    Superseded = 4,
312    CessationOfOperation = 5,
313    CertificateHold = 6,
314    RemoveFromCrl = 8,
315    PrivilegeWithdrawn = 9,
316    AaCompromise = 10,
317}
318
319impl Serialize for RevocationReason {
320    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
321        serializer.serialize_u8(self.clone() as u8)
322    }
323}
324
325#[derive(Serialize)]
326#[serde(rename_all = "camelCase")]
327pub(crate) struct NewAccountPayload<'a> {
328    #[serde(flatten)]
329    pub(crate) new_account: &'a NewAccount<'a>,
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub(crate) external_account_binding: Option<JoseJson>,
332}
333
334/// Input data for [Account](crate::Account) creation
335///
336/// To be passed into [Account::create()](crate::Account::create()).
337#[derive(Debug, Serialize)]
338#[serde(rename_all = "camelCase")]
339pub struct NewAccount<'a> {
340    /// A list of contact URIs (like `mailto:info@example.com`)
341    pub contact: &'a [&'a str],
342    /// Whether you agree to the terms of service
343    pub terms_of_service_agreed: bool,
344    /// Set to `true` in order to retrieve an existing account
345    ///
346    /// Setting this to `false` has not been tested.
347    pub only_return_existing: bool,
348}
349
350#[derive(Debug, Clone, Deserialize, Serialize)]
351#[serde(rename_all = "camelCase")]
352pub(crate) struct DirectoryUrls {
353    pub(crate) new_nonce: String,
354    pub(crate) new_account: String,
355    pub(crate) new_order: String,
356    pub(crate) revoke_cert: String,
357}
358
359#[derive(Serialize)]
360pub(crate) struct JoseJson {
361    pub(crate) protected: String,
362    pub(crate) payload: String,
363    pub(crate) signature: String,
364}
365
366impl JoseJson {
367    pub(crate) fn new(
368        payload: Option<&impl Serialize>,
369        protected: Header<'_>,
370        signer: &impl Signer,
371    ) -> Result<Self, Error> {
372        let protected = base64(&protected)?;
373        let payload = match payload {
374            Some(data) => base64(&data)?,
375            None => String::new(),
376        };
377
378        let combined = format!("{protected}.{payload}");
379        let signature = signer.sign(combined.as_bytes())?;
380        Ok(Self {
381            protected,
382            payload,
383            signature: base64::encode(signature.as_ref(), base64::Alphabet::UrlNoPadding),
384        })
385    }
386}
387
388pub(crate) trait Signer {
389    type Signature: AsRef<[u8]>;
390
391    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n>;
392
393    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error>;
394}
395
396fn base64(data: &impl Serialize) -> Result<String, serde_json::Error> {
397    let json_data = serde_json::to_vec(data)?;
398    let encoded_json_data = base64::encode(&json_data, base64::Alphabet::UrlNoPadding);
399    return Ok(encoded_json_data);
400}
401
402/// An ACME authorization as described in RFC 8555 (section 7.1.4)
403#[derive(Debug, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct Authorization {
406    /// The identifier that the account is authorized to represent
407    pub identifier: Identifier,
408    /// Current state of the authorization
409    pub status: AuthorizationStatus,
410    /// Possible challenges for the authorization
411    pub challenges: Vec<Challenge>,
412}
413
414/// Status for an [`Authorization`]
415#[allow(missing_docs)]
416#[derive(Clone, Copy, Debug, Deserialize)]
417#[serde(rename_all = "camelCase")]
418pub enum AuthorizationStatus {
419    Pending,
420    Valid,
421    Invalid,
422    Revoked,
423    Expired,
424}
425
426/// Represent an identifier in an ACME [Order](crate::Order)
427#[allow(missing_docs)]
428#[derive(Clone, Debug, Serialize, Deserialize)]
429#[serde(tag = "type", content = "value", rename_all = "camelCase")]
430pub enum Identifier {
431    Dns(String),
432}
433
434/// The challenge type
435#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
436#[allow(missing_docs)]
437pub enum ChallengeType {
438    #[serde(rename = "http-01")]
439    Http01,
440    #[serde(rename = "dns-01")]
441    Dns01,
442    #[serde(rename = "tls-alpn-01")]
443    TlsAlpn01,
444}
445
446#[derive(Clone, Copy, Debug, Deserialize)]
447#[serde(rename_all = "camelCase")]
448pub enum ChallengeStatus {
449    Pending,
450    Processing,
451    Valid,
452    Invalid,
453}
454
455/// Status of an [Order](crate::Order)
456#[allow(missing_docs)]
457#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
458#[serde(rename_all = "camelCase")]
459pub enum OrderStatus {
460    Pending,
461    Ready,
462    Processing,
463    Valid,
464    Invalid,
465}
466
467/// Helper type to reference Let's Encrypt server URLs
468#[allow(missing_docs)]
469#[derive(Clone, Copy, Debug)]
470pub enum LetsEncrypt {
471    Production,
472    Staging,
473}
474
475impl LetsEncrypt {
476    /// Get the directory URL for the given Let's Encrypt server
477    pub const fn url(&self) -> &'static str {
478        match self {
479            Self::Production => "https://acme-v02.api.letsencrypt.org/directory",
480            Self::Staging => "https://acme-staging-v02.api.letsencrypt.org/directory",
481        }
482    }
483}
484
485/// ZeroSSL ACME only supports production at the moment
486#[allow(missing_docs)]
487#[derive(Clone, Copy, Debug)]
488pub enum ZeroSsl {
489    Production,
490}
491
492impl ZeroSsl {
493    /// Get the directory URL for the given ZeroSSL server
494    pub const fn url(&self) -> &'static str {
495        match self {
496            Self::Production => "https://acme.zerossl.com/v2/DV90",
497        }
498    }
499}
500
501#[derive(Clone, Copy, Debug, Serialize)]
502#[serde(rename_all = "UPPERCASE")]
503pub(crate) enum SigningAlgorithm {
504    /// ECDSA using P-256 and SHA-256
505    Es256,
506    /// HMAC with SHA-256,
507    Hs256,
508}
509
510#[derive(Debug, Serialize)]
511pub(crate) struct Empty {}