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#[derive(Debug, Error)]
26#[non_exhaustive]
27pub enum Error {
28 #[error(transparent)]
32 Api(#[from] Problem),
33 #[error("cryptographic operation failed")]
35 Crypto,
36 #[error("invalid key bytes")]
38 KeyRejected,
39 #[error("HTTP request failure: {0}")]
41 Http(#[from] http::Error),
42 #[cfg(feature = "hyper-rustls")]
44 #[error("HTTP request failure: {0}")]
45 Hyper(#[from] hyper::Error),
46 #[error("invalid URI: {0}")]
48 InvalidUri(#[from] http::uri::InvalidUri),
49 #[error("failed to (de)serialize JSON: {0}")]
51 Json(#[from] serde_json::Error),
52 #[error("timed out waiting for an order update")]
56 Timeout(Option<Instant>),
57 #[error("ACME server does not support: {0}")]
59 Unsupported(&'static str),
60 #[error(transparent)]
62 Other(Box<dyn std::error::Error + Send + Sync + 'static>),
63 #[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#[must_use]
88#[derive(Deserialize, Serialize)]
89pub struct AccountCredentials {
90 pub(crate) id: String,
91 #[serde(with = "pkcs8_serde")]
93 pub(crate) key_pkcs8: PrivatePkcs8KeyDer<'static>,
94 pub(crate) directory: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
98 pub(crate) urls: Option<Directory>,
99}
100
101impl AccountCredentials {
102 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#[derive(Clone, Debug, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct Problem {
151 pub r#type: Option<String>,
155 pub detail: Option<String>,
157 pub status: Option<u16>,
159 #[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#[derive(Clone, Debug, Deserialize)]
213pub struct Subproblem {
214 pub identifier: Option<Identifier>,
216 pub r#type: Option<String>,
220 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#[derive(Debug, Deserialize)]
330pub struct Challenge {
331 pub r#type: ChallengeType,
333 pub url: String,
335 #[serde(default)]
339 pub token: String,
340 pub status: ChallengeStatus,
342 pub error: Option<Problem>,
344}
345
346#[derive(Debug, Deserialize)]
352#[non_exhaustive]
353#[serde(rename_all = "camelCase")]
354pub struct OrderState {
355 pub status: OrderStatus,
357 pub authorizations: Vec<Authorization>,
367 pub error: Option<Problem>,
369 pub finalize: String,
371 pub certificate: Option<String>,
373 #[serde(deserialize_with = "deserialize_static_certificate_identifier")]
375 #[serde(default)]
376 pub replaces: Option<CertificateIdentifier<'static>>,
377 #[serde(default)]
379 pub profile: Option<String>,
380}
381
382#[derive(Debug)]
390pub struct Authorization {
391 pub url: String,
393 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#[derive(Debug, Serialize)]
416#[serde(rename_all = "camelCase")]
417pub struct NewOrder<'a> {
418 #[serde(skip_serializing_if = "Option::is_none")]
420 pub(crate) replaces: Option<CertificateIdentifier<'a>>,
421 identifiers: &'a [Identifier],
423 #[serde(skip_serializing_if = "Option::is_none")]
424 profile: Option<&'a str>,
425}
426
427impl<'a> NewOrder<'a> {
428 pub fn new(identifiers: &'a [Identifier]) -> Self {
432 Self {
433 identifiers,
434 replaces: None,
435 profile: None,
436 }
437 }
438
439 pub fn replaces(mut self, replaces: CertificateIdentifier<'a>) -> Self {
452 self.replaces = Some(replaces);
453 self
454 }
455
456 pub fn profile(mut self, profile: &'a str) -> Self {
462 self.profile = Some(profile);
463 self
464 }
465
466 pub fn identifiers(&self) -> &[Identifier] {
468 self.identifiers
469 }
470}
471
472#[derive(Debug)]
475pub struct RevocationRequest<'a> {
476 pub certificate: &'a CertificateDer<'a>,
478 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#[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#[derive(Debug, Serialize)]
531#[serde(rename_all = "camelCase")]
532pub struct NewAccount<'a> {
533 pub contact: &'a [&'a str],
535 pub terms_of_service_agreed: bool,
537 #[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 pub(crate) new_authz: Option<String>,
554 pub(crate) revoke_cert: Option<String>,
555 pub(crate) key_change: Option<String>,
556 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#[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#[derive(Debug, Deserialize)]
621#[non_exhaustive]
622#[serde(rename_all = "camelCase")]
623pub struct AuthorizationState {
624 identifier: Identifier,
626 pub status: AuthorizationStatus,
628 pub challenges: Vec<Challenge>,
630 #[serde(default)]
632 pub wildcard: bool,
633}
634
635impl AuthorizationState {
636 pub fn identifier(&self) -> AuthorizedIdentifier<'_> {
638 self.identifier.authorized(self.wildcard)
639 }
640}
641
642#[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#[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 Ip(IpAddr),
667
668 PermanentIdentifier(String),
672
673 HardwareModule(String),
677}
678
679impl Identifier {
680 pub fn authorized(&self, wildcard: bool) -> AuthorizedIdentifier<'_> {
685 AuthorizedIdentifier {
686 identifier: self,
687 wildcard,
688 }
689 }
690}
691
692#[non_exhaustive]
694#[derive(Debug)]
695pub struct AuthorizedIdentifier<'a> {
696 pub identifier: &'a Identifier,
698 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#[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 #[serde(rename = "device-attest-01")]
732 DeviceAttest01,
733 #[serde(untagged)]
734 Unknown(String),
735}
736
737#[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#[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#[allow(missing_docs)]
762#[derive(Clone, Copy, Debug)]
763pub enum LetsEncrypt {
764 Production,
765 Staging,
766}
767
768impl LetsEncrypt {
769 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#[allow(missing_docs)]
780#[derive(Clone, Copy, Debug)]
781pub enum ZeroSsl {
782 Production,
783}
784
785impl ZeroSsl {
786 pub const fn url(&self) -> &'static str {
788 match self {
789 Self::Production => "https://acme.zerossl.com/v2/DV90",
790 }
791 }
792}
793
794#[derive(Clone, Debug, Eq, PartialEq)]
799pub struct CertificateIdentifier<'a> {
800 pub authority_key_identifier: Cow<'a, str>,
802
803 pub serial: Cow<'a, str>,
805}
806
807impl CertificateIdentifier<'_> {
808 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 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#[derive(Clone, Debug, Deserialize)]
917#[serde(rename_all = "camelCase")]
918#[cfg(feature = "time")]
919pub struct RenewalInfo {
920 pub suggested_window: SuggestedWindow,
922 #[serde(rename = "explanationURL")]
924 pub explanation_url: Option<String>,
925}
926
927#[derive(Clone, Debug, Deserialize)]
931#[serde(rename_all = "camelCase")]
932#[cfg(feature = "time")]
933pub struct SuggestedWindow {
934 #[serde(with = "time::serde::rfc3339")]
936 pub start: OffsetDateTime,
937 #[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 Es256,
957 Hs256,
959}
960
961pub struct DeviceAttestation<'a> {
965 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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 let encoded = CertificateIdentifier::try_from(ee_cert.der()).unwrap();
1216
1217 assert_eq!(format!("{encoded}"), "wP_u.AMr-");
1219 }
1220
1221 #[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}