Skip to main content

instant_acme/
types.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::fmt::{self, Write};
4use std::net::IpAddr;
5use std::time::Instant;
6
7use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine};
8use bytes::Bytes;
9use rustls_pki_types::{CertificateDer, Der, PrivatePkcs8KeyDer};
10use serde::de::{self, DeserializeOwned};
11use serde::ser::SerializeMap;
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14#[cfg(feature = "time")]
15use time::OffsetDateTime;
16#[cfg(feature = "x509-parser")]
17use x509_parser::extensions::ParsedExtension;
18#[cfg(feature = "x509-parser")]
19use x509_parser::parse_x509_certificate;
20
21use crate::BytesResponse;
22use crate::crypto::{self, KeyPair};
23
24/// Error type for instant-acme
25#[derive(Debug, Error)]
26#[non_exhaustive]
27pub enum Error {
28    /// An JSON problem as returned by the ACME server
29    ///
30    /// RFC 8555 uses problem documents as described in RFC 7807.
31    #[error(transparent)]
32    Api(#[from] Problem),
33    /// Failed from cryptographic operations
34    #[error("cryptographic operation failed")]
35    Crypto,
36    /// Failed to instantiate a private key
37    #[error("invalid key bytes")]
38    KeyRejected,
39    /// HTTP failure
40    #[error("HTTP request failure: {0}")]
41    Http(#[from] http::Error),
42    /// Hyper request failure
43    #[cfg(feature = "hyper-rustls")]
44    #[error("HTTP request failure: {0}")]
45    Hyper(#[from] hyper::Error),
46    /// Invalid ACME server URL
47    #[error("invalid URI: {0}")]
48    InvalidUri(#[from] http::uri::InvalidUri),
49    /// Failed to (de)serialize a JSON object
50    #[error("failed to (de)serialize JSON: {0}")]
51    Json(#[from] serde_json::Error),
52    /// Timed out while waiting for the server to update [`OrderStatus`]
53    ///
54    /// If `Some`, the nested `Instant` indicates when the server suggests to poll next.
55    #[error("timed out waiting for an order update")]
56    Timeout(Option<Instant>),
57    /// ACME server does not support a requested feature
58    #[error("ACME server does not support: {0}")]
59    Unsupported(&'static str),
60    /// Other kind of error
61    #[error(transparent)]
62    Other(Box<dyn std::error::Error + Send + Sync + 'static>),
63    /// Miscellaneous errors
64    #[error("missing data: {0}")]
65    Str(&'static str),
66}
67
68impl Error {
69    #[cfg(feature = "rcgen")]
70    pub(crate) fn from_rcgen(err: rcgen::Error) -> Self {
71        Self::Other(Box::new(err))
72    }
73}
74
75impl From<&'static str> for Error {
76    fn from(s: &'static str) -> Self {
77        Self::Str(s)
78    }
79}
80
81/// ACME account credentials
82///
83/// This opaque type contains the account ID, the private key data and the
84/// server URLs from the relevant ACME server. This can be used to serialize
85/// the account credentials to a file or secret manager and restore the
86/// account from persistent storage.
87#[must_use]
88#[derive(Deserialize, Serialize)]
89pub struct AccountCredentials {
90    pub(crate) id: String,
91    /// Stored in DER, serialized as base64
92    #[serde(with = "pkcs8_serde")]
93    pub(crate) key_pkcs8: PrivatePkcs8KeyDer<'static>,
94    pub(crate) directory: Option<String>,
95    /// We never serialize `urls` by default, but we support deserializing them
96    /// in order to support serialized data from older versions of the library.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub(crate) urls: Option<Directory>,
99}
100
101impl AccountCredentials {
102    /// The account's private key
103    pub fn private_key(&self) -> &PrivatePkcs8KeyDer<'_> {
104        &self.key_pkcs8
105    }
106}
107
108mod pkcs8_serde {
109    use std::fmt;
110
111    use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine};
112    use rustls_pki_types::PrivatePkcs8KeyDer;
113    use serde::{Deserializer, Serializer, de};
114
115    pub(crate) fn serialize<S: Serializer>(
116        key_pkcs8: &PrivatePkcs8KeyDer<'_>,
117        serializer: S,
118    ) -> Result<S::Ok, S::Error> {
119        let encoded = BASE64_URL_SAFE_NO_PAD.encode(key_pkcs8.secret_pkcs8_der());
120        serializer.serialize_str(&encoded)
121    }
122
123    pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
124        deserializer: D,
125    ) -> Result<PrivatePkcs8KeyDer<'static>, D::Error> {
126        struct Visitor;
127
128        impl de::Visitor<'_> for Visitor {
129            type Value = PrivatePkcs8KeyDer<'static>;
130
131            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132                formatter.write_str("a base64-encoded PKCS#8 private key")
133            }
134
135            fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
136                match BASE64_URL_SAFE_NO_PAD.decode(v) {
137                    Ok(bytes) => Ok(PrivatePkcs8KeyDer::from(bytes)),
138                    Err(err) => Err(de::Error::custom(err)),
139                }
140            }
141        }
142
143        deserializer.deserialize_str(Visitor)
144    }
145}
146
147/// An RFC 7807 problem document as returned by the ACME server
148#[derive(Clone, Debug, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct Problem {
151    /// One of an enumerated list of problem types
152    ///
153    /// See <https://datatracker.ietf.org/doc/html/rfc8555#section-6.7>
154    pub r#type: Option<String>,
155    /// A human-readable explanation of the problem
156    pub detail: Option<String>,
157    /// The HTTP status code returned for this response
158    pub status: Option<u16>,
159    /// One or more subproblems associated with specific identifiers
160    ///
161    /// See <https://www.rfc-editor.org/rfc/rfc8555#section-6.7.1>
162    #[serde(default)]
163    pub subproblems: Vec<Subproblem>,
164}
165
166impl Problem {
167    pub(crate) async fn check<T: DeserializeOwned>(rsp: BytesResponse) -> Result<T, Error> {
168        Ok(serde_json::from_slice(&Self::from_response(rsp).await?)?)
169    }
170
171    pub(crate) async fn from_response(rsp: BytesResponse) -> Result<Bytes, Error> {
172        let status = rsp.parts.status;
173        let body = rsp.body().await.map_err(Error::Other)?;
174        match status.is_informational() || status.is_success() || status.is_redirection() {
175            true => Ok(body),
176            false => Err(serde_json::from_slice::<Self>(&body)?.into()),
177        }
178    }
179}
180
181impl fmt::Display for Problem {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        f.write_str("API error")?;
184        if let Some(detail) = &self.detail {
185            write!(f, ": {detail}")?;
186        }
187
188        if let Some(r#type) = &self.r#type {
189            write!(f, " ({type})")?;
190        }
191
192        if !self.subproblems.is_empty() {
193            let count = self.subproblems.len();
194            write!(f, ": {count} subproblems: ")?;
195            for (i, subproblem) in self.subproblems.iter().enumerate() {
196                write!(f, "{subproblem}")?;
197                if i != count - 1 {
198                    f.write_str(", ")?;
199                }
200            }
201        }
202
203        Ok(())
204    }
205}
206
207impl std::error::Error for Problem {}
208
209/// An RFC 8555 subproblem document contained within a problem returned by the ACME server
210///
211/// See <https://www.rfc-editor.org/rfc/rfc8555#section-6.7.1>
212#[derive(Clone, Debug, Deserialize)]
213pub struct Subproblem {
214    /// The identifier associated with this problem
215    pub identifier: Option<Identifier>,
216    /// One of an enumerated list of problem types
217    ///
218    /// See <https://datatracker.ietf.org/doc/html/rfc8555#section-6.7>
219    pub r#type: Option<String>,
220    /// A human-readable explanation of the problem
221    pub detail: Option<String>,
222}
223
224impl fmt::Display for Subproblem {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        if let Some(identifier) = &self.identifier {
227            write!(f, r#"for "{}""#, identifier.authorized(false))?;
228        }
229
230        if let Some(detail) = &self.detail {
231            write!(f, ": {detail}")?;
232        }
233
234        if let Some(r#type) = &self.r#type {
235            write!(f, " ({type})")?;
236        }
237
238        Ok(())
239    }
240}
241
242#[derive(Debug, Serialize)]
243pub(crate) struct FinalizeRequest {
244    csr: String,
245}
246
247impl FinalizeRequest {
248    pub(crate) fn new(csr_der: &[u8]) -> Self {
249        Self {
250            csr: BASE64_URL_SAFE_NO_PAD.encode(csr_der),
251        }
252    }
253}
254
255#[derive(Debug, Serialize)]
256pub(crate) struct Header<'a> {
257    pub(crate) alg: SigningAlgorithm,
258    #[serde(flatten)]
259    pub(crate) key: KeyOrKeyId<'a>,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub(crate) nonce: Option<&'a str>,
262    pub(crate) url: &'a str,
263}
264
265#[derive(Debug, Serialize)]
266pub(crate) enum KeyOrKeyId<'a> {
267    #[serde(rename = "jwk")]
268    Key(Jwk),
269    #[serde(rename = "kid")]
270    KeyId(&'a str),
271}
272
273impl KeyOrKeyId<'_> {
274    pub(crate) fn from_key(key: &crypto::EcdsaKeyPair) -> KeyOrKeyId<'static> {
275        KeyOrKeyId::Key(Jwk::new(key))
276    }
277}
278
279#[derive(Debug, Serialize)]
280pub(crate) struct Jwk {
281    alg: SigningAlgorithm,
282    crv: &'static str,
283    kty: &'static str,
284    r#use: &'static str,
285    x: String,
286    y: String,
287}
288
289impl Jwk {
290    pub(crate) fn new(key: &crypto::EcdsaKeyPair) -> Self {
291        let (x, y) = key.public_key().as_ref()[1..].split_at(32);
292        Self {
293            alg: SigningAlgorithm::Es256,
294            crv: "P-256",
295            kty: "EC",
296            r#use: "sig",
297            x: BASE64_URL_SAFE_NO_PAD.encode(x),
298            y: BASE64_URL_SAFE_NO_PAD.encode(y),
299        }
300    }
301
302    pub(crate) fn thumb_sha256(
303        key: &crypto::EcdsaKeyPair,
304    ) -> Result<crypto::Digest, serde_json::Error> {
305        let jwk = Self::new(key);
306        Ok(crypto::digest(
307            &crypto::SHA256,
308            &serde_json::to_vec(&JwkThumb {
309                crv: jwk.crv,
310                kty: jwk.kty,
311                x: &jwk.x,
312                y: &jwk.y,
313            })?,
314        ))
315    }
316}
317
318#[derive(Debug, Serialize)]
319struct JwkThumb<'a> {
320    crv: &'a str,
321    kty: &'a str,
322    x: &'a str,
323    y: &'a str,
324}
325
326/// An ACME challenge as described in RFC 8555 (section 7.1.5)
327///
328/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.5>
329#[derive(Debug, Deserialize)]
330pub struct Challenge {
331    /// Type of challenge
332    pub r#type: ChallengeType,
333    /// Challenge identifier
334    pub url: String,
335    /// Token for this challenge
336    ///
337    /// Unknown `ChallengeType` instances may omit this field, leaving it empty.
338    #[serde(default)]
339    pub token: String,
340    /// Current status
341    pub status: ChallengeStatus,
342    /// Potential error state
343    pub error: Option<Problem>,
344}
345
346/// Contents of an ACME order as described in RFC 8555 (section 7.1.3)
347///
348/// The order identity will usually be represented by an [Order](crate::Order).
349///
350/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3>
351#[derive(Debug, Deserialize)]
352#[non_exhaustive]
353#[serde(rename_all = "camelCase")]
354pub struct OrderState {
355    /// Current status
356    pub status: OrderStatus,
357    /// Authorizations for this order.
358    ///
359    /// There should be one authorization per identifier in the order.
360    ///
361    /// Callers will usually interact with an [`AuthorizationHandle`] obtained
362    /// via [`Order::authorizations()`] instead of using this directly.
363    ///
364    /// [`AuthorizationHandle`]: crate::AuthorizationHandle
365    /// [`Order::authorizations()`]: crate::Order::authorizations()
366    pub authorizations: Vec<Authorization>,
367    /// Potential error state
368    pub error: Option<Problem>,
369    /// A finalization URL, to be used once status becomes `Ready`
370    pub finalize: String,
371    /// The certificate URL, which becomes available after finalization
372    pub certificate: Option<String>,
373    /// The certificate that this order is replacing, if any
374    #[serde(deserialize_with = "deserialize_static_certificate_identifier")]
375    #[serde(default)]
376    pub replaces: Option<CertificateIdentifier<'static>>,
377    /// The profile to be used for the order
378    #[serde(default)]
379    pub profile: Option<String>,
380}
381
382/// A wrapper for [`AuthorizationState`] as held in the [`OrderState`]
383///
384/// Callers will usually interact with an [`AuthorizationHandle`] obtained
385/// via [`Order::authorizations()`] instead of using this directly.
386///
387/// [`AuthorizationHandle`]: crate::AuthorizationHandle
388/// [`Order::authorizations()`]: crate::Order::authorizations()
389#[derive(Debug)]
390pub struct Authorization {
391    /// URL for this authorization
392    pub url: String,
393    /// Current state of the authorization
394    ///
395    /// This starts out as `None` when the [`OrderState`] is first deserialized.
396    /// It is populated when the authorization is first fetched from the server,
397    /// typically via [`Order::authorizations()`].
398    ///
399    /// [`Order::authorizations()`]: crate::Order::authorizations()
400    pub state: Option<AuthorizationState>,
401}
402
403impl<'de> Deserialize<'de> for Authorization {
404    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
405        Ok(Self {
406            url: String::deserialize(deserializer)?,
407            state: None,
408        })
409    }
410}
411
412/// Input data for [Order](crate::Order) creation
413///
414/// To be passed into [Account::new_order()](crate::Account::new_order()).
415#[derive(Debug, Serialize)]
416#[serde(rename_all = "camelCase")]
417pub struct NewOrder<'a> {
418    /// The [`CertificateIdentifier`] of a previously issued certificate being replaced by the order
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub(crate) replaces: Option<CertificateIdentifier<'a>>,
421    /// Identifiers to be included in the order
422    identifiers: &'a [Identifier],
423    #[serde(skip_serializing_if = "Option::is_none")]
424    profile: Option<&'a str>,
425}
426
427impl<'a> NewOrder<'a> {
428    /// Prepare to create a new order for the given identifiers
429    ///
430    /// To be passed into [Account::new_order()](crate::Account::new_order()).
431    pub fn new(identifiers: &'a [Identifier]) -> Self {
432        Self {
433            identifiers,
434            replaces: None,
435            profile: None,
436        }
437    }
438
439    /// Indicate to the ACME server that the `NewOrder` is replacing a previously issued certificate
440    ///
441    /// The previously issued certificate must be identified by a `EncodedCertificateIdentifier`.
442    ///
443    /// Some ACME servers may give preferential rate limits to orders that replace
444    /// existing certificates, or use this information to determine when it is safe
445    /// to revoke a certificate affected by a compliance incident.
446    ///
447    /// When provided, at least one of the `identifiers` for the new order must have been
448    /// present in the certificate being replaced. If the ACME CA does not support the
449    /// ACME renewal information (ARI) extension, the [crate::Account::new_order()] method will
450    /// return an error.
451    pub fn replaces(mut self, replaces: CertificateIdentifier<'a>) -> Self {
452        self.replaces = Some(replaces);
453        self
454    }
455
456    /// Set the profile to be used for the order
457    ///
458    /// [`Account::new_order()`][crate::Account::new_order()] will yield an error if the ACME
459    /// server does not support the profiles extension or if the specified profile is not
460    /// supported.
461    pub fn profile(mut self, profile: &'a str) -> Self {
462        self.profile = Some(profile);
463        self
464    }
465
466    /// Identifiers to be included in the order
467    pub fn identifiers(&self) -> &[Identifier] {
468        self.identifiers
469    }
470}
471
472/// Payload for a certificate revocation request
473/// Defined in <https://datatracker.ietf.org/doc/html/rfc8555#section-7.6>
474#[derive(Debug)]
475pub struct RevocationRequest<'a> {
476    /// The certificate to revoke
477    pub certificate: &'a CertificateDer<'a>,
478    /// Reason for revocation
479    pub reason: Option<RevocationReason>,
480}
481
482impl Serialize for RevocationRequest<'_> {
483    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
484        let base64 = BASE64_URL_SAFE_NO_PAD.encode(self.certificate);
485        let mut map = serializer.serialize_map(Some(2))?;
486        map.serialize_entry("certificate", &base64)?;
487        if let Some(reason) = &self.reason {
488            map.serialize_entry("reason", reason)?;
489        }
490        map.end()
491    }
492}
493
494/// The reason for a certificate revocation
495/// Defined in <https://datatracker.ietf.org/doc/html/rfc5280#section-5.3.1>
496#[allow(missing_docs)]
497#[derive(Debug, Clone)]
498#[repr(u8)]
499pub enum RevocationReason {
500    Unspecified = 0,
501    KeyCompromise = 1,
502    CaCompromise = 2,
503    AffiliationChanged = 3,
504    Superseded = 4,
505    CessationOfOperation = 5,
506    CertificateHold = 6,
507    RemoveFromCrl = 8,
508    PrivilegeWithdrawn = 9,
509    AaCompromise = 10,
510}
511
512impl Serialize for RevocationReason {
513    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
514        serializer.serialize_u8(self.clone() as u8)
515    }
516}
517
518#[derive(Serialize)]
519#[serde(rename_all = "camelCase")]
520pub(crate) struct NewAccountPayload<'a> {
521    #[serde(flatten)]
522    pub(crate) new_account: &'a NewAccount<'a>,
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub(crate) external_account_binding: Option<JoseJson>,
525}
526
527/// Input data for [Account](crate::Account) creation
528///
529/// To be passed into [AccountBuilder::create()](crate::AccountBuilder::create()).
530#[derive(Debug, Serialize)]
531#[serde(rename_all = "camelCase")]
532pub struct NewAccount<'a> {
533    /// A list of contact URIs (like `mailto:info@example.com`)
534    pub contact: &'a [&'a str],
535    /// Whether you agree to the terms of service
536    pub terms_of_service_agreed: bool,
537    /// Set to `true` in order to retrieve an existing account
538    ///
539    /// Setting this to `false` has not been tested.
540    #[serde(skip_serializing_if = "std::ops::Not::not")]
541    pub only_return_existing: bool,
542}
543
544#[derive(Debug, Clone, Deserialize, Serialize)]
545#[serde(rename_all = "camelCase")]
546pub(crate) struct Directory {
547    pub(crate) new_nonce: String,
548    pub(crate) new_account: String,
549    pub(crate) new_order: String,
550    // The fields below were added later and old `AccountCredentials` may not have it.
551    // Newer deserialized account credentials grab a fresh set of `Directory` on
552    // deserialization, so they should be fine. Newer fields should be optional, too.
553    pub(crate) new_authz: Option<String>,
554    pub(crate) revoke_cert: Option<String>,
555    pub(crate) key_change: Option<String>,
556    // Endpoint for the ACME renewal information (ARI) extension
557    //
558    // <https://www.rfc-editor.org/rfc/rfc9773.html>
559    pub(crate) renewal_info: Option<String>,
560    #[serde(default)]
561    pub(crate) meta: Meta,
562}
563
564#[derive(Clone, Debug, Default, Deserialize, Serialize)]
565pub(crate) struct Meta {
566    #[serde(default)]
567    pub(crate) profiles: HashMap<String, String>,
568}
569
570/// Profile meta information from the server directory
571#[allow(missing_docs)]
572#[derive(Clone, Copy, Debug)]
573pub struct ProfileMeta<'a> {
574    pub name: &'a str,
575    pub description: &'a str,
576}
577
578#[derive(Serialize)]
579pub(crate) struct JoseJson {
580    pub(crate) protected: String,
581    pub(crate) payload: String,
582    pub(crate) signature: String,
583}
584
585impl JoseJson {
586    pub(crate) fn new(
587        payload: Option<&impl Serialize>,
588        protected: Header<'_>,
589        signer: &impl Signer,
590    ) -> Result<Self, Error> {
591        let protected = base64(&protected)?;
592        let payload = match payload {
593            Some(data) => base64(&data)?,
594            None => String::new(),
595        };
596
597        let combined = format!("{protected}.{payload}");
598        let signature = signer.sign(combined.as_bytes())?;
599        Ok(Self {
600            protected,
601            payload,
602            signature: BASE64_URL_SAFE_NO_PAD.encode(signature.as_ref()),
603        })
604    }
605}
606
607pub(crate) trait Signer {
608    type Signature: AsRef<[u8]>;
609
610    fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n>;
611
612    fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error>;
613}
614
615fn base64(data: &impl Serialize) -> Result<String, serde_json::Error> {
616    Ok(BASE64_URL_SAFE_NO_PAD.encode(serde_json::to_vec(data)?))
617}
618
619/// An ACME authorization's state as described in RFC 8555 (section 7.1.4)
620#[derive(Debug, Deserialize)]
621#[non_exhaustive]
622#[serde(rename_all = "camelCase")]
623pub struct AuthorizationState {
624    /// The identifier that the account is authorized to represent
625    identifier: Identifier,
626    /// Current state of the authorization
627    pub status: AuthorizationStatus,
628    /// Possible challenges for the authorization
629    pub challenges: Vec<Challenge>,
630    /// Whether the identifier represents a wildcard domain name
631    #[serde(default)]
632    pub wildcard: bool,
633}
634
635impl AuthorizationState {
636    /// Creates an [`AuthorizedIdentifier`] for the identifier in this authorization
637    pub fn identifier(&self) -> AuthorizedIdentifier<'_> {
638        self.identifier.authorized(self.wildcard)
639    }
640}
641
642/// Status for an [`AuthorizationState`]
643#[allow(missing_docs)]
644#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
645#[serde(rename_all = "camelCase")]
646pub enum AuthorizationStatus {
647    Pending,
648    Valid,
649    Invalid,
650    Revoked,
651    Expired,
652    Deactivated,
653}
654
655/// Represent an identifier in an ACME [Order](crate::Order)
656#[allow(missing_docs)]
657#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
658#[non_exhaustive]
659#[serde(tag = "type", content = "value", rename_all = "kebab-case")]
660pub enum Identifier {
661    Dns(String),
662
663    /// An IP address (IPv4 or IPv6) identifier
664    ///
665    /// Note that not all ACME servers will accept an order with an IP address identifier.
666    Ip(IpAddr),
667
668    /// Permanent Identifier
669    ///
670    /// Note that this identifier is only used for attestation.
671    PermanentIdentifier(String),
672
673    /// Hardware Module identifier
674    ///
675    /// Note that this identifier is only used for attestation.
676    HardwareModule(String),
677}
678
679impl Identifier {
680    /// Create an [`AuthorizedIdentifier`], which implements `Display`
681    ///
682    /// Needs the `wildcard` context bit to determine whether the identifier represents a
683    /// wildcard domain.
684    pub fn authorized(&self, wildcard: bool) -> AuthorizedIdentifier<'_> {
685        AuthorizedIdentifier {
686            identifier: self,
687            wildcard,
688        }
689    }
690}
691
692/// An [`Identifier`] which knows its `wildcard` context
693#[non_exhaustive]
694#[derive(Debug)]
695pub struct AuthorizedIdentifier<'a> {
696    /// The source identifier, missing any wildcard context
697    pub identifier: &'a Identifier,
698    /// Whether the identifier should be interpreted as a wildcard
699    ///
700    /// This is only relevant for DNS identifiers and must be false for other
701    /// types of identifiers (e.g. IP addresses).
702    pub wildcard: bool,
703}
704
705impl fmt::Display for AuthorizedIdentifier<'_> {
706    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
707        match (self.wildcard, self.identifier) {
708            (true, Identifier::Dns(dns)) => f.write_fmt(format_args!("*.{dns}")),
709            (false, Identifier::Dns(dns)) => f.write_str(dns),
710            (_, Identifier::Ip(addr)) => write!(f, "{addr}"),
711            (_, Identifier::PermanentIdentifier(permanent_identifier)) => {
712                f.write_str(permanent_identifier)
713            }
714            (_, Identifier::HardwareModule(hardware_module)) => f.write_str(hardware_module),
715        }
716    }
717}
718
719/// The challenge type
720#[allow(missing_docs)]
721#[non_exhaustive]
722#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
723pub enum ChallengeType {
724    #[serde(rename = "http-01")]
725    Http01,
726    #[serde(rename = "dns-01")]
727    Dns01,
728    #[serde(rename = "tls-alpn-01")]
729    TlsAlpn01,
730    /// Note: Device attestation support is experimental
731    #[serde(rename = "device-attest-01")]
732    DeviceAttest01,
733    #[serde(untagged)]
734    Unknown(String),
735}
736
737/// Status of an ACME [Challenge]
738#[allow(missing_docs)]
739#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
740#[serde(rename_all = "camelCase")]
741pub enum ChallengeStatus {
742    Pending,
743    Processing,
744    Valid,
745    Invalid,
746}
747
748/// Status of an [Order](crate::Order)
749#[allow(missing_docs)]
750#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
751#[serde(rename_all = "camelCase")]
752pub enum OrderStatus {
753    Pending,
754    Ready,
755    Processing,
756    Valid,
757    Invalid,
758}
759
760/// Helper type to reference Let's Encrypt server URLs
761#[allow(missing_docs)]
762#[derive(Clone, Copy, Debug)]
763pub enum LetsEncrypt {
764    Production,
765    Staging,
766}
767
768impl LetsEncrypt {
769    /// Get the directory URL for the given Let's Encrypt server
770    pub const fn url(&self) -> &'static str {
771        match self {
772            Self::Production => "https://acme-v02.api.letsencrypt.org/directory",
773            Self::Staging => "https://acme-staging-v02.api.letsencrypt.org/directory",
774        }
775    }
776}
777
778/// ZeroSSL ACME only supports production at the moment
779#[allow(missing_docs)]
780#[derive(Clone, Copy, Debug)]
781pub enum ZeroSsl {
782    Production,
783}
784
785impl ZeroSsl {
786    /// Get the directory URL for the given ZeroSSL server
787    pub const fn url(&self) -> &'static str {
788        match self {
789            Self::Production => "https://acme.zerossl.com/v2/DV90",
790        }
791    }
792}
793
794/// A unique certificate identifier for the ACME renewal information (ARI) extension
795///
796/// See <https://www.rfc-editor.org/rfc/rfc9773.html#section-4.1> for
797/// more information.
798#[derive(Clone, Debug, Eq, PartialEq)]
799pub struct CertificateIdentifier<'a> {
800    /// The BASE64URL-encoded authority key identifier (AKI) extension `keyIdentifier` of the certificate
801    pub authority_key_identifier: Cow<'a, str>,
802
803    /// The BASE64URL-encoded serial number of the certificate
804    pub serial: Cow<'a, str>,
805}
806
807impl CertificateIdentifier<'_> {
808    /// Encode a unique certificate identifier using the provided authority key ID and serial
809    ///
810    /// `authority_key_identifier` must be the DER-encoded ASN.1 octet string from the
811    /// `keyIdentifier` field of the `AuthorityKeyIdentifier` extension found in the certificate
812    /// to be identified.
813    ///
814    /// `serial` must be the DER-encoded ASN.1 serial number from the certificate to be identified.
815    /// Care must be taken to use the **encoded** serial number, not a big integer representation.
816    ///
817    /// The combination uniquely identifies a certificate within all certificates issued by the
818    /// same CA.
819    ///
820    /// See [RFC 5280 §4.1.2.2], [RFC 5280 §4.2.1.1], and [RFC 9773 §4.1]
821    ///
822    /// [RFC 5280 §4.1.2.2]: https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.2
823    /// [RFC 5280 §4.2.1.1]: https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.1
824    /// [RFC 9773 §4.1]: https://www.rfc-editor.org/rfc/rfc9773.html#section-4.1
825    pub fn new(authority_key_identifier: Der<'_>, serial: Der<'_>) -> Self {
826        Self {
827            authority_key_identifier: BASE64_URL_SAFE_NO_PAD
828                .encode(authority_key_identifier)
829                .into(),
830            serial: BASE64_URL_SAFE_NO_PAD.encode(serial).into(),
831        }
832    }
833
834    /// Convert the `CertificateIdentifier` into an owned version with a static lifetime
835    pub fn into_owned(self) -> CertificateIdentifier<'static> {
836        CertificateIdentifier {
837            authority_key_identifier: Cow::Owned(self.authority_key_identifier.into_owned()),
838            serial: Cow::Owned(self.serial.into_owned()),
839        }
840    }
841}
842
843impl<'de: 'a, 'a> Deserialize<'de> for CertificateIdentifier<'a> {
844    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
845        let s = <&str>::deserialize(deserializer)?;
846
847        let Some((aki, serial)) = s.split_once('.') else {
848            return Err(de::Error::invalid_value(
849                de::Unexpected::Str(s),
850                &"a string containing 2 '.'-delimited parts",
851            ));
852        };
853
854        if serial.contains('.') {
855            return Err(de::Error::invalid_value(
856                de::Unexpected::Str(s),
857                &"only one '.' delimiter should be present",
858            ));
859        }
860
861        Ok(CertificateIdentifier {
862            authority_key_identifier: Cow::Borrowed(aki),
863            serial: Cow::Borrowed(serial),
864        })
865    }
866}
867
868impl Serialize for CertificateIdentifier<'_> {
869    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
870        serializer.collect_str(self)
871    }
872}
873
874#[cfg(feature = "x509-parser")]
875impl<'a> TryFrom<&'a CertificateDer<'_>> for CertificateIdentifier<'_> {
876    type Error = String;
877
878    fn try_from(cert: &'a CertificateDer<'_>) -> Result<Self, Self::Error> {
879        let (_, parsed_cert) = parse_x509_certificate(cert.as_ref())
880            .map_err(|e| format!("failed to parse certificate: {e}"))?;
881
882        let Some(authority_key_identifier) =
883            parsed_cert
884                .iter_extensions()
885                .find_map(|ext| match ext.parsed_extension() {
886                    ParsedExtension::AuthorityKeyIdentifier(aki_ext) => aki_ext
887                        .key_identifier
888                        .as_ref()
889                        .map(|aki| Der::from_slice(aki.0)),
890                    _ => None,
891                })
892        else {
893            return Err(
894                "certificate does not contain an Authority Key Identifier (AKI) extension".into(),
895            );
896        };
897
898        Ok(Self::new(
899            authority_key_identifier,
900            Der::from_slice(parsed_cert.tbs_certificate.raw_serial()),
901        ))
902    }
903}
904
905impl fmt::Display for CertificateIdentifier<'_> {
906    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
907        f.write_str(&self.authority_key_identifier)?;
908        f.write_char('.')?;
909        f.write_str(&self.serial)
910    }
911}
912
913/// Information about a suggested renewal window for a certificate
914///
915/// See <https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2>
916#[derive(Clone, Debug, Deserialize)]
917#[serde(rename_all = "camelCase")]
918#[cfg(feature = "time")]
919pub struct RenewalInfo {
920    /// The suggested renewal window for a certificate
921    pub suggested_window: SuggestedWindow,
922    /// A URL to a page explaining why the suggested renewal window has its current value
923    #[serde(rename = "explanationURL")]
924    pub explanation_url: Option<String>,
925}
926
927/// A suggested renewal window for a certificate
928///
929/// See <https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2>
930#[derive(Clone, Debug, Deserialize)]
931#[serde(rename_all = "camelCase")]
932#[cfg(feature = "time")]
933pub struct SuggestedWindow {
934    /// The start [`OffsetDateTime`] of the suggested renewal window
935    #[serde(with = "time::serde::rfc3339")]
936    pub start: OffsetDateTime,
937    /// The end [`OffsetDateTime`] of the suggested renewal window
938    #[serde(with = "time::serde::rfc3339")]
939    pub end: OffsetDateTime,
940}
941
942fn deserialize_static_certificate_identifier<'de, D: serde::Deserializer<'de>>(
943    deserializer: D,
944) -> Result<Option<CertificateIdentifier<'static>>, D::Error> {
945    let Some(cert_id) = Option::<CertificateIdentifier<'_>>::deserialize(deserializer)? else {
946        return Ok(None);
947    };
948
949    Ok(Some(cert_id.into_owned()))
950}
951
952#[derive(Clone, Copy, Debug, Serialize)]
953#[serde(rename_all = "UPPERCASE")]
954pub(crate) enum SigningAlgorithm {
955    /// ECDSA using P-256 and SHA-256
956    Es256,
957    /// HMAC with SHA-256,
958    Hs256,
959}
960
961/// Attestation payload used for device-attest-01
962///
963/// See <https://datatracker.ietf.org/doc/draft-acme-device-attest/> for details.
964pub struct DeviceAttestation<'a> {
965    /// CBOR encoded attestation payload
966    pub att_obj: Cow<'a, [u8]>,
967}
968
969#[derive(Debug, Serialize)]
970pub(crate) struct Empty {}
971
972#[cfg(test)]
973mod tests {
974    #[cfg(feature = "x509-parser")]
975    use rcgen::{
976        BasicConstraints, CertificateParams, DistinguishedName, IsCa, Issuer, KeyIdMethod, KeyPair,
977        SerialNumber,
978    };
979
980    use super::*;
981
982    // https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
983    #[test]
984    fn order() {
985        const ORDER: &str = r#"{
986          "status": "pending",
987          "expires": "2016-01-05T14:09:07.99Z",
988
989          "notBefore": "2016-01-01T00:00:00Z",
990          "notAfter": "2016-01-08T00:00:00Z",
991
992          "identifiers": [
993            { "type": "dns", "value": "www.example.org" },
994            { "type": "dns", "value": "example.org" }
995          ],
996
997          "authorizations": [
998            "https://example.com/acme/authz/PAniVnsZcis",
999            "https://example.com/acme/authz/r4HqLzrSrpI"
1000          ],
1001
1002          "finalize": "https://example.com/acme/order/TOlocE8rfgo/finalize"
1003        }"#;
1004
1005        let obj = serde_json::from_str::<OrderState>(ORDER).unwrap();
1006        assert_eq!(obj.status, OrderStatus::Pending);
1007        assert_eq!(obj.authorizations.len(), 2);
1008        assert_eq!(
1009            obj.finalize,
1010            "https://example.com/acme/order/TOlocE8rfgo/finalize"
1011        );
1012    }
1013
1014    // https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
1015    #[test]
1016    fn authorization() {
1017        const AUTHORIZATION: &str = r#"{
1018          "status": "valid",
1019          "expires": "2018-09-09T14:09:01.13Z",
1020
1021          "identifier": {
1022            "type": "dns",
1023            "value": "www.example.org"
1024          },
1025
1026          "challenges": [
1027            {
1028              "type": "http-01",
1029              "url": "https://example.com/acme/chall/prV_B7yEyA4",
1030              "status": "valid",
1031              "validated": "2014-12-01T12:05:13.72Z",
1032              "token": "IlirfxKKXAsHtmzK29Pj8A"
1033            }
1034          ]
1035        }"#;
1036
1037        let obj = serde_json::from_str::<AuthorizationState>(AUTHORIZATION).unwrap();
1038        assert_eq!(obj.status, AuthorizationStatus::Valid);
1039        assert_eq!(obj.identifier, Identifier::Dns("www.example.org".into()));
1040        assert_eq!(obj.challenges.len(), 1);
1041    }
1042
1043    // https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
1044    #[test]
1045    fn challenge() {
1046        const CHALLENGE: &str = r#"{
1047          "type": "dns-01",
1048          "url": "https://example.com/acme/chall/Rg5dV14Gh1Q",
1049          "status": "pending",
1050          "token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
1051        }"#;
1052
1053        let obj = serde_json::from_str::<Challenge>(CHALLENGE).unwrap();
1054        assert_eq!(obj.r#type, ChallengeType::Dns01);
1055        assert_eq!(obj.url, "https://example.com/acme/chall/Rg5dV14Gh1Q");
1056        assert_eq!(obj.status, ChallengeStatus::Pending);
1057        assert_eq!(obj.token, "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA");
1058    }
1059
1060    // https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
1061    #[test]
1062    fn problem() {
1063        const PROBLEM: &str = r#"{
1064          "type": "urn:ietf:params:acme:error:unauthorized",
1065          "detail": "No authorization provided for name example.org"
1066        }"#;
1067
1068        let obj = serde_json::from_str::<Problem>(PROBLEM).unwrap();
1069        assert_eq!(
1070            obj.r#type,
1071            Some("urn:ietf:params:acme:error:unauthorized".into())
1072        );
1073        assert_eq!(
1074            obj.detail,
1075            Some("No authorization provided for name example.org".into())
1076        );
1077        assert!(obj.subproblems.is_empty());
1078    }
1079
1080    // https://www.rfc-editor.org/rfc/rfc8555#section-6.7.1
1081    #[test]
1082    fn subproblems() {
1083        const PROBLEM: &str = r#"{
1084            "type": "urn:ietf:params:acme:error:malformed",
1085            "detail": "Some of the identifiers requested were rejected",
1086            "subproblems": [
1087                {
1088                    "type": "urn:ietf:params:acme:error:malformed",
1089                    "detail": "Invalid underscore in DNS name \"_example.org\"",
1090                    "identifier": {
1091                        "type": "dns",
1092                        "value": "_example.org"
1093                    }
1094                },
1095                {
1096                    "type": "urn:ietf:params:acme:error:rejectedIdentifier",
1097                    "detail": "This CA will not issue for \"example.net\"",
1098                    "identifier": {
1099                        "type": "dns",
1100                        "value": "example.net"
1101                    }
1102                }
1103            ]
1104        }"#;
1105
1106        let obj = serde_json::from_str::<Problem>(PROBLEM).unwrap();
1107        assert_eq!(
1108            obj.r#type,
1109            Some("urn:ietf:params:acme:error:malformed".into())
1110        );
1111        assert_eq!(
1112            obj.detail,
1113            Some("Some of the identifiers requested were rejected".into())
1114        );
1115
1116        let subproblems = &obj.subproblems;
1117        assert_eq!(subproblems.len(), 2);
1118
1119        let first_subproblem = subproblems.first().unwrap();
1120        assert_eq!(
1121            first_subproblem.identifier,
1122            Some(Identifier::Dns("_example.org".into()))
1123        );
1124        assert_eq!(
1125            first_subproblem.r#type,
1126            Some("urn:ietf:params:acme:error:malformed".into())
1127        );
1128        assert_eq!(
1129            first_subproblem.detail,
1130            Some(r#"Invalid underscore in DNS name "_example.org""#.into())
1131        );
1132
1133        let second_subproblem = subproblems.get(1).unwrap();
1134        assert_eq!(
1135            second_subproblem.identifier,
1136            Some(Identifier::Dns("example.net".into()))
1137        );
1138        assert_eq!(
1139            second_subproblem.r#type,
1140            Some("urn:ietf:params:acme:error:rejectedIdentifier".into())
1141        );
1142        assert_eq!(
1143            second_subproblem.detail,
1144            Some(r#"This CA will not issue for "example.net""#.into())
1145        );
1146
1147        let expected_display = "\
1148    API error: Some of the identifiers requested were rejected (urn:ietf:params:acme:error:malformed): \
1149    2 subproblems: \
1150    for \"_example.org\": Invalid underscore in DNS name \"_example.org\" (urn:ietf:params:acme:error:malformed), \
1151    for \"example.net\": This CA will not issue for \"example.net\" (urn:ietf:params:acme:error:rejectedIdentifier)";
1152        assert_eq!(format!("{obj}"), expected_display);
1153    }
1154
1155    // https://www.rfc-editor.org/rfc/rfc9773.html#section-4.1
1156    #[test]
1157    fn certificate_identifier() {
1158        const ORDER: &str = r#"{
1159          "status": "pending",
1160          "expires": "2016-01-05T14:09:07.99Z",
1161
1162          "notBefore": "2016-01-01T00:00:00Z",
1163          "notAfter": "2016-01-08T00:00:00Z",
1164
1165          "identifiers": [
1166            { "type": "dns", "value": "www.example.org" },
1167            { "type": "dns", "value": "example.org" }
1168          ],
1169
1170          "authorizations": [
1171            "https://example.com/acme/authz/PAniVnsZcis",
1172            "https://example.com/acme/authz/r4HqLzrSrpI"
1173          ],
1174
1175          "finalize": "https://example.com/acme/order/TOlocE8rfgo/finalize",
1176
1177          "replaces": "aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE"
1178        }"#;
1179
1180        let order = serde_json::from_str::<OrderState>(ORDER).unwrap();
1181        let cert_id = order.replaces.unwrap();
1182        assert_eq!(
1183            cert_id.authority_key_identifier,
1184            "aYhba4dGQEHhs3uEe6CuLN4ByNQ"
1185        );
1186        assert_eq!(cert_id.serial, "AIdlQyE");
1187
1188        let serialized = serde_json::to_string(&cert_id).unwrap();
1189        assert_eq!(serialized, r#""aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE""#);
1190    }
1191
1192    #[cfg(feature = "x509-parser")]
1193    #[test]
1194    fn encoded_certificate_identifier_from_cert() {
1195        // Generate a CA key_pair and self-signed cert with a specific subject key identifier.
1196        let ca_key_id = vec![0xC0, 0xFF, 0xEE];
1197        let ca_key = KeyPair::generate().unwrap();
1198        let mut ca_params = CertificateParams::default();
1199        ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
1200        ca_params.key_identifier_method = KeyIdMethod::PreSpecified(ca_key_id);
1201        let ca = Issuer::new(ca_params, ca_key);
1202
1203        // Generate an end entity certificate issued by the CA, with a specific serial number
1204        // and an AKI extension.
1205        let ee_key = KeyPair::generate().unwrap();
1206        let ee_serial = [0xCA, 0xFE];
1207        let mut ee_params = CertificateParams::new(["example.com".to_owned()]).unwrap();
1208        ee_params.distinguished_name = DistinguishedName::new();
1209        ee_params.serial_number = Some(SerialNumber::from_slice(ee_serial.as_slice()));
1210        ee_params.use_authority_key_identifier_extension = true;
1211        let ee_cert = ee_params.signed_by(&ee_key, &ca).unwrap();
1212
1213        // Extract the AKI and serial number from the EE certificate and create an encoded
1214        // certificate identifier.
1215        let encoded = CertificateIdentifier::try_from(ee_cert.der()).unwrap();
1216
1217        // We should arrive at the expected encoded certificate identifier.
1218        assert_eq!(format!("{encoded}"), "wP_u.AMr-");
1219    }
1220
1221    // https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2
1222    #[test]
1223    #[cfg(feature = "time")]
1224    fn renewal_info() {
1225        const INFO: &str = r#"{
1226          "suggestedWindow": {
1227            "start": "2025-01-02T04:00:00Z",
1228            "end": "2025-01-03T04:00:00Z"
1229          },
1230          "explanationURL": "https://acme.example.com/docs/ari"
1231        }
1232        "#;
1233
1234        let info = serde_json::from_str::<RenewalInfo>(INFO).unwrap();
1235        assert_eq!(
1236            info.explanation_url.unwrap(),
1237            "https://acme.example.com/docs/ari"
1238        );
1239        let window = info.suggested_window;
1240        assert_eq!(window.start.day(), 2);
1241        assert_eq!(window.end.day(), 3);
1242    }
1243}