instant_acme/
types.rs

1use std::fmt;
2
3use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
4use bytes::Bytes;
5use rustls_pki_types::CertificateDer;
6use serde::de::DeserializeOwned;
7use serde::ser::SerializeMap;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use crate::crypto::{self, KeyPair};
12use crate::BytesResponse;
13
14/// Error type for instant-acme
15#[derive(Debug, Error)]
16pub enum Error {
17    /// An JSON problem as returned by the ACME server
18    ///
19    /// RFC 8555 uses problem documents as described in RFC 7807.
20    #[error(transparent)]
21    Api(#[from] Problem),
22    /// Failed to base64-decode data
23    #[error("base64 decoding failed: {0}")]
24    Base64(#[from] base64::DecodeError),
25    /// Failed from cryptographic operations
26    #[error("cryptographic operation failed: {0}")]
27    Crypto(#[from] crypto::Unspecified),
28    /// Failed to instantiate a private key
29    #[error("invalid key bytes: {0}")]
30    CryptoKey(#[from] crypto::KeyRejected),
31    /// HTTP failure
32    #[error("HTTP request failure: {0}")]
33    Http(#[from] http::Error),
34    /// Hyper request failure
35    #[cfg(feature = "hyper-rustls")]
36    #[error("HTTP request failure: {0}")]
37    Hyper(#[from] hyper::Error),
38    /// Invalid ACME server URL
39    #[error("invalid URI: {0}")]
40    InvalidUri(#[from] http::uri::InvalidUri),
41    /// Failed to (de)serialize a JSON object
42    #[error("failed to (de)serialize JSON: {0}")]
43    Json(#[from] serde_json::Error),
44    /// Other kind of error
45    #[error(transparent)]
46    Other(Box<dyn std::error::Error + Send + Sync + 'static>),
47    /// Miscellaneous errors
48    #[error("missing data: {0}")]
49    Str(&'static str),
50}
51
52impl From<&'static str> for Error {
53    fn from(s: &'static str) -> Self {
54        Error::Str(s)
55    }
56}
57
58#[cfg(feature = "hyper-rustls")]
59impl From<hyper_util::client::legacy::Error> for Error {
60    fn from(value: hyper_util::client::legacy::Error) -> Self {
61        Self::Other(Box::new(value))
62    }
63}
64
65/// ACME account credentials
66///
67/// This opaque type contains the account ID, the private key data and the
68/// server URLs from the relevant ACME server. This can be used to serialize
69/// the account credentials to a file or secret manager and restore the
70/// account from persistent storage.
71#[derive(Deserialize, Serialize)]
72pub struct AccountCredentials {
73    pub(crate) id: String,
74    /// Stored in DER, serialized as base64
75    #[serde(with = "pkcs8_serde")]
76    pub(crate) key_pkcs8: Vec<u8>,
77    pub(crate) directory: Option<String>,
78    /// We never serialize `urls` by default, but we support deserializing them
79    /// in order to support serialized data from older versions of the library.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub(crate) urls: Option<DirectoryUrls>,
82}
83
84mod pkcs8_serde {
85    use std::fmt;
86
87    use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
88    use serde::{de, Deserializer, Serializer};
89
90    pub(crate) fn serialize<S>(key_pkcs8: &[u8], serializer: S) -> Result<S::Ok, S::Error>
91    where
92        S: Serializer,
93    {
94        let encoded = BASE64_URL_SAFE_NO_PAD.encode(key_pkcs8.as_ref());
95        serializer.serialize_str(&encoded)
96    }
97
98    pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
99        deserializer: D,
100    ) -> Result<Vec<u8>, D::Error> {
101        struct Visitor;
102
103        impl<'de> de::Visitor<'de> for Visitor {
104            type Value = Vec<u8>;
105
106            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107                formatter.write_str("a base64-encoded PKCS#8 private key")
108            }
109
110            fn visit_str<E>(self, v: &str) -> Result<Vec<u8>, E>
111            where
112                E: de::Error,
113            {
114                BASE64_URL_SAFE_NO_PAD.decode(v).map_err(de::Error::custom)
115            }
116        }
117
118        deserializer.deserialize_str(Visitor)
119    }
120}
121
122/// An RFC 7807 problem document as returned by the ACME server
123#[derive(Clone, Debug, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct Problem {
126    /// One of an enumerated list of problem types
127    ///
128    /// See <https://datatracker.ietf.org/doc/html/rfc8555#section-6.7>
129    pub r#type: Option<String>,
130    /// A human-readable explanation of the problem
131    pub detail: Option<String>,
132    /// The HTTP status code returned for this response
133    pub status: Option<u16>,
134}
135
136impl Problem {
137    pub(crate) async fn check<T: DeserializeOwned>(rsp: BytesResponse) -> Result<T, Error> {
138        Ok(serde_json::from_slice(&Self::from_response(rsp).await?)?)
139    }
140
141    pub(crate) async fn from_response(rsp: BytesResponse) -> Result<Bytes, Error> {
142        let status = rsp.parts.status;
143        let body = rsp.body().await.map_err(Error::Other)?;
144        match status.is_informational() || status.is_success() || status.is_redirection() {
145            true => Ok(body),
146            false => Err(serde_json::from_slice::<Problem>(&body)?.into()),
147        }
148    }
149}
150
151impl fmt::Display for Problem {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        f.write_str("API error")?;
154        if let Some(detail) = &self.detail {
155            write!(f, ": {detail}")?;
156        }
157
158        if let Some(r#type) = &self.r#type {
159            write!(f, " ({})", r#type)?;
160        }
161
162        Ok(())
163    }
164}
165
166impl std::error::Error for Problem {}
167
168#[derive(Debug, Serialize)]
169pub(crate) struct FinalizeRequest {
170    csr: String,
171}
172
173impl FinalizeRequest {
174    pub(crate) fn new(csr_der: &[u8]) -> Self {
175        Self {
176            csr: BASE64_URL_SAFE_NO_PAD.encode(csr_der),
177        }
178    }
179}
180
181#[derive(Debug, Serialize)]
182pub(crate) struct Header<'a> {
183    pub(crate) alg: SigningAlgorithm,
184    #[serde(flatten)]
185    pub(crate) key: KeyOrKeyId<'a>,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub(crate) nonce: Option<&'a str>,
188    pub(crate) url: &'a str,
189}
190
191#[derive(Debug, Serialize)]
192pub(crate) enum KeyOrKeyId<'a> {
193    #[serde(rename = "jwk")]
194    Key(Jwk),
195    #[serde(rename = "kid")]
196    KeyId(&'a str),
197}
198
199impl<'a> KeyOrKeyId<'a> {
200    pub(crate) fn from_key(key: &crypto::EcdsaKeyPair) -> KeyOrKeyId<'static> {
201        KeyOrKeyId::Key(Jwk::new(key))
202    }
203}
204
205#[derive(Debug, Serialize)]
206pub(crate) struct Jwk {
207    alg: SigningAlgorithm,
208    crv: &'static str,
209    kty: &'static str,
210    r#use: &'static str,
211    x: String,
212    y: String,
213}
214
215impl Jwk {
216    pub(crate) fn new(key: &crypto::EcdsaKeyPair) -> Self {
217        let (x, y) = key.public_key().as_ref()[1..].split_at(32);
218        Self {
219            alg: SigningAlgorithm::Es256,
220            crv: "P-256",
221            kty: "EC",
222            r#use: "sig",
223            x: BASE64_URL_SAFE_NO_PAD.encode(x),
224            y: BASE64_URL_SAFE_NO_PAD.encode(y),
225        }
226    }
227
228    pub(crate) fn thumb_sha256(
229        key: &crypto::EcdsaKeyPair,
230    ) -> Result<crypto::Digest, serde_json::Error> {
231        let jwk = Self::new(key);
232        Ok(crypto::digest(
233            &crypto::SHA256,
234            &serde_json::to_vec(&JwkThumb {
235                crv: jwk.crv,
236                kty: jwk.kty,
237                x: &jwk.x,
238                y: &jwk.y,
239            })?,
240        ))
241    }
242}
243
244#[derive(Debug, Serialize)]
245struct JwkThumb<'a> {
246    crv: &'a str,
247    kty: &'a str,
248    x: &'a str,
249    y: &'a str,
250}
251
252/// An ACME challenge as described in RFC 8555 (section 7.1.5)
253///
254/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.5>
255#[derive(Debug, Deserialize)]
256pub struct Challenge {
257    /// Type of challenge
258    pub r#type: ChallengeType,
259    /// Challenge identifier
260    pub url: String,
261    /// Token for this challenge
262    pub token: String,
263    /// Current status
264    pub status: ChallengeStatus,
265    /// Potential error state
266    pub error: Option<Problem>,
267}
268
269/// Contents of an ACME order as described in RFC 8555 (section 7.1.3)
270///
271/// The order identity will usually be represented by an [Order](crate::Order).
272///
273/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3>
274#[derive(Debug, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct OrderState {
277    /// Current status
278    pub status: OrderStatus,
279    /// Authorization URLs for this order
280    ///
281    /// There should be one authorization per identifier in the order.
282    pub authorizations: Vec<String>,
283    /// Potential error state
284    pub error: Option<Problem>,
285    /// A finalization URL, to be used once status becomes `Ready`
286    pub finalize: String,
287    /// The certificate URL, which becomes available after finalization
288    pub certificate: Option<String>,
289}
290
291/// Input data for [Order](crate::Order) creation
292///
293/// To be passed into [Account::new_order()](crate::Account::new_order()).
294#[derive(Debug, Serialize)]
295#[serde(rename_all = "camelCase")]
296pub struct NewOrder<'a> {
297    /// Identifiers to be included in the order
298    pub identifiers: &'a [Identifier],
299}
300
301/// Payload for a certificate revocation request
302/// Defined in <https://datatracker.ietf.org/doc/html/rfc8555#section-7.6>
303#[derive(Debug)]
304pub struct RevocationRequest<'a> {
305    /// The certificate to revoke
306    pub certificate: &'a CertificateDer<'a>,
307    /// Reason for revocation
308    pub reason: Option<RevocationReason>,
309}
310
311impl<'a> Serialize for RevocationRequest<'a> {
312    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
313        let base64 = BASE64_URL_SAFE_NO_PAD.encode(self.certificate);
314        let mut map = serializer.serialize_map(Some(2))?;
315        map.serialize_entry("certificate", &base64)?;
316        if let Some(reason) = &self.reason {
317            map.serialize_entry("reason", reason)?;
318        }
319        map.end()
320    }
321}
322
323/// The reason for a certificate revocation
324/// Defined in <https://datatracker.ietf.org/doc/html/rfc5280#section-5.3.1>
325#[allow(missing_docs)]
326#[derive(Debug, Clone)]
327#[repr(u8)]
328pub enum RevocationReason {
329    Unspecified = 0,
330    KeyCompromise = 1,
331    CaCompromise = 2,
332    AffiliationChanged = 3,
333    Superseded = 4,
334    CessationOfOperation = 5,
335    CertificateHold = 6,
336    RemoveFromCrl = 8,
337    PrivilegeWithdrawn = 9,
338    AaCompromise = 10,
339}
340
341impl Serialize for RevocationReason {
342    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
343        serializer.serialize_u8(self.clone() as u8)
344    }
345}
346
347#[derive(Serialize)]
348#[serde(rename_all = "camelCase")]
349pub(crate) struct NewAccountPayload<'a> {
350    #[serde(flatten)]
351    pub(crate) new_account: &'a NewAccount<'a>,
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub(crate) external_account_binding: Option<JoseJson>,
354}
355
356/// Input data for [Account](crate::Account) creation
357///
358/// To be passed into [Account::create()](crate::Account::create()).
359#[derive(Debug, Serialize)]
360#[serde(rename_all = "camelCase")]
361pub struct NewAccount<'a> {
362    /// A list of contact URIs (like `mailto:info@example.com`)
363    pub contact: &'a [&'a str],
364    /// Whether you agree to the terms of service
365    pub terms_of_service_agreed: bool,
366    /// Set to `true` in order to retrieve an existing account
367    ///
368    /// Setting this to `false` has not been tested.
369    pub only_return_existing: bool,
370}
371
372#[derive(Debug, Clone, Deserialize, Serialize)]
373#[serde(rename_all = "camelCase")]
374pub(crate) struct DirectoryUrls {
375    pub(crate) new_nonce: String,
376    pub(crate) new_account: String,
377    pub(crate) new_order: String,
378    // The fields below were added later and old `AccountCredentials` may not have it.
379    // Newer deserialized account credentials grab a fresh set of `DirectoryUrls` on
380    // deserialization, so they should be fine. Newer fields should be optional, too.
381    pub(crate) new_authz: Option<String>,
382    pub(crate) revoke_cert: Option<String>,
383    pub(crate) key_change: Option<String>,
384}
385
386#[derive(Serialize)]
387pub(crate) struct JoseJson {
388    pub(crate) protected: String,
389    pub(crate) payload: String,
390    pub(crate) signature: String,
391}
392
393impl JoseJson {
394    pub(crate) fn new(
395        payload: Option<&impl Serialize>,
396        protected: Header<'_>,
397        signer: &impl Signer,
398    ) -> Result<Self, Error> {
399        let protected = base64(&protected)?;
400        let payload = match payload {
401            Some(data) => base64(&data)?,
402            None => String::new(),
403        };
404
405        let combined = format!("{protected}.{payload}");
406        let signature = signer.sign(combined.as_bytes())?;
407        Ok(Self {
408            protected,
409            payload,
410            signature: BASE64_URL_SAFE_NO_PAD.encode(signature.as_ref()),
411        })
412    }
413}
414
415pub(crate) trait Signer {
416    type Signature: AsRef<[u8]>;
417
418    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n>;
419
420    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error>;
421}
422
423fn base64(data: &impl Serialize) -> Result<String, serde_json::Error> {
424    Ok(BASE64_URL_SAFE_NO_PAD.encode(serde_json::to_vec(data)?))
425}
426
427/// An ACME authorization as described in RFC 8555 (section 7.1.4)
428#[derive(Debug, Deserialize)]
429#[serde(rename_all = "camelCase")]
430pub struct Authorization {
431    /// The identifier that the account is authorized to represent
432    pub identifier: Identifier,
433    /// Current state of the authorization
434    pub status: AuthorizationStatus,
435    /// Possible challenges for the authorization
436    pub challenges: Vec<Challenge>,
437}
438
439/// Status for an [`Authorization`]
440#[allow(missing_docs)]
441#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
442#[serde(rename_all = "camelCase")]
443pub enum AuthorizationStatus {
444    Pending,
445    Valid,
446    Invalid,
447    Revoked,
448    Expired,
449}
450
451/// Represent an identifier in an ACME [Order](crate::Order)
452#[allow(missing_docs)]
453#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
454#[serde(tag = "type", content = "value", rename_all = "camelCase")]
455pub enum Identifier {
456    Dns(String),
457}
458
459/// The challenge type
460#[allow(missing_docs)]
461#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
462pub enum ChallengeType {
463    #[serde(rename = "http-01")]
464    Http01,
465    #[serde(rename = "dns-01")]
466    Dns01,
467    #[serde(rename = "tls-alpn-01")]
468    TlsAlpn01,
469    #[serde(untagged)]
470    Unknown(String),
471}
472
473#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
474#[serde(rename_all = "camelCase")]
475pub enum ChallengeStatus {
476    Pending,
477    Processing,
478    Valid,
479    Invalid,
480}
481
482/// Status of an [Order](crate::Order)
483#[allow(missing_docs)]
484#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
485#[serde(rename_all = "camelCase")]
486pub enum OrderStatus {
487    Pending,
488    Ready,
489    Processing,
490    Valid,
491    Invalid,
492}
493
494/// Helper type to reference Let's Encrypt server URLs
495#[allow(missing_docs)]
496#[derive(Clone, Copy, Debug)]
497pub enum LetsEncrypt {
498    Production,
499    Staging,
500}
501
502impl LetsEncrypt {
503    /// Get the directory URL for the given Let's Encrypt server
504    pub const fn url(&self) -> &'static str {
505        match self {
506            Self::Production => "https://acme-v02.api.letsencrypt.org/directory",
507            Self::Staging => "https://acme-staging-v02.api.letsencrypt.org/directory",
508        }
509    }
510}
511
512/// ZeroSSL ACME only supports production at the moment
513#[allow(missing_docs)]
514#[derive(Clone, Copy, Debug)]
515pub enum ZeroSsl {
516    Production,
517}
518
519impl ZeroSsl {
520    /// Get the directory URL for the given ZeroSSL server
521    pub const fn url(&self) -> &'static str {
522        match self {
523            Self::Production => "https://acme.zerossl.com/v2/DV90",
524        }
525    }
526}
527
528#[derive(Clone, Copy, Debug, Serialize)]
529#[serde(rename_all = "UPPERCASE")]
530pub(crate) enum SigningAlgorithm {
531    /// ECDSA using P-256 and SHA-256
532    Es256,
533    /// HMAC with SHA-256,
534    Hs256,
535}
536
537#[derive(Debug, Serialize)]
538pub(crate) struct Empty {}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    // https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
545    #[test]
546    fn order() {
547        const ORDER: &str = r#"{
548          "status": "pending",
549          "expires": "2016-01-05T14:09:07.99Z",
550
551          "notBefore": "2016-01-01T00:00:00Z",
552          "notAfter": "2016-01-08T00:00:00Z",
553
554          "identifiers": [
555            { "type": "dns", "value": "www.example.org" },
556            { "type": "dns", "value": "example.org" }
557          ],
558
559          "authorizations": [
560            "https://example.com/acme/authz/PAniVnsZcis",
561            "https://example.com/acme/authz/r4HqLzrSrpI"
562          ],
563
564          "finalize": "https://example.com/acme/order/TOlocE8rfgo/finalize"
565        }"#;
566
567        let obj = serde_json::from_str::<OrderState>(ORDER).unwrap();
568        assert_eq!(obj.status, OrderStatus::Pending);
569        assert_eq!(obj.authorizations.len(), 2);
570        assert_eq!(
571            obj.finalize,
572            "https://example.com/acme/order/TOlocE8rfgo/finalize"
573        );
574    }
575
576    // https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
577    #[test]
578    fn authorization() {
579        const AUTHORIZATION: &str = r#"{
580          "status": "valid",
581          "expires": "2018-09-09T14:09:01.13Z",
582
583          "identifier": {
584            "type": "dns",
585            "value": "www.example.org"
586          },
587
588          "challenges": [
589            {
590              "type": "http-01",
591              "url": "https://example.com/acme/chall/prV_B7yEyA4",
592              "status": "valid",
593              "validated": "2014-12-01T12:05:13.72Z",
594              "token": "IlirfxKKXAsHtmzK29Pj8A"
595            }
596          ]
597        }"#;
598
599        let obj = serde_json::from_str::<Authorization>(AUTHORIZATION).unwrap();
600        assert_eq!(obj.status, AuthorizationStatus::Valid);
601        assert_eq!(obj.identifier, Identifier::Dns("www.example.org".into()));
602        assert_eq!(obj.challenges.len(), 1);
603    }
604
605    // https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
606    #[test]
607    fn challenge() {
608        const CHALLENGE: &str = r#"{
609          "type": "dns-01",
610          "url": "https://example.com/acme/chall/Rg5dV14Gh1Q",
611          "status": "pending",
612          "token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
613        }"#;
614
615        let obj = serde_json::from_str::<Challenge>(CHALLENGE).unwrap();
616        assert_eq!(obj.r#type, ChallengeType::Dns01);
617        assert_eq!(obj.url, "https://example.com/acme/chall/Rg5dV14Gh1Q");
618        assert_eq!(obj.status, ChallengeStatus::Pending);
619        assert_eq!(obj.token, "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA");
620    }
621
622    // https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
623    #[test]
624    fn problem() {
625        const PROBLEM: &str = r#"{
626          "type": "urn:ietf:params:acme:error:unauthorized",
627          "detail": "No authorization provided for name example.org"
628        }"#;
629
630        let obj = serde_json::from_str::<Problem>(PROBLEM).unwrap();
631        assert_eq!(
632            obj.r#type,
633            Some("urn:ietf:params:acme:error:unauthorized".into())
634        );
635        assert_eq!(
636            obj.detail,
637            Some("No authorization provided for name example.org".into())
638        );
639    }
640}