1#![warn(unreachable_pub)]
4#![warn(missing_docs)]
5
6pub use ureq;
7
8use std::fmt;
9use std::sync::Arc;
10
11use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
12use ring::digest::{digest, SHA256};
13use ring::rand::SystemRandom;
14use ring::signature::{EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING};
15use ring::{hmac, pkcs8};
16use serde::de::DeserializeOwned;
17use serde::Serialize;
18
19mod types;
20pub use types::{
21 AccountCredentials, Authorization, AuthorizationStatus, Challenge, ChallengeType, Error,
22 Identifier, LetsEncrypt, NewAccount, NewOrder, OrderState, OrderStatus, Problem,
23 RevocationReason, RevocationRequest, ZeroSsl,
24};
25use types::{
26 DirectoryUrls, Empty, FinalizeRequest, Header, JoseJson, Jwk, KeyOrKeyId, NewAccountPayload,
27 Signer, SigningAlgorithm,
28};
29use ureq::Response;
30
31pub struct Order {
39 account: Arc<AccountInner>,
40 nonce: Option<String>,
41 url: String,
42 state: OrderState,
43}
44
45impl Order {
46 pub fn authorizations(&mut self) -> Result<Vec<Authorization>, Error> {
62 let mut authorizations = Vec::with_capacity(self.state.authorizations.len());
63 for url in &self.state.authorizations {
64 authorizations.push(self.account.get(&mut self.nonce, url)?);
65 }
66 Ok(authorizations)
67 }
68
69 pub fn key_authorization(&self, challenge: &Challenge) -> KeyAuthorization {
74 KeyAuthorization::new(challenge, &self.account.key)
75 }
76
77 pub fn finalize(&mut self, csr_der: &[u8]) -> Result<(), Error> {
83 let rsp = self.account.post(
84 Some(&FinalizeRequest::new(csr_der)),
85 self.nonce.take(),
86 &self.state.finalize,
87 )?;
88
89 self.nonce = nonce_from_response(&rsp);
90 self.state = Problem::check::<OrderState>(rsp)?;
91 Ok(())
92 }
93
94 pub fn certificate(&mut self) -> Result<Option<String>, Error> {
102 if matches!(self.state.status, OrderStatus::Processing) {
103 let rsp = self
104 .account
105 .post(None::<&Empty>, self.nonce.take(), &self.url)?;
106 self.nonce = nonce_from_response(&rsp);
107 self.state = Problem::check::<OrderState>(rsp)?;
108 }
109
110 if let Some(error) = &self.state.error {
111 return Err(Error::Api(error.clone()));
112 } else if self.state.status == OrderStatus::Processing {
113 return Ok(None);
114 } else if self.state.status != OrderStatus::Valid {
115 return Err(Error::Str("invalid order state"));
116 }
117
118 let cert_url = match &self.state.certificate {
119 Some(cert_url) => cert_url,
120 None => return Err(Error::Str("no certificate URL found")),
121 };
122
123 let rsp = self
124 .account
125 .post(None::<&Empty>, self.nonce.take(), cert_url)?;
126
127 self.nonce = nonce_from_response(&rsp);
128 let body = Problem::from_response(rsp)?;
129 Ok(Some(
130 String::from_utf8(body.to_vec())
131 .map_err(|_| "unable to decode certificate as UTF-8")?,
132 ))
133 }
134
135 pub fn set_challenge_ready(&mut self, challenge_url: &str) -> Result<(), Error> {
139 let rsp = self
140 .account
141 .post(Some(&Empty {}), self.nonce.take(), challenge_url)?;
142
143 self.nonce = nonce_from_response(&rsp);
144 let _ = Problem::check::<Challenge>(rsp)?;
145 Ok(())
146 }
147
148 pub fn challenge(&mut self, challenge_url: &str) -> Result<Challenge, Error> {
150 self.account.get(&mut self.nonce, challenge_url)
151 }
152
153 pub fn refresh(&mut self) -> Result<&OrderState, Error> {
155 let rsp = self
156 .account
157 .post(None::<&Empty>, self.nonce.take(), &self.url)?;
158
159 self.nonce = nonce_from_response(&rsp);
160 self.state = Problem::check::<OrderState>(rsp)?;
161 Ok(&self.state)
162 }
163
164 pub fn state(&mut self) -> &OrderState {
168 &self.state
169 }
170
171 pub fn url(&self) -> &str {
173 &self.url
174 }
175}
176
177#[derive(Clone)]
186pub struct Account {
187 inner: Arc<AccountInner>,
188}
189
190impl Account {
191 pub fn from_credentials(credentials: AccountCredentials) -> Result<Self, Error> {
195 Ok(Self {
196 inner: Arc::new(AccountInner::from_credentials(credentials)?),
197 })
198 }
199
200 pub fn from_parts(
205 id: String,
206 key_pkcs8_der: &[u8],
207 directory_url: &str,
208 ) -> Result<Self, Error> {
209 Ok(Self {
210 inner: Arc::new(AccountInner {
211 id,
212 key: Key::from_pkcs8_der(key_pkcs8_der)?,
213 client: Client::new(directory_url)?,
214 }),
215 })
216 }
217
218 pub fn create(
223 account: &NewAccount<'_>,
224 server_url: &str,
225 external_account: Option<&ExternalAccountKey>,
226 ) -> Result<(Account, AccountCredentials), Error> {
227 Self::create_inner(
228 account,
229 external_account,
230 Client::new(server_url)?,
231 server_url,
232 )
233 }
234
235 fn create_inner(
236 account: &NewAccount<'_>,
237 external_account: Option<&ExternalAccountKey>,
238 client: Client,
239 server_url: &str,
240 ) -> Result<(Account, AccountCredentials), Error> {
241 let (key, key_pkcs8) = Key::generate()?;
242 let payload = NewAccountPayload {
243 new_account: account,
244 external_account_binding: external_account
245 .map(|eak| {
246 JoseJson::new(
247 Some(&Jwk::new(&key.inner)),
248 eak.header(None, &client.urls.new_account),
249 eak,
250 )
251 })
252 .transpose()?,
253 };
254
255 let rsp = client.post(Some(&payload), None, &key, &client.urls.new_account)?;
256
257 let account_url = rsp.header("LOCATION").map(|s| s.to_owned());
258
259 let _ = Problem::from_response(rsp)?;
261 let id = account_url.ok_or("failed to get account URL")?;
262 let credentials = AccountCredentials {
263 id: id.clone(),
264 key_pkcs8: key_pkcs8.as_ref().to_vec(),
265 directory: Some(server_url.to_owned()),
266 urls: None,
269 };
270
271 let account = AccountInner {
272 client,
273 key,
274 id: id.clone(),
275 };
276
277 Ok((
278 Self {
279 inner: Arc::new(account),
280 },
281 credentials,
282 ))
283 }
284
285 pub fn new_order(&self, order: &NewOrder<'_>) -> Result<Order, Error> {
289 let rsp = self
290 .inner
291 .post(Some(order), None, &self.inner.client.urls.new_order)?;
292
293 let nonce = nonce_from_response(&rsp);
294 let order_url = rsp.header("LOCATION").map(|s| s.to_owned());
295
296 Ok(Order {
297 account: self.inner.clone(),
298 nonce,
299 state: Problem::check::<OrderState>(rsp)?,
303 url: order_url.ok_or("no order URL found")?,
304 })
305 }
306
307 pub fn revoke<'a>(&'a self, payload: &RevocationRequest<'a>) -> Result<(), Error> {
309 let rsp = self
310 .inner
311 .post(Some(payload), None, &self.inner.client.urls.revoke_cert)?;
312 let _ = Problem::from_response(rsp)?;
314 Ok(())
315 }
316}
317
318struct AccountInner {
319 client: Client,
320 key: Key,
321 id: String,
322}
323
324impl AccountInner {
325 fn from_credentials(credentials: AccountCredentials) -> Result<Self, Error> {
326 Ok(Self {
327 id: credentials.id,
328 key: Key::from_pkcs8_der(credentials.key_pkcs8.as_ref())?,
329 client: match (credentials.directory, credentials.urls) {
330 (Some(server_url), _) => Client::new(&server_url)?,
331 (None, Some(urls)) => Client {
332 client: client(),
333 urls,
334 },
335 (None, None) => return Err("no server URLs found".into()),
336 },
337 })
338 }
339
340 fn get<T: DeserializeOwned>(&self, nonce: &mut Option<String>, url: &str) -> Result<T, Error> {
341 let rsp = self.post(None::<&Empty>, nonce.take(), url)?;
342 *nonce = nonce_from_response(&rsp);
343 Problem::check(rsp)
344 }
345
346 fn post(
347 &self,
348 payload: Option<&impl Serialize>,
349 nonce: Option<String>,
350 url: &str,
351 ) -> Result<Response, Error> {
352 self.client.post(payload, nonce, self, url)
353 }
354}
355
356impl Signer for AccountInner {
357 type Signature = <Key as Signer>::Signature;
358
359 fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
360 debug_assert!(nonce.is_some());
361 Header {
362 alg: self.key.signing_algorithm,
363 key: KeyOrKeyId::KeyId(&self.id),
364 nonce,
365 url,
366 }
367 }
368
369 fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
370 self.key.sign(payload)
371 }
372}
373
374struct Client {
375 client: ureq::Agent,
376 urls: DirectoryUrls,
377}
378
379impl Client {
380 fn new(server_url: &str) -> Result<Self, Error> {
381 let client = client();
382 let rsp = client.get(server_url).call()?;
383 let urls = rsp.into_json()?;
384 Ok(Client { client, urls })
385 }
386
387 fn post(
388 &self,
389 payload: Option<&impl Serialize>,
390 nonce: Option<String>,
391 signer: &impl Signer,
392 url: &str,
393 ) -> Result<Response, Error> {
394 let nonce = self.nonce(nonce)?;
395 let body = JoseJson::new(payload, signer.header(Some(&nonce), url), signer)?;
396 let rsp = self
397 .client
398 .request("POST", url)
399 .set("CONTENT-TYPE", JOSE_JSON)
400 .send_json(body)?;
401 Ok(rsp)
402 }
403
404 fn nonce(&self, nonce: Option<String>) -> Result<String, Error> {
405 if let Some(nonce) = nonce {
406 return Ok(nonce);
407 }
408
409 let rsp = self.client.request("HEAD", &self.urls.new_nonce).call()?;
410 if rsp.status() != 200 {
414 return Err("error response from newNonce resource".into());
415 }
416
417 match nonce_from_response(&rsp) {
418 Some(nonce) => Ok(nonce),
419 None => Err("no nonce found in newNonce response".into()),
420 }
421 }
422}
423
424impl fmt::Debug for Client {
425 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426 f.debug_struct("Client")
427 .field("client", &"..")
428 .field("urls", &self.urls)
429 .finish()
430 }
431}
432
433struct Key {
434 rng: SystemRandom,
435 signing_algorithm: SigningAlgorithm,
436 inner: EcdsaKeyPair,
437 thumb: String,
438}
439
440impl Key {
441 fn generate() -> Result<(Self, pkcs8::Document), Error> {
442 let rng = SystemRandom::new();
443 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng)?;
444 let key = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)?;
445 let thumb = BASE64_URL_SAFE_NO_PAD.encode(Jwk::thumb_sha256(&key)?);
446
447 Ok((
448 Self {
449 rng,
450 signing_algorithm: SigningAlgorithm::Es256,
451 inner: key,
452 thumb,
453 },
454 pkcs8,
455 ))
456 }
457
458 fn from_pkcs8_der(pkcs8_der: &[u8]) -> Result<Self, Error> {
459 let rng = SystemRandom::new();
460 let key = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8_der, &rng)?;
461 let thumb = BASE64_URL_SAFE_NO_PAD.encode(Jwk::thumb_sha256(&key)?);
462
463 Ok(Self {
464 rng,
465 signing_algorithm: SigningAlgorithm::Es256,
466 inner: key,
467 thumb,
468 })
469 }
470}
471
472impl Signer for Key {
473 type Signature = ring::signature::Signature;
474
475 fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
476 debug_assert!(nonce.is_some());
477 Header {
478 alg: self.signing_algorithm,
479 key: KeyOrKeyId::from_key(&self.inner),
480 nonce,
481 url,
482 }
483 }
484
485 fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
486 Ok(self.inner.sign(&self.rng, payload)?)
487 }
488}
489
490pub struct KeyAuthorization(String);
496
497impl KeyAuthorization {
498 fn new(challenge: &Challenge, key: &Key) -> Self {
499 Self(format!("{}.{}", challenge.token, &key.thumb))
500 }
501
502 pub fn as_str(&self) -> &str {
506 &self.0
507 }
508
509 pub fn digest(&self) -> impl AsRef<[u8]> {
515 digest(&SHA256, self.0.as_bytes())
516 }
517
518 pub fn dns_value(&self) -> String {
522 BASE64_URL_SAFE_NO_PAD.encode(self.digest())
523 }
524}
525
526impl fmt::Debug for KeyAuthorization {
527 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
528 f.debug_tuple("KeyAuthorization").finish()
529 }
530}
531
532pub struct ExternalAccountKey {
536 id: String,
537 key: hmac::Key,
538}
539
540impl ExternalAccountKey {
541 pub fn new(id: String, key_value: &[u8]) -> Self {
543 Self {
544 id,
545 key: hmac::Key::new(hmac::HMAC_SHA256, key_value),
546 }
547 }
548}
549
550impl Signer for ExternalAccountKey {
551 type Signature = hmac::Tag;
552
553 fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
554 debug_assert_eq!(nonce, None);
555 Header {
556 alg: SigningAlgorithm::Hs256,
557 key: KeyOrKeyId::KeyId(&self.id),
558 nonce,
559 url,
560 }
561 }
562
563 fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
564 Ok(hmac::sign(&self.key, payload))
565 }
566}
567
568fn nonce_from_response(rsp: &Response) -> Option<String> {
569 rsp.header(REPLAY_NONCE).map(ToOwned::to_owned)
570}
571
572fn client() -> ureq::Agent {
573 ureq::builder().https_only(true).build()
574}
575
576const JOSE_JSON: &str = "application/jose+json";
577const REPLAY_NONCE: &str = "Replay-Nonce";
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 #[test]
584 fn deserialize_old_credentials() -> Result<(), Error> {
585 const CREDENTIALS: &str = r#"{"id":"id","key_pkcs8":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJVWC_QzOTCS5vtsJp2IG-UDc8cdDfeoKtxSZxaznM-mhRANCAAQenCPoGgPFTdPJ7VLLKt56RxPlYT1wNXnHc54PEyBg3LxKaH0-sJkX0mL8LyPEdsfL_Oz4TxHkWLJGrXVtNhfH","urls":{"newNonce":"new-nonce","newAccount":"new-acct","newOrder":"new-order", "revokeCert": "revoke-cert"}}"#;
586 Account::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?)?;
587 Ok(())
588 }
589
590 #[test]
591 fn deserialize_new_credentials() -> Result<(), Error> {
592 const CREDENTIALS: &str = r#"{"id":"id","key_pkcs8":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJVWC_QzOTCS5vtsJp2IG-UDc8cdDfeoKtxSZxaznM-mhRANCAAQenCPoGgPFTdPJ7VLLKt56RxPlYT1wNXnHc54PEyBg3LxKaH0-sJkX0mL8LyPEdsfL_Oz4TxHkWLJGrXVtNhfH","directory":"https://acme-staging-v02.api.letsencrypt.org/directory"}"#;
593 Account::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?)?;
594 Ok(())
595 }
596}