Simple ACME client
Go to file
Nicholas Rempel 485b771ca3 Add certificate provisioning example 2022-12-14 21:25:09 +01:00
.github Bump MSRV to 1.56.1 due to MSRV update in hashbrown 0.12.1 2022-06-23 20:27:24 +02:00
examples Add certificate provisioning example 2022-12-14 21:25:09 +01:00
src Rename to_bytes() to digest() 2022-11-15 10:11:13 +01:00
.gitignore Add .gitignore 2022-05-12 21:47:01 +02:00
Cargo.toml Add certificate provisioning example 2022-12-14 21:25:09 +01:00
LICENSE Add Apache 2.0 license file 2022-05-12 21:47:01 +02:00
README.md Add a hiring notice to the README 2022-05-12 13:47:16 -07:00
deny.toml Add CI configuration 2022-05-12 21:47:01 +02:00

README.md

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

  • Store/recover your account credentials by serializing/deserializing
  • Fully async implementation with tracing support
  • Support for processing multiple orders concurrently
  • Uses hyper with rustls and Tokio for HTTP requests
  • Uses ring for ECDSA signing
  • Minimum supported Rust version: 1.51

Limitations

  • Only tested with DNS challenges against Let's Encrypt so far (staging and production)
  • Only supports ECDSA keys for now

Getting started

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<String>) -> 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(())
}