Add API documentation
This commit is contained in:
parent
5b33409f41
commit
b7dd9c4dc0
64
src/lib.rs
64
src/lib.rs
|
@ -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<'_> {
|
||||
|
|
78
src/types.rs
78
src/types.rs
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue