instant-acme: async, pure-Rust ACME client

Documentation Crates.io Build status License: Apache 2.0

instant-acme is an async, pure-Rust ACME (RFC 8555) client.

If you think this is interesting, Instant Domains is hiring experienced Rust engineers.

instant-acme is used in production at InstantDomain to help us provision TLS certificates within seconds for our customers. instant-acme relies on Tokio and rustls to implement the RFC 8555 specification.

Features

Limitations

Getting started

```rust use std::time::Duration;

use rcgen::{Certificate, CertificateParams, DistinguishedName}; use tokio::time::sleep; use tracing::{error, info};

use acme::{ Account, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt, NewAccount, NewOrder, OrderStatus, };

async fn provision(names: Vec) -> anyhow::Result<()> { // Create a new account. This will generate a fresh ECDSA key for you. // Alternatively, restore an account from serialized credentials by // using Account::from_credentials().

let account = Account::create(
    &NewAccount {
        contact: &[],
        terms_of_service_agreed: true,
        only_return_existing: false,
    },
    LetsEncrypt::Staging.url(),
)
.await?;

// Create the ACME order based on the given domain names.
// Note that this only needs an `&Account`, so the library will let you
// process multiple orders in parallel for a single account.

let identifiers = names
    .iter()
    .map(|name| Identifier::Dns(name.into()))
    .collect::<Vec<_>>();
let (mut order, state) = account
    .new_order(&NewOrder {
        identifiers: &identifiers,
    })
    .await
    .unwrap();

info!("order state: {:#?}", state);
assert!(matches!(state.status, OrderStatus::Pending));

// Pick the desired challenge type and prepare the response.

let authorizations = order.authorizations(&state.authorizations).await.unwrap();
let mut challenges = Vec::with_capacity(authorizations.len());
for authz in &authorizations {
    match authz.status {
        AuthorizationStatus::Pending => {}
        AuthorizationStatus::Valid => continue,
        _ => todo!(),
    }

    // We'll use the DNS challenges for this example, but you could
    // pick something else to use here.

    let challenge = authz
        .challenges
        .iter()
        .find(|c| c.r#type == ChallengeType::Dns01)
        .ok_or_else(|| anyhow::anyhow!("no dns01 challenge found"))?;

    let Identifier::Dns(identifier) = &authz.identifier;
    // TODO: set up the challenge response here.
    challenges.push((identifier, &challenge.url));
}

// Let the server know we're ready to accept the challenges.

for (_, url) in &challenges {
    order.set_challenge_ready(url).await.unwrap();
}

// Exponentially back off until the order becomes ready or invalid.

let mut tries = 1u8;
let mut delay = Duration::from_millis(250);
let state = loop {
    sleep(delay).await;
    let state = order.state().await.unwrap();
    if let OrderStatus::Ready | OrderStatus::Invalid = state.status {
        info!("order state: {:#?}", state);
        break state;
    }

    delay *= 2;
    tries += 1;
    match tries < 5 {
        true => info!(?state, tries, "order is not ready, waiting {delay:?}"),
        false => {
            error!(?state, tries, "order is not ready");
            return Err(anyhow::anyhow!("order is not ready"));
        }
    }
};

if state.status == OrderStatus::Invalid {
    return Err(anyhow::anyhow!("order is invalid"));
}

let mut names = Vec::with_capacity(challenges.len());
for (identifier, _) in challenges {
    names.push(identifier.to_owned());
}

// If the order is ready, we can provision the certificate.
// Use the rcgen library to create a Certificate Signing Request.

let mut params = CertificateParams::new(names.clone());
params.distinguished_name = DistinguishedName::new();
let cert = Certificate::from_params(params).unwrap();
let csr = cert.serialize_request_der()?;

// Finalize the order and print certificate chain, private key and account credentials.

let cert_chain_pem = order.finalize(&csr, &state.finalize).await.unwrap();
info!("certficate chain:\n\n{}", cert_chain_pem,);
info!("private key:\n\n{}", cert.serialize_private_key_pem());
info!(
    "account credentials:\n\n{}",
    serde_json::to_string_pretty(&account.credentials()).unwrap()
);

Ok(())

} ```