1use std::fmt;
2
3use crypto::{Hasher, p256::PublicKey, sha2::Sha256};
4use reqwest::Response;
5use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9#[derive(Debug, Error)]
13pub enum Error {
14 #[error(transparent)]
18 Api(#[from] Problem),
19 #[error("base64 decoding failed: {0}")]
21 Base64(#[from] base64::DecodeError),
22 #[error("cryptographic operation failed")]
24 Crypto,
25 #[error("invalid key bytes")]
27 CryptoKey,
28 #[error("HTTP request failure: {0}")]
30 Http(#[from] reqwest::Error),
31 #[error("HTTP IO failure: {0}")]
33 HttpIo(#[from] std::io::Error),
34 #[error("failed to (de)serialize JSON: {0}")]
36 Json(#[from] serde_json::Error),
37 #[error("missing data: {0}")]
39 Str(&'static str),
40}
41
42impl From<&'static str> for Error {
43 fn from(s: &'static str) -> Self {
44 Error::Str(s)
45 }
46}
47
48#[derive(Deserialize, Serialize, Clone)]
55pub struct AccountCredentials {
56 pub(crate) id: String,
57 #[serde(with = "pkcs8_serde")]
59 pub(crate) key_pkcs8: Vec<u8>,
60 pub(crate) directory: Option<String>,
61 pub(crate) urls: Option<DirectoryUrls>,
62}
63
64mod pkcs8_serde {
65 use std::fmt;
66
67 use serde::{Deserializer, Serializer, de};
68
69 pub(crate) fn serialize<S>(key_pkcs8: &[u8], serializer: S) -> Result<S::Ok, S::Error>
70 where
71 S: Serializer,
72 {
73 let encoded = base64::encode(key_pkcs8.as_ref(), base64::Alphabet::UrlNoPadding);
74 serializer.serialize_str(&encoded)
75 }
76
77 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
78 struct Visitor;
79
80 impl<'de> de::Visitor<'de> for Visitor {
81 type Value = Vec<u8>;
82
83 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84 formatter.write_str("a base64-encoded PKCS#8 private key")
85 }
86
87 fn visit_str<E>(self, v: &str) -> Result<Vec<u8>, E>
88 where
89 E: de::Error,
90 {
91 base64::decode(v, base64::Alphabet::UrlNoPadding).map_err(de::Error::custom)
92 }
93 }
94
95 deserializer.deserialize_str(Visitor)
96 }
97}
98
99#[derive(Clone, Debug, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct Problem {
103 pub r#type: Option<String>,
107 pub detail: Option<String>,
109 pub status: Option<u16>,
111}
112
113impl Problem {
114 pub(crate) async fn check<T: DeserializeOwned>(rsp: Response) -> Result<T, Error> {
115 rsp.json().await.map_err(Error::Http)
116 }
117
118 pub(crate) async fn from_response(rsp: Response) -> Result<Vec<u8>, Error> {
119 let status = rsp.status();
120 let body = rsp.bytes().await?;
121
122 if (100..=399).contains(&status.as_u16()) {
123 return Ok(body.to_vec());
124 }
125
126 Err(serde_json::from_slice::<Problem>(&body)?.into())
127 }
128}
129
130impl fmt::Display for Problem {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 f.write_str("API error")?;
133 if let Some(detail) = &self.detail {
134 write!(f, ": {detail}")?;
135 }
136
137 if let Some(r#type) = &self.r#type {
138 write!(f, " ({})", r#type)?;
139 }
140
141 Ok(())
142 }
143}
144
145impl std::error::Error for Problem {}
146
147#[derive(Debug, Serialize)]
148pub(crate) struct FinalizeRequest {
149 csr: String,
150}
151
152impl FinalizeRequest {
153 pub(crate) fn new(csr_der: &[u8]) -> Self {
154 Self {
155 csr: base64::encode(csr_der, base64::Alphabet::UrlNoPadding),
156 }
157 }
158}
159
160#[derive(Debug, Serialize)]
161pub(crate) struct Header<'a> {
162 pub(crate) alg: SigningAlgorithm,
163 #[serde(flatten)]
164 pub(crate) key: KeyOrKeyId<'a>,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub(crate) nonce: Option<&'a str>,
167 pub(crate) url: &'a str,
168}
169
170#[derive(Debug, Serialize)]
171pub(crate) enum KeyOrKeyId<'a> {
172 #[serde(rename = "jwk")]
173 Key(Jwk),
174 #[serde(rename = "kid")]
175 KeyId(&'a str),
176}
177
178impl<'a> KeyOrKeyId<'a> {
179 pub(crate) fn from_key(key: &PublicKey) -> KeyOrKeyId<'static> {
180 KeyOrKeyId::Key(Jwk::new(key))
181 }
182}
183
184#[derive(Debug, Serialize)]
185pub(crate) struct Jwk {
186 alg: SigningAlgorithm,
187 crv: &'static str,
188 kty: &'static str,
189 r#use: &'static str,
190 x: String,
191 y: String,
192}
193
194impl Jwk {
195 pub(crate) fn new(key: &PublicKey) -> Self {
196 let bytes = key.to_bytes();
197 let (x, y) = bytes[1..].split_at(32);
198 Self {
199 alg: SigningAlgorithm::Es256,
200 crv: "P-256",
201 kty: "EC",
202 r#use: "sig",
203 x: base64::encode(x, base64::Alphabet::UrlNoPadding),
204 y: base64::encode(y, base64::Alphabet::UrlNoPadding),
205 }
206 }
207
208 pub(crate) fn thumb_sha256(key: &PublicKey) -> Result<[u8; 32], serde_json::Error> {
209 let jwk = Self::new(key);
210 let hash = Sha256::hash(&serde_json::to_vec(&JwkThumb {
211 crv: jwk.crv,
212 kty: jwk.kty,
213 x: &jwk.x,
214 y: &jwk.y,
215 })?);
216 let mut out = [0u8; 32];
217 out.copy_from_slice(&hash.as_ref()[..32]);
218 Ok(out)
219 }
220}
221
222#[derive(Debug, Serialize)]
223struct JwkThumb<'a> {
224 crv: &'a str,
225 kty: &'a str,
226 x: &'a str,
227 y: &'a str,
228}
229
230#[derive(Debug, Deserialize)]
234pub struct Challenge {
235 pub r#type: ChallengeType,
237 pub url: String,
239 pub token: String,
241 pub status: ChallengeStatus,
243 pub error: Option<Problem>,
245}
246
247#[derive(Debug, Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct OrderState {
255 pub status: OrderStatus,
257 pub authorizations: Vec<String>,
261 pub error: Option<Problem>,
263 pub finalize: String,
265 pub certificate: Option<String>,
267}
268
269#[derive(Debug, Serialize)]
273#[serde(rename_all = "camelCase")]
274pub struct NewOrder<'a> {
275 pub identifiers: &'a [Identifier],
277}
278
279#[allow(missing_docs)]
304#[derive(Debug, Clone)]
305#[repr(u8)]
306pub enum RevocationReason {
307 Unspecified = 0,
308 KeyCompromise = 1,
309 CaCompromise = 2,
310 AffiliationChanged = 3,
311 Superseded = 4,
312 CessationOfOperation = 5,
313 CertificateHold = 6,
314 RemoveFromCrl = 8,
315 PrivilegeWithdrawn = 9,
316 AaCompromise = 10,
317}
318
319impl Serialize for RevocationReason {
320 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
321 serializer.serialize_u8(self.clone() as u8)
322 }
323}
324
325#[derive(Serialize)]
326#[serde(rename_all = "camelCase")]
327pub(crate) struct NewAccountPayload<'a> {
328 #[serde(flatten)]
329 pub(crate) new_account: &'a NewAccount<'a>,
330 #[serde(skip_serializing_if = "Option::is_none")]
331 pub(crate) external_account_binding: Option<JoseJson>,
332}
333
334#[derive(Debug, Serialize)]
338#[serde(rename_all = "camelCase")]
339pub struct NewAccount<'a> {
340 pub contact: &'a [&'a str],
342 pub terms_of_service_agreed: bool,
344 pub only_return_existing: bool,
348}
349
350#[derive(Debug, Clone, Deserialize, Serialize)]
351#[serde(rename_all = "camelCase")]
352pub(crate) struct DirectoryUrls {
353 pub(crate) new_nonce: String,
354 pub(crate) new_account: String,
355 pub(crate) new_order: String,
356 pub(crate) revoke_cert: String,
357}
358
359#[derive(Serialize)]
360pub(crate) struct JoseJson {
361 pub(crate) protected: String,
362 pub(crate) payload: String,
363 pub(crate) signature: String,
364}
365
366impl JoseJson {
367 pub(crate) fn new(
368 payload: Option<&impl Serialize>,
369 protected: Header<'_>,
370 signer: &impl Signer,
371 ) -> Result<Self, Error> {
372 let protected = base64(&protected)?;
373 let payload = match payload {
374 Some(data) => base64(&data)?,
375 None => String::new(),
376 };
377
378 let combined = format!("{protected}.{payload}");
379 let signature = signer.sign(combined.as_bytes())?;
380 Ok(Self {
381 protected,
382 payload,
383 signature: base64::encode(signature.as_ref(), base64::Alphabet::UrlNoPadding),
384 })
385 }
386}
387
388pub(crate) trait Signer {
389 type Signature: AsRef<[u8]>;
390
391 fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n>;
392
393 fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error>;
394}
395
396fn base64(data: &impl Serialize) -> Result<String, serde_json::Error> {
397 let json_data = serde_json::to_vec(data)?;
398 let encoded_json_data = base64::encode(&json_data, base64::Alphabet::UrlNoPadding);
399 return Ok(encoded_json_data);
400}
401
402#[derive(Debug, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct Authorization {
406 pub identifier: Identifier,
408 pub status: AuthorizationStatus,
410 pub challenges: Vec<Challenge>,
412}
413
414#[allow(missing_docs)]
416#[derive(Clone, Copy, Debug, Deserialize)]
417#[serde(rename_all = "camelCase")]
418pub enum AuthorizationStatus {
419 Pending,
420 Valid,
421 Invalid,
422 Revoked,
423 Expired,
424}
425
426#[allow(missing_docs)]
428#[derive(Clone, Debug, Serialize, Deserialize)]
429#[serde(tag = "type", content = "value", rename_all = "camelCase")]
430pub enum Identifier {
431 Dns(String),
432}
433
434#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
436#[allow(missing_docs)]
437pub enum ChallengeType {
438 #[serde(rename = "http-01")]
439 Http01,
440 #[serde(rename = "dns-01")]
441 Dns01,
442 #[serde(rename = "tls-alpn-01")]
443 TlsAlpn01,
444}
445
446#[derive(Clone, Copy, Debug, Deserialize)]
447#[serde(rename_all = "camelCase")]
448pub enum ChallengeStatus {
449 Pending,
450 Processing,
451 Valid,
452 Invalid,
453}
454
455#[allow(missing_docs)]
457#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
458#[serde(rename_all = "camelCase")]
459pub enum OrderStatus {
460 Pending,
461 Ready,
462 Processing,
463 Valid,
464 Invalid,
465}
466
467#[allow(missing_docs)]
469#[derive(Clone, Copy, Debug)]
470pub enum LetsEncrypt {
471 Production,
472 Staging,
473}
474
475impl LetsEncrypt {
476 pub const fn url(&self) -> &'static str {
478 match self {
479 Self::Production => "https://acme-v02.api.letsencrypt.org/directory",
480 Self::Staging => "https://acme-staging-v02.api.letsencrypt.org/directory",
481 }
482 }
483}
484
485#[allow(missing_docs)]
487#[derive(Clone, Copy, Debug)]
488pub enum ZeroSsl {
489 Production,
490}
491
492impl ZeroSsl {
493 pub const fn url(&self) -> &'static str {
495 match self {
496 Self::Production => "https://acme.zerossl.com/v2/DV90",
497 }
498 }
499}
500
501#[derive(Clone, Copy, Debug, Serialize)]
502#[serde(rename_all = "UPPERCASE")]
503pub(crate) enum SigningAlgorithm {
504 Es256,
506 Hs256,
508}
509
510#[derive(Debug, Serialize)]
511pub(crate) struct Empty {}