Current section

Files

Jump to
site_encrypt lib site_encrypt acme client.ex
Raw

lib/site_encrypt/acme/client.ex

defmodule SiteEncrypt.Acme.Client do
@moduledoc """
ACME related functions for endpoints managed by `SiteEncrypt`.
This module exposes higher-level ACME client-side scenarios, such as new account creation, and
certification. Normally these functions are invoked automatically by `SiteEncrypt` processes.
The functions in this module operate on a running endpoint managed by `SiteEncrypt`. For Phoenix
this means that the endpoint must be started through `SiteEncrypt.Phoenix`.
See also `SiteEncrypt.Acme.Client.API` for details about API sessions and lower level API.
"""
alias SiteEncrypt.Acme.Client.{API, Crypto}
@doc "Creates the new account."
@spec new_account(SiteEncrypt.id(), API.session_opts()) :: API.session()
def new_account(id, session_opts \\ []) do
config = SiteEncrypt.Registry.config(id)
account_key = JOSE.JWK.generate_key({:rsa, config.key_size})
session = start_session(SiteEncrypt.directory_url(config), account_key, session_opts)
{:ok, session} = API.new_account(session, config.emails)
session
end
@doc "Returns `API` session for the existing account."
@spec for_existing_account(SiteEncrypt.id(), JOSE.JWK.t(), API.session_opts()) :: API.session()
def for_existing_account(id, account_key, session_opts) do
config = SiteEncrypt.Registry.config(id)
session = start_session(SiteEncrypt.directory_url(config), account_key, session_opts)
{:ok, session} = API.fetch_kid(session)
session
end
@doc """
Obtains the new certificate.
The obtained certificate will not be applied to the endpoint or stored to disk. If you want to
apply the new certificate to the endpoint, you can pass the returned pems to the function
`SiteEncrypt.set_certificate/2`.
"""
@spec create_certificate(API.session(), SiteEncrypt.id()) :: {SiteEncrypt.pems(), API.session()}
def create_certificate(session, id) do
config = SiteEncrypt.Registry.config(id)
{:ok, order, session} = API.new_order(session, config.domains)
{private_key, order, session} = process_new_order(session, order, config)
{:ok, cert, chain, session} = API.get_cert(session, order)
{%{privkey: Crypto.private_key_to_pem(private_key), cert: cert, chain: chain}, session}
end
defp start_session(directory_url, account_key, session_opts) do
{:ok, session} = API.new_session(directory_url, account_key, session_opts)
session
end
defp process_new_order(session, %{status: :pending} = order, config) do
{pending, session} =
Enum.reduce(
order.authorizations,
{[], session},
fn authorization, {pending_authorizations, session} ->
case authorize(session, config, authorization) do
{:pending, challenge, session} ->
{[{authorization, challenge} | pending_authorizations], session}
{:valid, session} ->
{pending_authorizations, session}
end
end
)
{pending_authorizations, pending_challenges} = Enum.unzip(pending)
SiteEncrypt.Registry.await_challenges(config.id, pending_challenges, :timer.minutes(1))
{:ok, session} = poll(session, config, &validate_authorizations(&1, pending_authorizations))
{order, session} =
poll(session, config, fn session ->
case API.order_status(session, order) do
{:ok, %{status: :ready} = order, session} -> {order, session}
{:ok, _, session} -> {nil, session}
end
end)
process_new_order(session, order, config)
end
defp process_new_order(session, %{status: :ready} = order, config) do
private_key = Crypto.new_private_key(Map.get(config, :key_size, 4096))
csr = Crypto.csr(private_key, config.domains)
{:ok, _finalization, session} = API.finalize(session, order, csr)
{order, session} =
poll(session, config, fn session ->
case API.order_status(session, order) do
{:ok, %{status: :valid} = order, session} -> {order, session}
{:ok, _, session} -> {nil, session}
end
end)
{private_key, order, session}
end
defp authorize(session, config, authorization) do
{:ok, challenges, session} = API.authorization(session, authorization)
http_challenge = Enum.find(challenges, &(&1.type == "http-01"))
false = is_nil(http_challenge)
case http_challenge.status do
:pending ->
key_thumbprint = JOSE.JWK.thumbprint(session.account_key)
SiteEncrypt.Registry.register_challenge(config.id, http_challenge.token, key_thumbprint)
{:ok, _challenge_response, session} = API.challenge(session, http_challenge)
{:pending, http_challenge.token, session}
:valid ->
{:valid, session}
end
end
defp validate_authorizations(session, []), do: {:ok, session}
defp validate_authorizations(session, [authorization | other_authorizations]) do
{:ok, challenges, session} = API.authorization(session, authorization)
if Enum.any?(challenges, &(&1.status == :valid)),
do: validate_authorizations(session, other_authorizations),
else: {nil, session}
end
defp poll(session, config, operation) do
poll(
session,
operation,
60,
if(SiteEncrypt.local_ca?(config), do: 50, else: :timer.seconds(2))
)
end
defp poll(session, _operation, 0, _), do: {:error, session}
defp poll(session, operation, attempt, delay) do
with {nil, session} <- operation.(session) do
Process.sleep(delay)
poll(session, operation, attempt - 1, delay)
end
end
end