Skip to content

jtdowney/ex_acme

Repository files navigation

ExAcme

Hex.pm Docs License Coverage Status Build Status

ExAcme is a lightweight, developer-friendly Elixir library for interacting with RFC 8555-compliant ACME servers like Let's Encrypt. It simplifies the process of managing X.509 (TLS/SSL) certificates by providing a straightforward API for registering accounts, handling domain challenges, and issuing certificates.

Features

  • Designed with developer productivity and Elixir idioms in mind.
  • Easy integration into your projects with minimal configuration.

Missing

Installation

The package can be installed by adding ex_acme to your list of dependencies in mix.exs:

def deps do
  [
    {:ex_acme, "~> 0.5.2"}
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. The docs can be found at https://hexdocs.pm/ex_acme.

Examples

Starting the client

You can add ExAcme to your supervision tree.

children = [
  {ExAcme, name: MyAcme, directory_url: :lets_encrypt_staging}
]

Registering an account

To register a new account with the ACME server, you need to generate an account key, create a registration, and agree to the terms of service.

alias ExAcme.RegistrationBuilder

# Generate a new account key
key = ExAcme.generate_key()

# Create and configure the registration
registration =
  RegistrationBuilder.new_registration()
  |> RegistrationBuilder.contacts(email: "[email protected]")
  |> RegistrationBuilder.agree_to_terms()

# Register the account
case ExAcme.register_account(registration, key, MyAcme) do
  {:ok, account, account_key} ->
    IO.puts("Account registered successfully!")
    IO.inspect(account)

  {:error, reason} ->
    IO.puts("Failed to register account:")
    IO.inspect(reason)
end

From now on you can use the account_key to perform operations on the account. This structure holds the JSON Web Key (JWK) associated with the account and the Key ID (kid) assigned by the ACME server.

Creating an order request

Once you have registered an account, you can create an order for a certificate by specifying the domain(s) you wish to obtain certificates for.

alias ExAcme.OrderBuilder

# Create a new order request
order_request =
  OrderBuilder.new_order()
  |> OrderBuilder.add_dns_identifier(["example.com", "www.example.com"])

# Submit the order
case ExAcme.submit_order(order_request, account_key, MyAcme) do
  {:ok, order} ->
    IO.puts("Order created successfully!")
    IO.inspect(order)

  {:error, reason} ->
    IO.puts("Failed to create order:")
    IO.inspect(reason)
end

Fetching the challenges for the order

After creating an order, you need to complete the necessary challenges to prove ownership of the domain.

alias ExAcme.Challenge

for auth_url <- order.authorizations do
  {:ok, authorization} = ExAcme.fetch_authorization(auth_url, account_key, MyAcme)
  challenge = Challenge.find_by_type(authorization, "dns-01")

  if challenge do
    value = Challenge.key_authorization(challenge.token, account_key)
    # Set up challenge (implementation depends on your setup)
    setup_challenge(authorization.identifier["value"], value)

    # Trigger validation
    {:ok, _validated_challenge} = ExAcme.start_challenge_validation(challenge.url, account_key, MyAcme)

    # Optionally, wait and verify the challenge status
    :timer.sleep(5000)
    {:ok, validated_challenge} = ExAcme.fetch_challenge(challenge.url, account_key, MyAcme)

    if validated_challenge.status == "valid" do
      IO.puts("Challenge for #{authorization.identifier["value"]} validated successfully.")
    else
      IO.puts("Challenge for #{authorization.identifier["value"]} failed.")
    end
  else
    IO.puts("No challenge found for #{authorization.identifier["value"]}.")
  end
end

Finalizing the order with a Certificate Signing Request (CSR)

After all challenges are validated, you can finalize the order by submitting a CSR.

alias ExAcme.Order

# Create a private key for the certificate
private_key = X509.PrivateKey.new_ec(:secp256r1)

# Generate CSR from the order and private key
csr = Order.to_csr(order, private_key)

# Finalize the order by submitting the CSR
case ExAcme.finalize_order(order.finalize_url, csr, account_key, MyAcme) do
  {:ok, finalized_order} ->
    IO.puts("Order finalized successfully!")
    IO.inspect(finalized_order)

  {:error, reason} ->
    IO.puts("Failed to finalize order:")
    IO.inspect(reason)
end

Fetching the certificate

Once the order is finalized and the certificate is issued, you can fetch the certificate from the ACME server.

case ExAcme.fetch_certificates(finalized_order.certificate_url, account_key, MyAcme) do
  {:ok, certificates} ->
    Enum.each(certificates, fn cert ->
      IO.puts("Fetched Certificate:")
      IO.puts(X509.Certificate.to_pem(cert))
    end)

  {:error, reason} ->
    IO.puts("Failed to fetch certificate:")
    IO.inspect(reason)
end

License

This library is licensed under the MIT License.

About

A library for interacting with ACME servers like Let's Encrypt.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published