rustls-acme

repository·main·Indexed 41 days ago

https://github.com/florianuekermann/rustls-acme

A library for automated TLS certificate management and serving using rustls. It enables ACME-based certificate acquisition and renewal, providing tools like AcmeAcceptor for handling TLS-ALPN-01 challenges, AxumAcceptor for Axum integration, and customizable caching via CertCache and AccountCache traits. Version 0.15.4.

Tokens
6.7K
Snippets
23
Records
32
Agent score
52%

What's inside rustls-acme

  1. Implement Account and Certificate Caching

    main

    To avoid exhausting Let's Encrypt API rate limits, production servers must implement both account and certificate caching.

    • DirCache: A provided file-based cache implementation.
    • Cache trait: Allows you to implement custom persistence layers for both certificates and accounts.
    • CertCache and AccountCache traits: The underlying traits for specific caching needs.
    • CompositeCache: A wrapper that combines two different implementors of CertCache and AccountCache into a single Cache.

    Tip: Because error type parameters of the cache carry over to AcmeConfig, use AcmeConfig::cache_with_boxed_err if you want to avoid type specialization based on the specific cache type used.

  2. Use the Cache trait for combined certificate and account storage

    main

    The Cache trait is a convenience trait that combines both CertCache and AccountCache. Any type that implements both traits automatically implements Cache. This is useful when providing a single storage backend (like a filesystem or database) that handles both certificates and account data.

    // Any type implementing both is automatically a Cache
    pub trait Cache: CertCache + AccountCache {}
    impl<T> Cache for T where T: CertCache + AccountCache {}
  3. How the Low-level Rustls API works

    main

    If you need to interact with rustls or futures_rustls directly, rustls-acme exposes:

    1. AcmeState: Manages the underlying certificate lifecycle.
    2. ResolvesServerCertAcme: A resolver that implements the rustls::server::ResolvesServerCert trait, allowing you to integrate ACME certificate management into a standard rustls server setup.
  4. AcmeAccept

    main

    A future that resolves an incoming I/O stream into either a standard TLS handshake or an ACME TLS-ALPN challenge.

    When polled, it checks if the client hello contains the ACME TLS-ALPN challenge. If it does, it handles the validation internally and returns Ok(None). Otherwise, it returns Ok(Some(StartHandshake)) for standard TLS traffic.

  5. Use the High-level API to serve TLS connections

    main

    The high-level API provides a single stream of incoming TLS connections. Polling this stream automatically handles ACME certificate acquisition and renewal, as well as the TLS handshake. When a connection is successfully established, the TLS stream is handed over to the caller.

    By default, rustls-acme uses the TLS-ALPN-01 validation mechanism, which is recommended. If your DNS points to an HTTP proxy that terminates TLS, you may need to use HTTP-01 instead.

    use futures::prelude::*;
    use rustls_acme::{AcmeConfig, caches::DirCache};
    
    #[macro_rules::apply(smol_macros::main!)]
    async fn main() {
        simple_logger::init_with_level(log::Level::Info).unwrap();
    
        let tcp_listener = smol::net::TcpListener::bind("[::]:443").await.unwrap();
    
        let mut tls_incoming = AcmeConfig::new(["example.com"])
            .contact_push("mailto:admin@example.com")
            .cache(DirCache::new("./rustls_acme_cache"))
            .incoming(tcp_listener.incoming(), Vec::new());
    
        while let Some(tls) = tls_incoming.next().await {
            let mut tls = tls.unwrap();
            smol::spawn(async move {
                tls.write_all(HELLO).await.unwrap();
                tls.close().await.unwrap();
            }).detach();
        }
    }
    
    const HELLO: &'static [u8] = br"#HTTP/1.1 200 OK
    Content-Length: 10
    Content-Type: text/plain; charset=utf-8
    
    Hello Tls!"#;
  6. Troubleshoot OrderError and CertParseError

    main

    When the ACME order process fails, it returns an OrderError. Common causes include:

    • Acme(AcmeError): A low-level error returned by the ACME protocol/directory.
    • Rcgen(rcgen::Error): An error during CSR (Certificate Signing Request) generation.
    • BadOrder(Order) / BadAuth(Auth): The order or authorization object received from the server was invalid.
    • TooManyAttemptsAuth(String): The authorization for a specific domain failed too many times.
    • ProcessingTimeout(Order): The order status remained in Processing for too long.

    If certificate parsing fails, check for CertParseError:

    • X509: The certificate failed X509 validation.
    • Pem: The certificate was not in valid PEM format.
    • TooFewPem: The PEM data did not contain the required number of blocks (e.g., missing private key or chain).
    • InvalidPrivateKey: The private key type is unsupported.
  7. Use TestCache for ACME incompatible test environments

    main

    The TestCache is a specialized cache implementation designed for testing. It generates self-signed certificates and dummy account data, making it suitable for environments where a real ACME server is unavailable or undesirable.

    When using TestCache:

    • It acts as a mock CA (Certificate Authority).
    • load_cert generates a new certificate signed by the internal test CA.
    • store_cert, load_account, and store_account are no-ops that log a message but do not persist any data.
    • You can retrieve the CA's PEM representation using the ca_pem() method, which may be useful for configuring clients to trust the test CA.
    # use rustls_acme::{AcmeConfig, caches::{DirCache, TestCache}};
    # let test_environment = true;
    let mut config = AcmeConfig::new(["example.com"])
        .cache(DirCache::new("./cache"));
    
    if test_environment {
        config = config.cache(TestCache::default());
    }
  8. Use `NoCache` to disable certificate and account persistence

    main

    The NoCache struct provides a no-op implementation of both CertCache and AccountCache traits. Use it when you want to disable the persistence of certificates and ACME accounts (e.g., for testing or in-memory only environments).

    When using NoCache:

    • load_cert and load_account will always return Ok(None).
    • store_cert and store_account will always return Ok(()) but will not actually save any data.
    • All operations will log an info message stating that no cache is configured.
    use rustls_acme::caches::NoCache;
    
    // Initialize a no-op cache with Infallible error types
    let no_cache = NoCache::<std::io::Error, std::io::Error>::default();
  9. Create an incoming connection stream with AcmeState

    main

    To handle incoming TLS connections using the certificates managed by AcmeState, use the incoming method. This provides a high-level API for accepting connections and managing the ACME lifecycle automatically.

    If you are using Tokio, use tokio_incoming for a compatible integration.

    If you are using Axum, use axum_acceptor to create an acceptor compatible with Axum's ecosystem.

    // Standard async-io/futures approach
    let incoming = state.incoming(tcp_stream, alpn_protocols);
    
    // Tokio approach
    let incoming = state.tokio_incoming(tokio_tcp_stream, alpn_protocols);
    
    // Axum approach
    let acceptor = state.axum_acceptor(rustls_config);
  10. Wrap Tokio TCP streams with TokioIncomingTcpWrapper

    main

    To wrap a stream of raw Tokio TCP connections into a Stream of tokio_util::compat::Compat<TokioTCP>, use TokioIncomingTcpWrapper. This is useful when you want to bridge raw Tokio TCP streams into a format compatible with futures-based APIs without applying TLS immediately.

    // Assuming 'incoming_tcp' is a Stream of Result<TokioTCP, ETCP>
    let tcp_wrapper = TokioIncomingTcpWrapper::from(incoming_tcp);