acme_lite/
api.rs

1//! Low-level API JSON objects.
2//!
3//! Unstable and not to be used directly. Provided to aid debugging.
4
5use std::fmt;
6
7use serde::{
8    ser::{SerializeMap, Serializer},
9    Deserialize, Serialize,
10};
11
12/// Serializes to `""`.
13pub struct ApiEmptyString;
14
15impl Serialize for ApiEmptyString {
16    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
17        serializer.serialize_str("")
18    }
19}
20
21/// Serializes to `{}`.
22pub struct ApiEmptyObject;
23
24impl Serialize for ApiEmptyObject {
25    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
26        serializer.serialize_map(Some(0))?.end()
27    }
28}
29
30#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
31pub struct ApiProblem {
32    #[serde(rename = "type")]
33    pub _type: String,
34
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub detail: Option<String>,
37
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub subproblems: Option<Vec<ApiSubproblem>>,
40}
41
42impl ApiProblem {
43    pub fn is_bad_nonce(&self) -> bool {
44        self._type == "badNonce"
45    }
46
47    pub fn is_jwt_verification_error(&self) -> bool {
48        (self._type == "urn:acme:error:malformed"
49            || self._type == "urn:ietf:params:acme:error:malformed")
50            && self
51                .detail
52                .as_ref()
53                .map(|s| s == "JWS verification error")
54                .unwrap_or(false)
55    }
56}
57
58impl fmt::Display for ApiProblem {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        if let Some(detail) = &self.detail {
61            write!(f, "{}: {detail}", self._type)
62        } else {
63            write!(f, "{}", self._type)
64        }
65    }
66}
67
68#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
69pub struct ApiSubproblem {
70    #[serde(rename = "type")]
71    pub _type: String,
72    pub detail: Option<String>,
73    pub identifier: Option<ApiIdentifier>,
74}
75
76/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1>
77//
78// {
79//   "newNonce": "https://example.com/acme/new-nonce",
80//   "newAccount": "https://example.com/acme/new-account",
81//   "newOrder": "https://example.com/acme/new-order",
82//   "newAuthz": "https://example.com/acme/new-authz",
83//   "revokeCert": "https://example.com/acme/revoke-cert",
84//   "keyChange": "https://example.com/acme/key-change",
85//   "meta": {
86//     "termsOfService": "https://example.com/acme/terms/2017-5-30",
87//     "website": "https://www.example.com/",
88//     "caaIdentities": ["example.com"],
89//     "externalAccountRequired": false
90//   }
91// }
92#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct ApiDirectory {
95    pub new_nonce: String,
96    pub new_account: String,
97    pub new_order: String,
98
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub new_authz: Option<String>,
101
102    pub revoke_cert: String,
103    pub key_change: String,
104
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub meta: Option<ApiDirectoryMeta>,
107}
108
109/// <https://datatracker.ietf.org/doc/html/rfc8555#section-9.7.6>
110#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct ApiDirectoryMeta {
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub terms_of_service: Option<String>,
115
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub website: Option<String>,
118
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub caa_identities: Option<Vec<String>>,
121
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub external_account_required: Option<bool>,
124}
125
126impl ApiDirectoryMeta {
127    pub fn external_account_required(&self) -> bool {
128        self.external_account_required.unwrap_or(false)
129    }
130}
131
132/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.2>
133//
134//    {
135//      "status": "valid",
136//      "contact": [
137//        "mailto:cert-admin@example.com",
138//        "mailto:admin@example.com"
139//      ],
140//      "termsOfServiceAgreed": true,
141//      "orders": "https://example.com/acme/acct/evOfKhNU60wg/orders"
142//    }
143#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct ApiAccount {
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub status: Option<String>,
148
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub contact: Option<Vec<String>>,
151
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub external_account_binding: Option<String>,
154
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub terms_of_service_agreed: Option<bool>,
157
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub only_return_existing: Option<bool>,
160
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub orders: Option<String>,
163}
164
165impl ApiAccount {
166    pub fn is_status_valid(&self) -> bool {
167        self.status.as_ref().map(|s| s.as_ref()) == Some("valid")
168    }
169
170    pub fn is_status_deactivated(&self) -> bool {
171        self.status.as_ref().map(|s| s.as_ref()) == Some("deactivated")
172    }
173
174    pub fn is_status_revoked(&self) -> bool {
175        self.status.as_ref().map(|s| s.as_ref()) == Some("revoked")
176    }
177
178    pub fn terms_of_service_agreed(&self) -> bool {
179        self.terms_of_service_agreed.unwrap_or(false)
180    }
181}
182
183/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3>
184// {
185//   "status": "pending",
186//   "expires": "2019-01-09T08:26:43.570360537Z",
187//   "identifiers": [
188//     {
189//       "type": "dns",
190//       "value": "acmetest.algesten.se"
191//     }
192//   ],
193//   "authorizations": [
194//     "https://example.com/acme/authz/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs"
195//   ],
196//   "finalize": "https://example.com/acme/finalize/7738992/18234324"
197// }
198#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct ApiOrder {
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub status: Option<String>,
203
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub expires: Option<String>,
206
207    pub identifiers: Vec<ApiIdentifier>,
208    pub not_before: Option<String>,
209    pub not_after: Option<String>,
210    pub error: Option<ApiProblem>,
211    pub authorizations: Option<Vec<String>>,
212    pub finalize: String,
213    pub certificate: Option<String>,
214}
215
216impl ApiOrder {
217    /// As long as there are outstanding authorizations.
218    pub fn is_status_pending(&self) -> bool {
219        self.status.as_ref().map(|s| s.as_ref()) == Some("pending")
220    }
221
222    /// When all authorizations are finished, and we need to call "finalize".
223    pub fn is_status_ready(&self) -> bool {
224        self.status.as_ref().map(|s| s.as_ref()) == Some("ready")
225    }
226
227    /// On "finalize" the server is processing to sign CSR.
228    pub fn is_status_processing(&self) -> bool {
229        self.status.as_ref().map(|s| s.as_ref()) == Some("processing")
230    }
231
232    /// Once the certificate is issued and can be downloaded.
233    pub fn is_status_valid(&self) -> bool {
234        self.status.as_ref().map(|s| s.as_ref()) == Some("valid")
235    }
236
237    /// If the order failed and can't be used again.
238    pub fn is_status_invalid(&self) -> bool {
239        self.status.as_ref().map(|s| s.as_ref()) == Some("invalid")
240    }
241
242    /// Returns all domains.
243    pub fn domains(&self) -> Vec<&str> {
244        self.identifiers.iter().map(|i| i.value.as_ref()).collect()
245    }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249pub struct ApiIdentifier {
250    #[serde(rename = "type")]
251    pub _type: String,
252    pub value: String,
253}
254
255impl ApiIdentifier {
256    pub fn is_type_dns(&self) -> bool {
257        self._type == "dns"
258    }
259}
260
261// {
262//   "identifier": {
263//     "type": "dns",
264//     "value": "acmetest.algesten.se"
265//   },
266//   "status": "pending",
267//   "expires": "2019-01-09T08:26:43Z",
268//   "challenges": [
269//     {
270//       "type": "http-01",
271//       "status": "pending",
272//       "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789597",
273//       "token": "MUi-gqeOJdRkSb_YR2eaMxQBqf6al8dgt_dOttSWb0w"
274//     },
275//     {
276//       "type": "tls-alpn-01",
277//       "status": "pending",
278//       "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789598",
279//       "token": "WCdRWkCy4THTD_j5IH4ISAzr59lFIg5wzYmKxuOJ1lU"
280//     },
281//     {
282//       "type": "dns-01",
283//       "status": "pending",
284//       "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789599",
285//       "token": "RRo2ZcXAEqxKvMH8RGcATjSK1KknLEUmauwfQ5i3gG8"
286//     }
287//   ]
288// }
289//
290// on incorrect challenge, something like:
291//
292//   "challenges": [
293//     {
294//       "type": "dns-01",
295//       "status": "invalid",
296//       "error": {
297//         "type": "urn:ietf:params:acme:error:dns",
298//         "detail": "DNS problem: NXDOMAIN looking up TXT for _acme-challenge.martintest.foobar.com",
299//         "status": 400
300//       },
301//       "url": "https://example.com/acme/challenge/afyChhlFB8GLLmIqEnqqcXzX0Ss3GBw6oUlKAGDG6lY/221695600",
302//       "token": "YsNqBWZnyYjDun3aUC2CkCopOaqZRrI5hp3tUjxPLQU"
303//     },
304// "Incorrect TXT record \"caOh44dp9eqXNRkd0sYrKVF8dBl0L8h8-kFpIBje-2c\" found at _acme-challenge.martintest.foobar.com
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
306pub struct ApiAuth {
307    pub identifier: ApiIdentifier,
308    pub status: Option<String>,
309    pub expires: Option<String>,
310    pub challenges: Vec<ApiChallenge>,
311
312    /// This field MUST be present and true for authorizations created as a result of a newOrder
313    /// request containing a DNS identifier with a value that was a wildcard domain name. For other
314    /// authorizations, it MUST be absent. Wildcard domain names are described in ยง7.1.3.
315    pub wildcard: Option<bool>,
316}
317
318impl ApiAuth {
319    pub fn is_status_pending(&self) -> bool {
320        self.status.as_ref().map(|s| s.as_ref()) == Some("pending")
321    }
322
323    pub fn is_status_valid(&self) -> bool {
324        self.status.as_ref().map(|s| s.as_ref()) == Some("valid")
325    }
326
327    pub fn is_status_invalid(&self) -> bool {
328        self.status.as_ref().map(|s| s.as_ref()) == Some("invalid")
329    }
330
331    pub fn is_status_deactivated(&self) -> bool {
332        self.status.as_ref().map(|s| s.as_ref()) == Some("deactivated")
333    }
334
335    pub fn is_status_expired(&self) -> bool {
336        self.status.as_ref().map(|s| s.as_ref()) == Some("expired")
337    }
338
339    pub fn is_status_revoked(&self) -> bool {
340        self.status.as_ref().map(|s| s.as_ref()) == Some("revoked")
341    }
342
343    pub fn wildcard(&self) -> bool {
344        self.wildcard.unwrap_or(false)
345    }
346
347    pub fn http_challenge(&self) -> Option<&ApiChallenge> {
348        self.challenges.iter().find(|c| c._type == "http-01")
349    }
350
351    pub fn dns_challenge(&self) -> Option<&ApiChallenge> {
352        self.challenges.iter().find(|c| c._type == "dns-01")
353    }
354
355    pub fn tls_alpn_challenge(&self) -> Option<&ApiChallenge> {
356        self.challenges.iter().find(|c| c._type == "tls-alpn-01")
357    }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
361pub struct ApiChallenge {
362    pub url: String,
363    #[serde(rename = "type")]
364    pub _type: String,
365    pub status: String,
366    pub token: String,
367    pub validated: Option<String>,
368    pub error: Option<ApiProblem>,
369}
370
371// {
372//   "type": "http-01",
373//   "status": "pending",
374//   "url": "https://acme-staging-v02.api.letsencrypt.org/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789597",
375//   "token": "MUi-gqeOJdRkSb_YR2eaMxQBqf6al8dgt_dOttSWb0w"
376// }
377impl ApiChallenge {
378    pub fn is_status_pending(&self) -> bool {
379        &self.status == "pending"
380    }
381
382    pub fn is_status_processing(&self) -> bool {
383        &self.status == "processing"
384    }
385
386    pub fn is_status_valid(&self) -> bool {
387        &self.status == "valid"
388    }
389
390    pub fn is_status_invalid(&self) -> bool {
391        &self.status == "invalid"
392    }
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
396pub struct ApiFinalize {
397    pub csr: String,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct ApiRevocation {
402    pub certificate: String,
403    pub reason: usize,
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn test_api_empty_string() {
412        let x = serde_json::to_string(&ApiEmptyString).unwrap();
413        assert_eq!("\"\"", x);
414    }
415
416    #[test]
417    fn test_api_empty_object() {
418        let x = serde_json::to_string(&ApiEmptyObject).unwrap();
419        assert_eq!("{}", x);
420    }
421}