#![warn(unreachable_pub)]
#![warn(missing_docs)]
use std::fmt;
use std::sync::Arc;
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
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,
};
use ureq::Response;
pub struct Order {
account: Arc<AccountInner>,
nonce: Option<String>,
url: String,
state: OrderState,
}
impl Order {
pub 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)?);
}
Ok(authorizations)
}
pub fn key_authorization(&self, challenge: &Challenge) -> KeyAuthorization {
KeyAuthorization::new(challenge, &self.account.key)
}
pub 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,
)?;
self.nonce = nonce_from_response(&rsp);
self.state = Problem::check::<OrderState>(rsp)?;
Ok(())
}
pub 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)?;
self.nonce = nonce_from_response(&rsp);
self.state = Problem::check::<OrderState>(rsp)?;
}
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)?;
self.nonce = nonce_from_response(&rsp);
let body = Problem::from_response(rsp)?;
Ok(Some(
String::from_utf8(body.to_vec())
.map_err(|_| "unable to decode certificate as UTF-8")?,
))
}
pub fn set_challenge_ready(&mut self, challenge_url: &str) -> Result<(), Error> {
let rsp = self
.account
.post(Some(&Empty {}), self.nonce.take(), challenge_url)?;
self.nonce = nonce_from_response(&rsp);
let _ = Problem::check::<Challenge>(rsp)?;
Ok(())
}
pub fn challenge(&mut self, challenge_url: &str) -> Result<Challenge, Error> {
self.account.get(&mut self.nonce, challenge_url)
}
pub fn refresh(&mut self) -> Result<&OrderState, Error> {
let rsp = self
.account
.post(None::<&Empty>, self.nonce.take(), &self.url)?;
self.nonce = nonce_from_response(&rsp);
self.state = Problem::check::<OrderState>(rsp)?;
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 {
pub fn from_credentials(credentials: AccountCredentials) -> Result<Self, Error> {
Ok(Self {
inner: Arc::new(AccountInner::from_credentials(credentials)?),
})
}
pub fn from_parts(
id: String,
key_pkcs8_der: &[u8],
directory_url: &str,
) -> Result<Self, Error> {
Ok(Self {
inner: Arc::new(AccountInner {
id,
key: Key::from_pkcs8_der(key_pkcs8_der)?,
client: Client::new(directory_url)?,
}),
})
}
pub 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)?,
server_url,
)
}
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)?;
let account_url = rsp.header("LOCATION").map(|s| s.to_owned());
let _ = Problem::from_response(rsp)?;
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 fn new_order(&self, order: &NewOrder<'_>) -> Result<Order, Error> {
let rsp = self
.inner
.post(Some(order), None, &self.inner.client.urls.new_order)?;
let nonce = nonce_from_response(&rsp);
let order_url = rsp.header("LOCATION").map(|s| s.to_owned());
Ok(Order {
account: self.inner.clone(),
nonce,
state: Problem::check::<OrderState>(rsp)?,
url: order_url.ok_or("no order URL found")?,
})
}
pub fn revoke<'a>(&'a self, payload: &RevocationRequest<'a>) -> Result<(), Error> {
let rsp = self
.inner
.post(Some(payload), None, &self.inner.client.urls.revoke_cert)?;
let _ = Problem::from_response(rsp)?;
Ok(())
}
}
struct AccountInner {
client: Client,
key: Key,
id: String,
}
impl AccountInner {
fn from_credentials(credentials: AccountCredentials) -> 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)?,
(None, Some(urls)) => Client {
client: client(),
urls,
},
(None, None) => return Err("no server URLs found".into()),
},
})
}
fn get<T: DeserializeOwned>(&self, nonce: &mut Option<String>, url: &str) -> Result<T, Error> {
let rsp = self.post(None::<&Empty>, nonce.take(), url)?;
*nonce = nonce_from_response(&rsp);
Problem::check(rsp)
}
fn post(
&self,
payload: Option<&impl Serialize>,
nonce: Option<String>,
url: &str,
) -> Result<Response, Error> {
self.client.post(payload, nonce, self, url)
}
}
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 {
client: ureq::Agent,
urls: DirectoryUrls,
}
impl Client {
fn new(server_url: &str) -> Result<Self, Error> {
let client = client();
let rsp = client.get(server_url).call()?;
let urls = rsp.into_json()?;
Ok(Client { client, urls })
}
fn post(
&self,
payload: Option<&impl Serialize>,
nonce: Option<String>,
signer: &impl Signer,
url: &str,
) -> Result<Response, Error> {
let nonce = self.nonce(nonce)?;
let body = JoseJson::new(payload, signer.header(Some(&nonce), url), signer)?;
let rsp = self
.client
.request("POST", url)
.set("CONTENT-TYPE", JOSE_JSON)
.send_json(body)?;
Ok(rsp)
}
fn nonce(&self, nonce: Option<String>) -> Result<String, Error> {
if let Some(nonce) = nonce {
return Ok(nonce);
}
let rsp = self.client.request("HEAD", &self.urls.new_nonce).call()?;
if rsp.status() != 200 {
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) -> Option<String> {
rsp.header(REPLAY_NONCE).map(ToOwned::to_owned)
}
fn client() -> ureq::Agent {
ureq::builder().https_only(true).build()
}
const JOSE_JSON: &str = "application/jose+json";
const REPLAY_NONCE: &str = "Replay-Nonce";
#[cfg(test)]
mod tests {
use super::*;
#[test]
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)?)?;
Ok(())
}
#[test]
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)?)?;
Ok(())
}
}