Add API documentation

This commit is contained in:
Dirkjan Ochtman 2022-05-12 14:17:31 +02:00
parent 5b33409f41
commit b7dd9c4dc0
2 changed files with 137 additions and 5 deletions

View File

@ -1,4 +1,7 @@
//! Async pure-Rust ACME (RFC 8555) client.
#![warn(unreachable_pub)]
#![warn(missing_docs)]
use std::borrow::Cow;
use std::sync::Arc;
@ -14,14 +17,21 @@ use serde::Serialize;
mod types;
pub use types::{
AccountCredentials, Authorization, AuthorizationStatus, ChallengeType, Error, Identifier,
LetsEncrypt, NewAccount, NewOrder, OrderState, OrderStatus,
AccountCredentials, Authorization, AuthorizationStatus, Challenge, ChallengeType, Error,
Identifier, KeyAuthorization, LetsEncrypt, NewAccount, NewOrder, OrderState, OrderStatus,
Problem,
};
use types::{
Challenge, DirectoryUrls, Empty, FinalizeRequest, Header, JoseJson, Jwk, KeyAuthorization,
KeyOrKeyId, Problem, SigningAlgorithm,
DirectoryUrls, Empty, FinalizeRequest, Header, JoseJson, Jwk, KeyOrKeyId, SigningAlgorithm,
};
/// An ACME order as described in RFC 8555 (section 7.1.3)
///
/// An order is created from an [`Account`] by calling [`Account::new_order()`]. The `Order`
/// type represents the stable identity of an order, while the [`Order::state()`] method
/// gives you access to the current state of the order according to the server.
///
/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3>
pub struct Order {
account: Arc<AccountInner>,
nonce: Option<String>,
@ -29,6 +39,21 @@ pub struct Order {
}
impl Order {
/// Retrieve the authorizations for this order
///
/// An order will contain one authorization to complete per identifier in the order.
/// After creating an order, you'll need to retrieve the authorizations so that
/// you can set up a challenge response for each authorization.
///
/// For each authorization, you'll need to:
///
/// * Select which [`ChallengeType`] you want to complete
/// * Create a [`KeyAuthorization`] for that [`Challenge`]
/// * Call [`Order::set_challenge_ready()`] for that challenge
///
/// After the challenges have been set up, check the [`Order::state()`] to see
/// if the order is ready to be finalized (or becomes invalid). Once it is
/// ready, call `Order::finalize()` to get the certificate.
pub async fn authorizations(
&mut self,
authz_urls: &[String],
@ -40,10 +65,19 @@ impl Order {
Ok(authorizations)
}
/// Create a [`KeyAuthorization`] for the given [`Challenge`]
///
/// Signs the challenge's token with the account's private key and use the
/// value from [`KeyAuthorization::as_str()`] as the challenge response.
pub fn key_authorization(&self, challenge: &Challenge) -> KeyAuthorization {
KeyAuthorization(format!("{}.{}", challenge.token, &self.account.key.thumb))
}
/// Request a certificate from the given Certificate Signing Request (CSR)
///
/// Creating a CSR is outside of the scope of instant-acme. Make sure you pass in a
/// DER representation of the CSR in `csr_der` and the [`OrderState::finalize`] URL
/// in `finalize_url`. The resulting `String` will contain the PEM-encoded certificate chain.
pub async fn finalize(&mut self, csr_der: &[u8], finalize_url: &str) -> Result<String, Error> {
let rsp = self
.account
@ -75,6 +109,9 @@ impl Order {
)
}
/// Notify the server that the given challenge is ready to be completed
///
/// `challenge_url` should be the `Challenge::url` field.
pub async fn set_challenge_ready(&mut self, challenge_url: &str) -> Result<(), Error> {
let rsp = self
.account
@ -86,27 +123,41 @@ impl Order {
Ok(())
}
/// Get the current state of the given challenge
pub async fn challenge(&mut self, challenge_url: &str) -> Result<Challenge, Error> {
self.account.get(&mut self.nonce, challenge_url).await
}
/// Get the current state of the order
pub async fn state(&mut self) -> Result<OrderState, Error> {
self.account.get(&mut self.nonce, &self.order_url).await
}
}
/// An ACME account as described in RFC 8555 (section 7.1.2)
///
/// Create an [`Account`] with [`Account::create()`] or restore it from serialized data
/// by passing deserialized [`AccountCredentials`] to [`Account::from_credentials()`].
///
/// The [`Account`] type is cheap to clone.
///
/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.2>
#[derive(Clone)]
pub struct Account {
inner: Arc<AccountInner>,
}
impl Account {
/// Restore an existing account from the given credentials
///
/// The [`AccountCredentials`] type is opaque, but supports deserialization.
pub fn from_credentials(credentials: AccountCredentials<'_>) -> Result<Self, Error> {
Ok(Self {
inner: Arc::new(AccountInner::from_credentials(credentials)?),
})
}
/// Create a new account on the `server_url` with the information in [`NewAccount`]
pub async fn create(account: &NewAccount<'_>, server_url: &str) -> Result<Account, Error> {
let client = Client::new(server_url).await?;
let key = Key::generate()?;
@ -131,6 +182,9 @@ impl Account {
})
}
/// Create a new order based on the given [`NewOrder`]
///
/// Returns both an [`Order`] instance and the initial [`OrderState`].
pub async fn new_order<'a>(
&'a self,
order: &NewOrder<'_>,
@ -158,7 +212,7 @@ impl Account {
))
}
/// Get the account's credentials, which can be serialized.
/// Get the account's credentials, which can be serialized
///
/// Pass the credentials to [`Account::from_credentials`] to regain access to the `Account`.
pub fn credentials(&self) -> AccountCredentials<'_> {

View File

@ -9,22 +9,33 @@ use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Error type for instant-acme
#[derive(Debug, Error)]
pub enum Error {
/// An JSON problem as returned by the ACME server
///
/// RFC 8555 uses problem documents as described in RFC 7807.
#[error("API error: {0}")]
Api(#[from] Problem),
/// Failed to base64-decode data
#[error("base64 decoding failed: {0}")]
Base64(#[from] base64::DecodeError),
/// Failed from cryptographic operations
#[error("cryptographic operation failed: {0}")]
Crypto(#[from] ring::error::Unspecified),
/// Failed to instantiate a private key
#[error("invalid key bytes: {0}")]
CryptoKey(#[from] ring::error::KeyRejected),
/// HTTP request failure
#[error("HTTP request failure: {0}")]
Http(#[from] hyper::Error),
/// Invalid ACME server URL
#[error("invalid URI: {0}")]
InvalidUri(#[from] hyper::http::uri::InvalidUri),
/// Failed to (de)serialize a JSON object
#[error("failed to (de)serialize JSON: {0}")]
Json(#[from] serde_json::Error),
/// Miscellaneous errors
#[error("missing data: {0}")]
Str(&'static str),
}
@ -35,6 +46,12 @@ impl From<&'static str> for Error {
}
}
/// ACME account credentials
///
/// This opaque type contains the account ID, the private key data and the
/// server URLs from the relevant ACME server. This can be used to serialize
/// the account credentials to a file or secret manager and restore the
/// account from persistent storage.
#[derive(Deserialize, Serialize)]
pub struct AccountCredentials<'a> {
pub(crate) id: Cow<'a, str>,
@ -42,11 +59,17 @@ pub struct AccountCredentials<'a> {
pub(crate) urls: Cow<'a, DirectoryUrls>,
}
/// An RFC 7807 problem document as returned by the ACME server
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Problem {
/// One of an enumerated list of problem types
///
/// See <https://datatracker.ietf.org/doc/html/rfc8555#section-6.7>
pub r#type: String,
/// A human-readable explanation of the problem
pub detail: String,
/// The HTTP status code returned for this response
pub status: u16,
}
@ -77,13 +100,21 @@ impl fmt::Display for Problem {
impl std::error::Error for Problem {}
/// The response value to use for challenge responses
///
/// Use [`KeyAuthorization::dns_value()`] for DNS challenges and
/// [`KeyAuthorization::as_str()`] for other challenge types.
///
/// <https://datatracker.ietf.org/doc/html/rfc8555#section-8.1>
pub struct KeyAuthorization(pub(crate) String);
impl KeyAuthorization {
/// Get the key authorization value
pub fn as_str(&self) -> &str {
&self.0
}
/// Get the base64-encoded SHA256 digest of the key authorization
pub fn dns_value(&self) -> String {
base64::encode_config(digest(&SHA256, self.0.as_bytes()), URL_SAFE_NO_PAD)
}
@ -176,36 +207,68 @@ struct JwkThumb<'a> {
y: &'a str,
}
/// An ACME challenge as described in RFC 8555 (section 7.1.5)
///
/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.5>
#[derive(Debug, Deserialize)]
pub struct Challenge {
/// Type of challenge
pub r#type: ChallengeType,
/// Challenge identifier
pub url: String,
/// Token for this challenge
pub token: String,
/// Current status
pub status: ChallengeStatus,
/// Potential error state
pub error: Option<Problem>,
}
/// Contents of an ACME order as described in RFC 8555 (section 7.1.3)
///
/// The order identity will usually be represented by an [Order](crate::Order).
///
/// <https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3>
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderState {
/// Current status
pub status: OrderStatus,
/// Authorization URLs for this order
///
/// There should be one authorization per identifier in the order.
pub authorizations: Vec<String>,
/// Potential error state
pub error: Option<Problem>,
/// A finalization URL, to be used once status becomes `Ready`
pub finalize: String,
/// The certificate URL, which becomes available after finalization
pub certificate: Option<String>,
}
/// Input data for [Order](crate::Order) creation
///
/// To be passed into [Account::new_order()](crate::Account::new_order()).
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NewOrder<'a> {
/// Identifiers to be included in the order
pub identifiers: &'a [Identifier],
}
/// Input data for [Account](crate::Account) creation
///
/// To be passed into [Account::create()](crate::Account::create()).
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NewAccount<'a> {
/// A list of contact URIs (like `mailto:info@example.com`)
pub contact: &'a [&'a str],
/// Whether you agree to the terms of service
pub terms_of_service_agreed: bool,
/// Set to `true` in order to retrieve an existing account
///
/// Setting this to `false` has not been tested.
pub only_return_existing: bool,
}
@ -224,14 +287,20 @@ pub(crate) struct JoseJson {
pub(crate) signature: String,
}
/// An ACME authorization as described in RFC 8555 (section 7.1.4)
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Authorization {
/// The identifier that the account is authorized to represent
pub identifier: Identifier,
/// Current state of the authorization
pub status: AuthorizationStatus,
/// Possible challenges for the authorization
pub challenges: Vec<Challenge>,
}
/// Status for an [`Authorization`]
#[allow(missing_docs)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AuthorizationStatus {
@ -242,13 +311,17 @@ pub enum AuthorizationStatus {
Expired,
}
/// Represent an identifier in an ACME [Order](crate::Order)
#[allow(missing_docs)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
pub enum Identifier {
Dns(String),
}
/// The challenge type
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
#[allow(missing_docs)]
pub enum ChallengeType {
#[serde(rename = "http-01")]
Http01,
@ -267,6 +340,8 @@ pub enum ChallengeStatus {
Invalid,
}
/// Status of an [Order](crate::Order)
#[allow(missing_docs)]
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum OrderStatus {
@ -277,6 +352,8 @@ pub enum OrderStatus {
Invalid,
}
/// Helper type to reference Let's Encrypt server URLs
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug)]
pub enum LetsEncrypt {
Production,
@ -284,6 +361,7 @@ pub enum LetsEncrypt {
}
impl LetsEncrypt {
/// Get the directory URL for the given Let's Encrypt server
pub const fn url(&self) -> &'static str {
match self {
LetsEncrypt::Production => "https://acme-v02.api.letsencrypt.org/directory",