1use std::fmt;
2
3use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
4use ring::digest::{digest, Digest, SHA256};
5use ring::signature::{EcdsaKeyPair, KeyPair};
6use rustls_pki_types::CertificateDer;
7use serde::de::DeserializeOwned;
8use serde::ser::SerializeMap;
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11use ureq::Response;
12
13#[derive(Debug, Error)]
15pub enum Error {
16 #[error(transparent)]
20 Api(#[from] Problem),
21 #[error("base64 decoding failed: {0}")]
23 Base64(#[from] base64::DecodeError),
24 #[error("cryptographic operation failed: {0}")]
26 Crypto(#[from] ring::error::Unspecified),
27 #[error("invalid key bytes: {0}")]
29 CryptoKey(#[from] ring::error::KeyRejected),
30 #[error("HTTP request failure: {0}")]
32 Http(#[from] Box<ureq::Error>),
33 #[error("HTTP IO failure: {0}")]
35 HttpIo(#[from] std::io::Error),
36 #[error("failed to (de)serialize JSON: {0}")]
38 Json(#[from] serde_json::Error),
39 #[error("missing data: {0}")]
41 Str(&'static str),
42}
43impl From<ureq::Error> for Error {
44 fn from(value: ureq::Error) -> Self {
45 Self::Http(Box::new(value))
46 }
47}
48
49impl From<&'static str> for Error {
50 fn from(s: &'static str) -> Self {
51 Error::Str(s)
52 }
53}
54
55#[derive(Deserialize, Serialize, Clone)]
62pub struct AccountCredentials {
63 pub(crate) id: String,
64 #[serde(with = "pkcs8_serde")]
66 pub(crate) key_pkcs8: Vec<u8>,
67 pub(crate) directory: Option<String>,
68 pub(crate) urls: Option<DirectoryUrls>,
69}
70
71mod pkcs8_serde {
72 use std::fmt;
73
74 use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
75 use serde::{de, Deserializer, Serializer};
76
77 pub(crate) fn serialize<S>(key_pkcs8: &[u8], serializer: S) -> Result<S::Ok, S::Error>
78 where
79 S: Serializer,
80 {
81 let encoded = BASE64_URL_SAFE_NO_PAD.encode(key_pkcs8.as_ref());
82 serializer.serialize_str(&encoded)
83 }
84
85 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
86 deserializer: D,
87 ) -> Result<Vec<u8>, D::Error> {
88 struct Visitor;
89
90 impl<'de> de::Visitor<'de> for Visitor {
91 type Value = Vec<u8>;
92
93 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94 formatter.write_str("a base64-encoded PKCS#8 private key")
95 }
96
97 fn visit_str<E>(self, v: &str) -> Result<Vec<u8>, E>
98 where
99 E: de::Error,
100 {
101 BASE64_URL_SAFE_NO_PAD.decode(v).map_err(de::Error::custom)
102 }
103 }
104
105 deserializer.deserialize_str(Visitor)
106 }
107}
108
109#[derive(Clone, Debug, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct Problem {
113 pub r#type: Option<String>,
117 pub detail: Option<String>,
119 pub status: Option<u16>,
121}
122
123impl Problem {
124 pub(crate) fn check<T: DeserializeOwned>(rsp: Response) -> Result<T, Error> {
125 rsp.into_json().map_err(Error::HttpIo)
126 }
127
128 pub(crate) fn from_response(rsp: Response) -> Result<Vec<u8>, Error> {
129 let status = rsp.status();
130 let mut body = Vec::new();
131 rsp.into_reader()
132 .read_to_end(&mut body)
133 .map_err(Error::HttpIo)?;
134 if (100..=399).contains(&status) {
135 return Ok(body);
136 }
137
138 Err(serde_json::from_slice::<Problem>(&body)?.into())
139 }
140}
141
142impl fmt::Display for Problem {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 f.write_str("API error")?;
145 if let Some(detail) = &self.detail {
146 write!(f, ": {detail}")?;
147 }
148
149 if let Some(r#type) = &self.r#type {
150 write!(f, " ({})", r#type)?;
151 }
152
153 Ok(())
154 }
155}
156
157impl std::error::Error for Problem {}
158
159#[derive(Debug, Serialize)]
160pub(crate) struct FinalizeRequest {
161 csr: String,
162}
163
164impl FinalizeRequest {
165 pub(crate) fn new(csr_der: &[u8]) -> Self {
166 Self {
167 csr: BASE64_URL_SAFE_NO_PAD.encode(csr_der),
168 }
169 }
170}
171
172#[derive(Debug, Serialize)]
173pub(crate) struct Header<'a> {
174 pub(crate) alg: SigningAlgorithm,
175 #[serde(flatten)]
176 pub(crate) key: KeyOrKeyId<'a>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub(crate) nonce: Option<&'a str>,
179 pub(crate) url: &'a str,
180}
181
182#[derive(Debug, Serialize)]
183pub(crate) enum KeyOrKeyId<'a> {
184 #[serde(rename = "jwk")]
185 Key(Jwk),
186 #[serde(rename = "kid")]
187 KeyId(&'a str),
188}
189
190impl<'a> KeyOrKeyId<'a> {
191 pub(crate) fn from_key(key: &EcdsaKeyPair) -> KeyOrKeyId<'static> {
192 KeyOrKeyId::Key(Jwk::new(key))
193 }
194}
195
196#[derive(Debug, Serialize)]
197pub(crate) struct Jwk {
198 alg: SigningAlgorithm,
199 crv: &'static str,
200 kty: &'static str,
201 r#use: &'static str,
202 x: String,
203 y: String,
204}
205
206impl Jwk {
207 pub(crate) fn new(key: &EcdsaKeyPair) -> Self {
208 let (x, y) = key.public_key().as_ref()[1..].split_at(32);
209 Self {
210 alg: SigningAlgorithm::Es256,
211 crv: "P-256",
212 kty: "EC",
213 r#use: "sig",
214 x: BASE64_URL_SAFE_NO_PAD.encode(x),
215 y: BASE64_URL_SAFE_NO_PAD.encode(y),
216 }
217 }
218
219 pub(crate) fn thumb_sha256(key: &EcdsaKeyPair) -> Result<Digest, serde_json::Error> {
220 let jwk = Self::new(key);
221 Ok(digest(
222 &SHA256,
223 &serde_json::to_vec(&JwkThumb {
224 crv: jwk.crv,
225 kty: jwk.kty,
226 x: &jwk.x,
227 y: &jwk.y,
228 })?,
229 ))
230 }
231}
232
233#[derive(Debug, Serialize)]
234struct JwkThumb<'a> {
235 crv: &'a str,
236 kty: &'a str,
237 x: &'a str,
238 y: &'a str,
239}
240
241#[derive(Debug, Deserialize)]
245pub struct Challenge {
246 pub r#type: ChallengeType,
248 pub url: String,
250 pub token: String,
252 pub status: ChallengeStatus,
254 pub error: Option<Problem>,
256}
257
258#[derive(Debug, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct OrderState {
266 pub status: OrderStatus,
268 pub authorizations: Vec<String>,
272 pub error: Option<Problem>,
274 pub finalize: String,
276 pub certificate: Option<String>,
278}
279
280#[derive(Debug, Serialize)]
284#[serde(rename_all = "camelCase")]
285pub struct NewOrder<'a> {
286 pub identifiers: &'a [Identifier],
288}
289
290#[derive(Debug)]
293pub struct RevocationRequest<'a> {
294 pub certificate: &'a CertificateDer<'a>,
296 pub reason: Option<RevocationReason>,
298}
299
300impl<'a> Serialize for RevocationRequest<'a> {
301 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
302 let base64 = BASE64_URL_SAFE_NO_PAD.encode(self.certificate);
303 let mut map = serializer.serialize_map(Some(2))?;
304 map.serialize_entry("certificate", &base64)?;
305 if let Some(reason) = &self.reason {
306 map.serialize_entry("reason", reason)?;
307 }
308 map.end()
309 }
310}
311
312#[allow(missing_docs)]
315#[derive(Debug, Clone)]
316#[repr(u8)]
317pub enum RevocationReason {
318 Unspecified = 0,
319 KeyCompromise = 1,
320 CaCompromise = 2,
321 AffiliationChanged = 3,
322 Superseded = 4,
323 CessationOfOperation = 5,
324 CertificateHold = 6,
325 RemoveFromCrl = 8,
326 PrivilegeWithdrawn = 9,
327 AaCompromise = 10,
328}
329
330impl Serialize for RevocationReason {
331 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
332 serializer.serialize_u8(self.clone() as u8)
333 }
334}
335
336#[derive(Serialize)]
337#[serde(rename_all = "camelCase")]
338pub(crate) struct NewAccountPayload<'a> {
339 #[serde(flatten)]
340 pub(crate) new_account: &'a NewAccount<'a>,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub(crate) external_account_binding: Option<JoseJson>,
343}
344
345#[derive(Debug, Serialize)]
349#[serde(rename_all = "camelCase")]
350pub struct NewAccount<'a> {
351 pub contact: &'a [&'a str],
353 pub terms_of_service_agreed: bool,
355 pub only_return_existing: bool,
359}
360
361#[derive(Debug, Clone, Deserialize, Serialize)]
362#[serde(rename_all = "camelCase")]
363pub(crate) struct DirectoryUrls {
364 pub(crate) new_nonce: String,
365 pub(crate) new_account: String,
366 pub(crate) new_order: String,
367 pub(crate) revoke_cert: String,
368}
369
370#[derive(Serialize)]
371pub(crate) struct JoseJson {
372 pub(crate) protected: String,
373 pub(crate) payload: String,
374 pub(crate) signature: String,
375}
376
377impl JoseJson {
378 pub(crate) fn new(
379 payload: Option<&impl Serialize>,
380 protected: Header<'_>,
381 signer: &impl Signer,
382 ) -> Result<Self, Error> {
383 let protected = base64(&protected)?;
384 let payload = match payload {
385 Some(data) => base64(&data)?,
386 None => String::new(),
387 };
388
389 let combined = format!("{protected}.{payload}");
390 let signature = signer.sign(combined.as_bytes())?;
391 Ok(Self {
392 protected,
393 payload,
394 signature: BASE64_URL_SAFE_NO_PAD.encode(signature.as_ref()),
395 })
396 }
397}
398
399pub(crate) trait Signer {
400 type Signature: AsRef<[u8]>;
401
402 fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n>;
403
404 fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error>;
405}
406
407fn base64(data: &impl Serialize) -> Result<String, serde_json::Error> {
408 Ok(BASE64_URL_SAFE_NO_PAD.encode(serde_json::to_vec(data)?))
409}
410
411#[derive(Debug, Deserialize)]
413#[serde(rename_all = "camelCase")]
414pub struct Authorization {
415 pub identifier: Identifier,
417 pub status: AuthorizationStatus,
419 pub challenges: Vec<Challenge>,
421}
422
423#[allow(missing_docs)]
425#[derive(Clone, Copy, Debug, Deserialize)]
426#[serde(rename_all = "camelCase")]
427pub enum AuthorizationStatus {
428 Pending,
429 Valid,
430 Invalid,
431 Revoked,
432 Expired,
433}
434
435#[allow(missing_docs)]
437#[derive(Clone, Debug, Serialize, Deserialize)]
438#[serde(tag = "type", content = "value", rename_all = "camelCase")]
439pub enum Identifier {
440 Dns(String),
441}
442
443#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
445#[allow(missing_docs)]
446pub enum ChallengeType {
447 #[serde(rename = "http-01")]
448 Http01,
449 #[serde(rename = "dns-01")]
450 Dns01,
451 #[serde(rename = "tls-alpn-01")]
452 TlsAlpn01,
453}
454
455#[derive(Clone, Copy, Debug, Deserialize)]
456#[serde(rename_all = "camelCase")]
457pub enum ChallengeStatus {
458 Pending,
459 Processing,
460 Valid,
461 Invalid,
462}
463
464#[allow(missing_docs)]
466#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
467#[serde(rename_all = "camelCase")]
468pub enum OrderStatus {
469 Pending,
470 Ready,
471 Processing,
472 Valid,
473 Invalid,
474}
475
476#[allow(missing_docs)]
478#[derive(Clone, Copy, Debug)]
479pub enum LetsEncrypt {
480 Production,
481 Staging,
482}
483
484impl LetsEncrypt {
485 pub const fn url(&self) -> &'static str {
487 match self {
488 Self::Production => "https://acme-v02.api.letsencrypt.org/directory",
489 Self::Staging => "https://acme-staging-v02.api.letsencrypt.org/directory",
490 }
491 }
492}
493
494#[allow(missing_docs)]
496#[derive(Clone, Copy, Debug)]
497pub enum ZeroSsl {
498 Production,
499}
500
501impl ZeroSsl {
502 pub const fn url(&self) -> &'static str {
504 match self {
505 Self::Production => "https://acme.zerossl.com/v2/DV90",
506 }
507 }
508}
509
510#[derive(Clone, Copy, Debug, Serialize)]
511#[serde(rename_all = "UPPERCASE")]
512pub(crate) enum SigningAlgorithm {
513 Es256,
515 Hs256,
517}
518
519#[derive(Debug, Serialize)]
520pub(crate) struct Empty {}