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#[derive(Debug, Error)]
16pub enum Error {
17 #[error(transparent)]
21 Api(#[from] Problem),
22 #[error("base64 decoding failed: {0}")]
24 Base64(#[from] base64::DecodeError),
25 #[error("cryptographic operation failed: {0}")]
27 Crypto(#[from] crypto::Unspecified),
28 #[error("invalid key bytes: {0}")]
30 CryptoKey(#[from] crypto::KeyRejected),
31 #[error("HTTP request failure: {0}")]
33 Http(#[from] http::Error),
34 #[cfg(feature = "hyper-rustls")]
36 #[error("HTTP request failure: {0}")]
37 Hyper(#[from] hyper::Error),
38 #[error("invalid URI: {0}")]
40 InvalidUri(#[from] http::uri::InvalidUri),
41 #[error("failed to (de)serialize JSON: {0}")]
43 Json(#[from] serde_json::Error),
44 #[error(transparent)]
46 Other(Box<dyn std::error::Error + Send + Sync + 'static>),
47 #[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#[derive(Deserialize, Serialize)]
72pub struct AccountCredentials {
73 pub(crate) id: String,
74 #[serde(with = "pkcs8_serde")]
76 pub(crate) key_pkcs8: Vec<u8>,
77 pub(crate) directory: Option<String>,
78 #[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#[derive(Clone, Debug, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct Problem {
126 pub r#type: Option<String>,
130 pub detail: Option<String>,
132 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#[derive(Debug, Deserialize)]
256pub struct Challenge {
257 pub r#type: ChallengeType,
259 pub url: String,
261 pub token: String,
263 pub status: ChallengeStatus,
265 pub error: Option<Problem>,
267}
268
269#[derive(Debug, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct OrderState {
277 pub status: OrderStatus,
279 pub authorizations: Vec<String>,
283 pub error: Option<Problem>,
285 pub finalize: String,
287 pub certificate: Option<String>,
289}
290
291#[derive(Debug, Serialize)]
295#[serde(rename_all = "camelCase")]
296pub struct NewOrder<'a> {
297 pub identifiers: &'a [Identifier],
299}
300
301#[derive(Debug)]
304pub struct RevocationRequest<'a> {
305 pub certificate: &'a CertificateDer<'a>,
307 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#[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#[derive(Debug, Serialize)]
360#[serde(rename_all = "camelCase")]
361pub struct NewAccount<'a> {
362 pub contact: &'a [&'a str],
364 pub terms_of_service_agreed: bool,
366 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 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#[derive(Debug, Deserialize)]
429#[serde(rename_all = "camelCase")]
430pub struct Authorization {
431 pub identifier: Identifier,
433 pub status: AuthorizationStatus,
435 pub challenges: Vec<Challenge>,
437}
438
439#[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#[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#[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#[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#[allow(missing_docs)]
496#[derive(Clone, Copy, Debug)]
497pub enum LetsEncrypt {
498 Production,
499 Staging,
500}
501
502impl LetsEncrypt {
503 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#[allow(missing_docs)]
514#[derive(Clone, Copy, Debug)]
515pub enum ZeroSsl {
516 Production,
517}
518
519impl ZeroSsl {
520 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 Es256,
533 Hs256,
535}
536
537#[derive(Debug, Serialize)]
538pub(crate) struct Empty {}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[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 #[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 #[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 #[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}