small_acme/
types.rs

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