use std::fmt;
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
use hyper::{Body, Response};
use ring::digest::{digest, Digest, SHA256};
use ring::signature::{EcdsaKeyPair, KeyPair};
use rustls_pki_types::CertificateDer;
use serde::de::DeserializeOwned;
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Api(#[from] Problem),
#[error("base64 decoding failed: {0}")]
Base64(#[from] base64::DecodeError),
#[error("cryptographic operation failed: {0}")]
Crypto(#[from] ring::error::Unspecified),
#[error("invalid key bytes: {0}")]
CryptoKey(#[from] ring::error::KeyRejected),
#[error("HTTP request failure: {0}")]
Http(#[from] hyper::Error),
#[error("invalid URI: {0}")]
InvalidUri(#[from] hyper::http::uri::InvalidUri),
#[error("failed to (de)serialize JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("missing data: {0}")]
Str(&'static str),
}
impl From<&'static str> for Error {
fn from(s: &'static str) -> Self {
Error::Str(s)
}
}
#[derive(Deserialize, Serialize)]
pub struct AccountCredentials {
pub(crate) id: String,
#[serde(with = "pkcs8_serde")]
pub(crate) key_pkcs8: Vec<u8>,
pub(crate) directory: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) urls: Option<DirectoryUrls>,
}
mod pkcs8_serde {
use std::fmt;
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
use serde::{de, Deserializer, Serializer};
pub(crate) fn serialize<S>(key_pkcs8: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let encoded = BASE64_URL_SAFE_NO_PAD.encode(key_pkcs8.as_ref());
serializer.serialize_str(&encoded)
}
pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<u8>, D::Error> {
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = Vec<u8>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a base64-encoded PKCS#8 private key")
}
fn visit_str<E>(self, v: &str) -> Result<Vec<u8>, E>
where
E: de::Error,
{
BASE64_URL_SAFE_NO_PAD.decode(v).map_err(de::Error::custom)
}
}
deserializer.deserialize_str(Visitor)
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Problem {
pub r#type: String,
pub detail: String,
pub status: u16,
}
impl Problem {
pub(crate) async fn check<T: DeserializeOwned>(rsp: Response<Body>) -> Result<T, Error> {
Ok(serde_json::from_slice(
&hyper::body::to_bytes(Self::from_response(rsp).await?).await?,
)?)
}
pub(crate) async fn from_response(rsp: Response<Body>) -> Result<Body, Error> {
let status = rsp.status();
let body = rsp.into_body();
if status.is_informational() || status.is_success() || status.is_redirection() {
return Ok(body);
}
let body = hyper::body::to_bytes(body).await?;
Err(serde_json::from_slice::<Problem>(&body)?.into())
}
}
impl fmt::Display for Problem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "API error: {} ({})", self.detail, self.r#type)
}
}
impl std::error::Error for Problem {}
#[derive(Debug, Serialize)]
pub(crate) struct FinalizeRequest {
csr: String,
}
impl FinalizeRequest {
pub(crate) fn new(csr_der: &[u8]) -> Self {
Self {
csr: BASE64_URL_SAFE_NO_PAD.encode(csr_der),
}
}
}
#[derive(Debug, Serialize)]
pub(crate) struct Header<'a> {
pub(crate) alg: SigningAlgorithm,
#[serde(flatten)]
pub(crate) key: KeyOrKeyId<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) nonce: Option<&'a str>,
pub(crate) url: &'a str,
}
#[derive(Debug, Serialize)]
pub(crate) enum KeyOrKeyId<'a> {
#[serde(rename = "jwk")]
Key(Jwk),
#[serde(rename = "kid")]
KeyId(&'a str),
}
impl<'a> KeyOrKeyId<'a> {
pub(crate) fn from_key(key: &EcdsaKeyPair) -> KeyOrKeyId<'static> {
KeyOrKeyId::Key(Jwk::new(key))
}
}
#[derive(Debug, Serialize)]
pub(crate) struct Jwk {
alg: SigningAlgorithm,
crv: &'static str,
kty: &'static str,
r#use: &'static str,
x: String,
y: String,
}
impl Jwk {
pub(crate) fn new(key: &EcdsaKeyPair) -> Self {
let (x, y) = key.public_key().as_ref()[1..].split_at(32);
Self {
alg: SigningAlgorithm::Es256,
crv: "P-256",
kty: "EC",
r#use: "sig",
x: BASE64_URL_SAFE_NO_PAD.encode(x),
y: BASE64_URL_SAFE_NO_PAD.encode(y),
}
}
pub(crate) fn thumb_sha256(key: &EcdsaKeyPair) -> Result<Digest, serde_json::Error> {
let jwk = Self::new(key);
Ok(digest(
&SHA256,
&serde_json::to_vec(&JwkThumb {
crv: jwk.crv,
kty: jwk.kty,
x: &jwk.x,
y: &jwk.y,
})?,
))
}
}
#[derive(Debug, Serialize)]
struct JwkThumb<'a> {
crv: &'a str,
kty: &'a str,
x: &'a str,
y: &'a str,
}
#[derive(Debug, Deserialize)]
pub struct Challenge {
pub r#type: ChallengeType,
pub url: String,
pub token: String,
pub status: ChallengeStatus,
pub error: Option<Problem>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderState {
pub status: OrderStatus,
pub authorizations: Vec<String>,
pub error: Option<Problem>,
pub finalize: String,
pub certificate: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NewOrder<'a> {
pub identifiers: &'a [Identifier],
}
#[derive(Debug)]
pub struct RevocationRequest<'a> {
pub certificate: &'a CertificateDer<'a>,
pub reason: Option<RevocationReason>,
}
impl<'a> Serialize for RevocationRequest<'a> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let base64 = BASE64_URL_SAFE_NO_PAD.encode(self.certificate);
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("certificate", &base64)?;
if let Some(reason) = &self.reason {
map.serialize_entry("reason", reason)?;
}
map.end()
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
#[repr(u8)]
pub enum RevocationReason {
Unspecified = 0,
KeyCompromise = 1,
CaCompromise = 2,
AffiliationChanged = 3,
Superseded = 4,
CessationOfOperation = 5,
CertificateHold = 6,
RemoveFromCrl = 8,
PrivilegeWithdrawn = 9,
AaCompromise = 10,
}
impl Serialize for RevocationReason {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u8(self.clone() as u8)
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct NewAccountPayload<'a> {
#[serde(flatten)]
pub(crate) new_account: &'a NewAccount<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) external_account_binding: Option<JoseJson>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NewAccount<'a> {
pub contact: &'a [&'a str],
pub terms_of_service_agreed: bool,
pub only_return_existing: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DirectoryUrls {
pub(crate) new_nonce: String,
pub(crate) new_account: String,
pub(crate) new_order: String,
pub(crate) new_authz: Option<String>,
pub(crate) revoke_cert: Option<String>,
pub(crate) key_change: Option<String>,
}
#[derive(Serialize)]
pub(crate) struct JoseJson {
pub(crate) protected: String,
pub(crate) payload: String,
pub(crate) signature: String,
}
impl JoseJson {
pub(crate) fn new(
payload: Option<&impl Serialize>,
protected: Header<'_>,
signer: &impl Signer,
) -> Result<Self, Error> {
let protected = base64(&protected)?;
let payload = match payload {
Some(data) => base64(&data)?,
None => String::new(),
};
let combined = format!("{protected}.{payload}");
let signature = signer.sign(combined.as_bytes())?;
Ok(Self {
protected,
payload,
signature: BASE64_URL_SAFE_NO_PAD.encode(signature.as_ref()),
})
}
}
pub(crate) trait Signer {
type Signature: AsRef<[u8]>;
fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n>;
fn sign(&self, payload: &[u8]) -> Result<Self::Signature, Error>;
}
fn base64(data: &impl Serialize) -> Result<String, serde_json::Error> {
Ok(BASE64_URL_SAFE_NO_PAD.encode(serde_json::to_vec(data)?))
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Authorization {
pub identifier: Identifier,
pub status: AuthorizationStatus,
pub challenges: Vec<Challenge>,
}
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AuthorizationStatus {
Pending,
Valid,
Invalid,
Revoked,
Expired,
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
pub enum Identifier {
Dns(String),
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
#[allow(missing_docs)]
pub enum ChallengeType {
#[serde(rename = "http-01")]
Http01,
#[serde(rename = "dns-01")]
Dns01,
#[serde(rename = "tls-alpn-01")]
TlsAlpn01,
}
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ChallengeStatus {
Pending,
Processing,
Valid,
Invalid,
}
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum OrderStatus {
Pending,
Ready,
Processing,
Valid,
Invalid,
}
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug)]
pub enum LetsEncrypt {
Production,
Staging,
}
impl LetsEncrypt {
pub const fn url(&self) -> &'static str {
match self {
Self::Production => "https://acme-v02.api.letsencrypt.org/directory",
Self::Staging => "https://acme-staging-v02.api.letsencrypt.org/directory",
}
}
}
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug)]
pub enum ZeroSsl {
Production,
}
impl ZeroSsl {
pub const fn url(&self) -> &'static str {
match self {
Self::Production => "https://acme.zerossl.com/v2/DV90",
}
}
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub(crate) enum SigningAlgorithm {
Es256,
Hs256,
}
#[derive(Debug, Serialize)]
pub(crate) struct Empty {}