#![warn(unreachable_pub)]
#![warn(missing_docs)]
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
use hyper::client::connect::Connect;
#[cfg(feature = "hyper-rustls")]
use hyper::client::HttpConnector;
use hyper::header::{CONTENT_TYPE, LOCATION};
use hyper::{Body, Method, Request, Response, StatusCode};
use ring::digest::{digest, SHA256};
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING};
use ring::{hmac, pkcs8};
use serde::de::DeserializeOwned;
use serde::Serialize;
mod types;
pub use types::{
AccountCredentials, Authorization, AuthorizationStatus, Challenge, ChallengeType, Error,
Identifier, LetsEncrypt, NewAccount, NewOrder, OrderState, OrderStatus, Problem,
RevocationReason, RevocationRequest, ZeroSsl,
};
use types::{
DirectoryUrls, Empty, FinalizeRequest, Header, JoseJson, Jwk, KeyOrKeyId, NewAccountPayload,
Signer, SigningAlgorithm,
};
pub struct Order {
account: Arc<AccountInner>,
nonce: Option<String>,
url: String,
state: OrderState,
}
impl Order {
pub async fn authorizations(&mut self) -> Result<Vec<Authorization>, Error> {
let mut authorizations = Vec::with_capacity(self.state.authorizations.len());
for url in &self.state.authorizations {
authorizations.push(self.account.get(&mut self.nonce, url).await?);
}
Ok(authorizations)
}
pub fn key_authorization(&self, challenge: &Challenge) -> KeyAuthorization {
KeyAuthorization::new(challenge, &self.account.key)
}
pub async fn finalize(&mut self, csr_der: &[u8]) -> Result<(), Error> {
let rsp = self
.account
.post(
Some(&FinalizeRequest::new(csr_der)),
self.nonce.take(),
&self.state.finalize,
)
.await?;
self.nonce = nonce_from_response(&rsp);
self.state = Problem::check::<OrderState>(rsp).await?;
Ok(())
}
pub async fn certificate(&mut self) -> Result<Option<String>, Error> {
if matches!(self.state.status, OrderStatus::Processing) {
let rsp = self
.account
.post(None::<&Empty>, self.nonce.take(), &self.url)
.await?;
self.nonce = nonce_from_response(&rsp);
self.state = Problem::check::<OrderState>(rsp).await?;
}
if let Some(error) = &self.state.error {
return Err(Error::Api(error.clone()));
} else if self.state.status == OrderStatus::Processing {
return Ok(None);
} else if self.state.status != OrderStatus::Valid {
return Err(Error::Str("invalid order state"));
}
let cert_url = match &self.state.certificate {
Some(cert_url) => cert_url,
None => return Err(Error::Str("no certificate URL found")),
};
let rsp = self
.account
.post(None::<&Empty>, self.nonce.take(), cert_url)
.await?;
self.nonce = nonce_from_response(&rsp);
let body = hyper::body::to_bytes(Problem::from_response(rsp).await?).await?;
Ok(Some(
String::from_utf8(body.to_vec())
.map_err(|_| "unable to decode certificate as UTF-8")?,
))
}
pub async fn set_challenge_ready(&mut self, challenge_url: &str) -> Result<(), Error> {
let rsp = self
.account
.post(Some(&Empty {}), self.nonce.take(), challenge_url)
.await?;
self.nonce = nonce_from_response(&rsp);
let _ = Problem::check::<Challenge>(rsp).await?;
Ok(())
}
pub async fn challenge(&mut self, challenge_url: &str) -> Result<Challenge, Error> {
self.account.get(&mut self.nonce, challenge_url).await
}
pub async fn refresh(&mut self) -> Result<&OrderState, Error> {
let rsp = self
.account
.post(None::<&Empty>, self.nonce.take(), &self.url)
.await?;
self.nonce = nonce_from_response(&rsp);
self.state = Problem::check::<OrderState>(rsp).await?;
Ok(&self.state)
}
pub fn state(&mut self) -> &OrderState {
&self.state
}
pub fn url(&self) -> &str {
&self.url
}
}
#[derive(Clone)]
pub struct Account {
inner: Arc<AccountInner>,
}
impl Account {
#[cfg(feature = "hyper-rustls")]
pub async fn from_credentials(credentials: AccountCredentials) -> Result<Self, Error> {
Ok(Self {
inner: Arc::new(
AccountInner::from_credentials(credentials, Box::<DefaultClient>::default())
.await?,
),
})
}
pub async fn from_credentials_and_http(
credentials: AccountCredentials,
http: Box<dyn HttpClient>,
) -> Result<Self, Error> {
Ok(Self {
inner: Arc::new(AccountInner::from_credentials(credentials, http).await?),
})
}
pub async fn from_parts(
id: String,
key_pkcs8_der: &[u8],
directory_url: &str,
http: Box<dyn HttpClient>,
) -> Result<Self, Error> {
Ok(Self {
inner: Arc::new(AccountInner {
id,
key: Key::from_pkcs8_der(key_pkcs8_der)?,
client: Client::new(directory_url, http).await?,
}),
})
}
#[cfg(feature = "hyper-rustls")]
pub async fn create(
account: &NewAccount<'_>,
server_url: &str,
external_account: Option<&ExternalAccountKey>,
) -> Result<(Account, AccountCredentials), Error> {
Self::create_inner(
account,
external_account,
Client::new(server_url, Box::<DefaultClient>::default()).await?,
server_url,
)
.await
}
pub async fn create_with_http(
account: &NewAccount<'_>,
server_url: &str,
external_account: Option<&ExternalAccountKey>,
http: Box<dyn HttpClient>,
) -> Result<(Account, AccountCredentials), Error> {
Self::create_inner(
account,
external_account,
Client::new(server_url, http).await?,
server_url,
)
.await
}
async fn create_inner(
account: &NewAccount<'_>,
external_account: Option<&ExternalAccountKey>,
client: Client,
server_url: &str,
) -> Result<(Account, AccountCredentials), Error> {
let (key, key_pkcs8) = Key::generate()?;
let payload = NewAccountPayload {
new_account: account,
external_account_binding: external_account
.map(|eak| {
JoseJson::new(
Some(&Jwk::new(&key.inner)),
eak.header(None, &client.urls.new_account),
eak,
)
})
.transpose()?,
};
let rsp = client
.post(Some(&payload), None, &key, &client.urls.new_account)
.await?;
let account_url = rsp
.headers()
.get(LOCATION)
.and_then(|hv| hv.to_str().ok())
.map(|s| s.to_owned());
let _ = Problem::from_response(rsp).await?;
let id = account_url.ok_or("failed to get account URL")?;
let credentials = AccountCredentials {
id: id.clone(),
key_pkcs8: key_pkcs8.as_ref().to_vec(),
directory: Some(server_url.to_owned()),
urls: None,
};
let account = AccountInner {
client,
key,
id: id.clone(),
};
Ok((
Self {
inner: Arc::new(account),
},
credentials,
))
}
pub async fn new_order<'a>(&'a self, order: &NewOrder<'_>) -> Result<Order, Error> {
let rsp = self
.inner
.post(Some(order), None, &self.inner.client.urls.new_order)
.await?;
let nonce = nonce_from_response(&rsp);
let order_url = rsp
.headers()
.get(LOCATION)
.and_then(|hv| hv.to_str().ok())
.map(|s| s.to_owned());
Ok(Order {
account: self.inner.clone(),
nonce,
state: Problem::check::<OrderState>(rsp).await?,
url: order_url.ok_or("no order URL found")?,
})
}
pub async fn revoke<'a>(&'a self, payload: &RevocationRequest<'a>) -> Result<(), Error> {
let revoke_url = match self.inner.client.urls.revoke_cert.as_deref() {
Some(url) => url,
None => return Err("no revokeCert URL found".into()),
};
let rsp = self.inner.post(Some(payload), None, revoke_url).await?;
let _ = Problem::from_response(rsp).await?;
Ok(())
}
}
struct AccountInner {
client: Client,
key: Key,
id: String,
}
impl AccountInner {
async fn from_credentials(
credentials: AccountCredentials,
http: Box<dyn HttpClient>,
) -> Result<Self, Error> {
Ok(Self {
id: credentials.id,
key: Key::from_pkcs8_der(credentials.key_pkcs8.as_ref())?,
client: match (credentials.directory, credentials.urls) {
(Some(server_url), _) => Client::new(&server_url, http).await?,
(None, Some(urls)) => Client { http, urls },
(None, None) => return Err("no server URLs found".into()),
},
})
}
async fn get<T: DeserializeOwned>(
&self,
nonce: &mut Option<String>,
url: &str,
) -> Result<T, Error> {
let rsp = self.post(None::<&Empty>, nonce.take(), url).await?;
*nonce = nonce_from_response(&rsp);
Problem::check(rsp).await
}
async fn post(
&self,
payload: Option<&impl Serialize>,
nonce: Option<String>,
url: &str,
) -> Result<Response<Body>, Error> {
self.client.post(payload, nonce, self, url).await
}
}
impl Signer for AccountInner {
type Signature = <Key as Signer>::Signature;
fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
debug_assert!(nonce.is_some());
Header {
alg: self.key.signing_algorithm,
key: KeyOrKeyId::KeyId(&self.id),
nonce,
url,
}
}
fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
self.key.sign(payload)
}
}
struct Client {
http: Box<dyn HttpClient>,
urls: DirectoryUrls,
}
impl Client {
async fn new(server_url: &str, http: Box<dyn HttpClient>) -> Result<Self, Error> {
let req = Request::builder()
.uri(server_url)
.body(Body::empty())
.unwrap();
let rsp = http.request(req).await?;
let body = hyper::body::to_bytes(rsp.into_body()).await?;
Ok(Client {
http,
urls: serde_json::from_slice(&body)?,
})
}
async fn post(
&self,
payload: Option<&impl Serialize>,
nonce: Option<String>,
signer: &impl Signer,
url: &str,
) -> Result<Response<Body>, Error> {
let nonce = self.nonce(nonce).await?;
let body = JoseJson::new(payload, signer.header(Some(&nonce), url), signer)?;
let request = Request::builder()
.method(Method::POST)
.uri(url)
.header(CONTENT_TYPE, JOSE_JSON)
.body(Body::from(serde_json::to_vec(&body)?))
.unwrap();
Ok(self.http.request(request).await?)
}
async fn nonce(&self, nonce: Option<String>) -> Result<String, Error> {
if let Some(nonce) = nonce {
return Ok(nonce);
}
let request = Request::builder()
.method(Method::HEAD)
.uri(&self.urls.new_nonce)
.body(Body::empty())
.unwrap();
let rsp = self.http.request(request).await?;
if rsp.status() != StatusCode::OK {
return Err("error response from newNonce resource".into());
}
match nonce_from_response(&rsp) {
Some(nonce) => Ok(nonce),
None => Err("no nonce found in newNonce response".into()),
}
}
}
impl fmt::Debug for Client {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Client")
.field("client", &"..")
.field("urls", &self.urls)
.finish()
}
}
struct Key {
rng: SystemRandom,
signing_algorithm: SigningAlgorithm,
inner: EcdsaKeyPair,
thumb: String,
}
impl Key {
fn generate() -> Result<(Self, pkcs8::Document), Error> {
let rng = SystemRandom::new();
let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng)?;
let key = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)?;
let thumb = BASE64_URL_SAFE_NO_PAD.encode(Jwk::thumb_sha256(&key)?);
Ok((
Self {
rng,
signing_algorithm: SigningAlgorithm::Es256,
inner: key,
thumb,
},
pkcs8,
))
}
fn from_pkcs8_der(pkcs8_der: &[u8]) -> Result<Self, Error> {
let rng = SystemRandom::new();
let key = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8_der, &rng)?;
let thumb = BASE64_URL_SAFE_NO_PAD.encode(Jwk::thumb_sha256(&key)?);
Ok(Self {
rng,
signing_algorithm: SigningAlgorithm::Es256,
inner: key,
thumb,
})
}
}
impl Signer for Key {
type Signature = ring::signature::Signature;
fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
debug_assert!(nonce.is_some());
Header {
alg: self.signing_algorithm,
key: KeyOrKeyId::from_key(&self.inner),
nonce,
url,
}
}
fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
Ok(self.inner.sign(&self.rng, payload)?)
}
}
pub struct KeyAuthorization(String);
impl KeyAuthorization {
fn new(challenge: &Challenge, key: &Key) -> Self {
Self(format!("{}.{}", challenge.token, &key.thumb))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn digest(&self) -> impl AsRef<[u8]> {
digest(&SHA256, self.0.as_bytes())
}
pub fn dns_value(&self) -> String {
BASE64_URL_SAFE_NO_PAD.encode(self.digest())
}
}
impl fmt::Debug for KeyAuthorization {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("KeyAuthorization").finish()
}
}
pub struct ExternalAccountKey {
id: String,
key: hmac::Key,
}
impl ExternalAccountKey {
pub fn new(id: String, key_value: &[u8]) -> Self {
Self {
id,
key: hmac::Key::new(hmac::HMAC_SHA256, key_value),
}
}
}
impl Signer for ExternalAccountKey {
type Signature = hmac::Tag;
fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> {
debug_assert_eq!(nonce, None);
Header {
alg: SigningAlgorithm::Hs256,
key: KeyOrKeyId::KeyId(&self.id),
nonce,
url,
}
}
fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error> {
Ok(hmac::sign(&self.key, payload))
}
}
fn nonce_from_response(rsp: &Response<Body>) -> Option<String> {
rsp.headers()
.get(REPLAY_NONCE)
.and_then(|hv| String::from_utf8(hv.as_ref().to_vec()).ok())
}
#[cfg(feature = "hyper-rustls")]
struct DefaultClient(hyper::Client<hyper_rustls::HttpsConnector<HttpConnector>>);
#[cfg(feature = "hyper-rustls")]
impl HttpClient for DefaultClient {
fn request(
&self,
req: Request<Body>,
) -> Pin<Box<dyn Future<Output = hyper::Result<Response<Body>>> + Send>> {
Box::pin(self.0.request(req))
}
}
#[cfg(feature = "hyper-rustls")]
impl Default for DefaultClient {
fn default() -> Self {
Self(
hyper::Client::builder().build(
hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.https_only()
.enable_http1()
.enable_http2()
.build(),
),
)
}
}
pub trait HttpClient: Send + Sync + 'static {
fn request(
&self,
req: Request<Body>,
) -> Pin<Box<dyn Future<Output = hyper::Result<Response<Body>>> + Send>>;
}
impl<C> HttpClient for hyper::Client<C>
where
C: Connect + Clone + Send + Sync + 'static,
{
fn request(
&self,
req: Request<Body>,
) -> Pin<Box<dyn Future<Output = hyper::Result<Response<Body>>> + Send>> {
Box::pin(<hyper::Client<C>>::request(self, req))
}
}
const JOSE_JSON: &str = "application/jose+json";
const REPLAY_NONCE: &str = "Replay-Nonce";
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn deserialize_old_credentials() -> Result<(), Error> {
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"}}"#;
Account::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?).await?;
Ok(())
}
#[tokio::test]
async fn deserialize_new_credentials() -> Result<(), Error> {
const CREDENTIALS: &str = r#"{"id":"id","key_pkcs8":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJVWC_QzOTCS5vtsJp2IG-UDc8cdDfeoKtxSZxaznM-mhRANCAAQenCPoGgPFTdPJ7VLLKt56RxPlYT1wNXnHc54PEyBg3LxKaH0-sJkX0mL8LyPEdsfL_Oz4TxHkWLJGrXVtNhfH","directory":"https://acme-staging-v02.api.letsencrypt.org/directory"}"#;
Account::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?).await?;
Ok(())
}
}