instant-acme

repository·main·Indexed 42 days ago

https://github.com/djc/instant-acme

An async, pure-Rust ACME (RFC 8555) client for automated TLS certificate provisioning. Version 0.9.0. It utilizes Tokio for the async runtime and hyper with rustls for HTTP requests, supporting crypto backends such as aws-lc-rs and ring for ECDSA signing.

Tokens
6.1K
Snippets
26
Records
34
Agent score
54%

What's inside instant-acme

  1. Overview of instant-acme

    main
    instant-acme is an asynchronous, pure-Rust implementation of the ACME (RFC 8555) protocol. It is designed for high-performance TLS certificate provisioning and is used in production at Instant Domain Search. The library relies on Tokio for the async runtime, hyper with rustls for HTTP requests, and supports both aws-lc-rs and ring for ECDSA signing.
  2. Parse ACME server problem documents

    main

    When an ACME server returns an error, it follows RFC 7807. The Problem struct captures these details:

    • r#type: An enumerated problem type (e.g., urn:ietf:params:acme:error:invalid-environment).
    • detail: A human-readable explanation.
    • status: The HTTP status code.
    • subproblems: A list of Subproblem objects, which can associate errors with specific identifiers.
  3. Manage ACME accounts with the Account struct

    main

    The Account struct is the primary interface for managing ACME account operations. It is cheap to clone and provides methods for creating orders, revoking certificates, updating account information, and managing account keys.

    To use an Account, you typically start with an AccountBuilder to either create a new account or restore one from existing AccountCredentials.

    // Example conceptual flow
    let builder = Account::builder(http_client, &crypto_provider)?;
    let account = builder.create(&new_account_info, directory_url, None).await?;
    
    // Use the account to create an order
    let order = account.new_order(&new_order_payload).await?;
  4. Certificate Identifier encoding (RFC 9773)

    main
    The CertificateIdentifier is an encoded representation of a certificate used for renewal and replacement. It is constructed from the certificate's Authority Key Identifier (AKI) and its Serial Number. The identifier is serialized as a string (e.g., "authority_key_id.serial").
  5. Format of ACME Problem objects

    main
    ACME error responses are represented by the Problem type. A Problem contains a type (a URI identifying the error), a detail string, and an optional list of subproblems. Subproblems allow the CA to provide granular error details, such as which specific identifier in a request caused a failure.
  6. Configure Cargo features for instant-acme

    main

    You can customize the library's behavior and dependencies using Cargo features.

    Crypto Backends

    • aws-lc-rs (default): Uses the aws-lc-rs crate as the crypto backend.
    • ring: Uses the ring crate as the crypto backend.
    • Note: If both are enabled, aws-lc-rs takes precedence.

    Specialized Features

    • fips: Enables FIPS-compliant mode for the aws-lc-rs backend.
    • x509-parser: Enables extracting CertificateIdentifier values for ACME Renewal Information (ARI) from certificates.
    • time: Enables fetching RenewalInfo for a CertificateIdentifier.

    Default HTTP Stack

    • hyper-rustls (default): Uses a hyper client with rustls.
  7. Understand the BytesResponse structure

    main

    A BytesResponse is an object-safe representation of an HTTP response used by the client. It contains:

    • parts: The http::response::Parts (status code, headers, etc.).
    • body: A boxed BytesBody that can be consumed to retrieve the response payload as Bytes.
    // To consume the body:
    let body_bytes = response.body.into_bytes().await?;
  8. Manage ACME certificate orders with Order

    main

    The Order struct represents a stable identity for an ACME order (RFC 8555). You can create an order from an Account using Account::new_order().

    Key workflows:

    1. Retrieve Authorizations: Use order.authorizations() to get a stream of authorizations required to validate the identifiers in the order.
    2. Complete Challenges: For each authorization, use AuthorizationHandle::challenge(type) to get a ChallengeHandle, then use that handle to complete the specific challenge (e.g., via set_ready() or key_authorization()).
    3. Finalize Order: Once challenges are complete, use order.poll_ready(retry_policy) to wait for the order to become ready, then call order.finalize() (which generates a CSR and finalizes) or order.finalize_csr(der_bytes) if you have a custom CSR.
    4. Retrieve Certificate: After the order is valid, use order.poll_certificate(retry_policy) to wait for and retrieve the PEM encoded certificate chain.
    // Conceptual workflow
    let mut order = account.new_order(identifiers).await?;
    
    // 1. Handle Authorizations/Challenges
    let mut auths = order.authorizations();
    while let Some(Ok(auth_handle)) = auths.next().await {
        let mut challenge = auth_handle.challenge(ChallengeType::Http01)?;
        // ... setup challenge response ...
        challenge.set_ready().await?;
    }
    
    // 2. Wait for readiness and finalize
    let retry_policy = RetryPolicy::new();
    order.poll_ready(&retry_policy).await?;
    let private_key_pem = order.finalize().await?;
    
    // 3. Wait for certificate
    let cert_pem = order.poll_certificate(&retry_policy).await?;
  9. Create or restore an account using AccountBuilder

    main

    Use AccountBuilder to initialize an Account instance. There are several ways to instantiate an account:

    1. Create a new account: Use .create() with NewAccount details and a directory URL.
    2. Restore from credentials: Use .from_credentials() with previously serialized AccountCredentials.
    3. Load from an existing private key: Use .from_key() to load an account associated with a specific key, or .create_from_key() to create a new account if one doesn't exist for that key.
    4. Restore from individual parts: Use .from_parts() if you have the account ID, a PKCS#8 DER private key, and the directory URL separately.
    // Create a new account
    let (account, credentials) = builder
        .create(&new_account, directory_url, None)
        .await?;
    
    // Restore an account from stored credentials
    let account = builder
        .from_credentials(stored_credentials)
        .await?;
    
    // Load an account using a specific private key
    let (account, credentials) = builder
        .from_key(key_tuple, directory_url)
        .await?;
  10. Complete an ACME challenge with ChallengeHandle

    main

    A ChallengeHandle is used to interact with a specific challenge within an authorization.

    To complete a challenge:

    1. Get Key Authorization: Use handle.key_authorization() to generate the required response value. Depending on the challenge type, use .dns_value() (for DNS-01), .as_str() (for HTTP-01), or .digest() (for TLS-ALPN-01).
    2. Setup Infrastructure: Provision the response in your DNS or web server.
    3. Notify Server: Call handle.set_ready().await to tell the ACME server that the challenge is ready for validation.

    For the experimental device-attest-01 challenge, use handle.send_device_attestation(payload) instead.

    // Example: HTTP-01 challenge
    let mut challenge = auth_handle.challenge(ChallengeType::Http01)?;
    let auth_value = challenge.key_authorization()?.as_str();
    
    // [User logic: Place auth_value in .well-known/acme-challenge/...] 
    
    challenge.set_ready().await?;
  11. Understand instant-acme limitations

    main

    When using instant-acme, be aware of the following current limitations:

    • Account Keys: Currently only supports P-256 ECDSA account keys.
    • Device Attestation: Support for device attestation is considered experimental.
  12. Configure the CryptoProvider for ACME operations

    main

    The CryptoProvider struct is the central configuration point for cryptographic operations required by the ACME protocol, such as JWS signing, JWK thumbprints, and EAB HMAC.

    Note that CryptoProvider is distinct from the rustls::crypto::CryptoProvider used for TLS/HTTPS connections.

    You can use built-in backends by enabling the corresponding Cargo features, or implement your own by manually populating the fields:

    • key_provider: A SigningKeyProvider for loading and generating account keys.
    • sha256: A Sha256 implementation for JWK thumbprints and challenge digests.
    • hmac: An HmacKeyProvider for External Account Binding (EAB).

    Built-in backends are available via:

    • CryptoProvider::aws_lc_rs() (requires aws-lc-rs feature)
    • CryptoProvider::ring() (requires ring feature)
    // Using built-in ring backend
    let provider = CryptoProvider::ring();