lib/ex_aws/sns/public_key_cache.ex

defmodule ExAws.SNS.PublicKeyCache do
  use GenServer

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, :ok, opts)
  end

  def get(cert_url) do
    case :ets.lookup(__MODULE__, cert_url) do
      [{_cert_url, public_key}] -> {:ok, public_key}
      [] -> GenServer.call(__MODULE__, {:get_public_key, cert_url})
    end
  end

  ## Callbacks

  def init(:ok) do
    ets = :ets.new(__MODULE__, [:named_table, read_concurrency: true])
    {:ok, ets}
  end

  def handle_call({:get_public_key, cert_url}, _from, ets) do
    with {:ok, cert} <- fetch_certificate(cert_url),
         public_key <- get_public_key(cert) do
      :ets.insert(__MODULE__, {cert_url, public_key})
      {:reply, {:ok, public_key}, ets}
    else
      error -> {:reply, error, ets}
    end
  end

  defp fetch_certificate(cert_url) do
    http_client = Application.get_env(:ex_aws, :http_client, ExAws.Request.Hackney)

    with {:ok, %{status_code: 200, body: cert_binary}} <- http_client.request(:get, cert_url) do
      get_pem_entry(:public_key.pem_decode(cert_binary))
    else
      {:ok, %{status_code: status_code}} ->
        {:error,
         "Could not fetch certificate from #{cert_url}, expected http code 200, got: #{status_code}"}

      {:error, %{reason: reason}} ->
        {:error,
         "Unexpected error, could not fetch certificate from #{cert_url}, got #{inspect(reason)}"}
    end
  end

  defp get_pem_entry(pem_entries) do
    case pem_entries do
      [entry] ->
        try do
          {:ok, :public_key.pem_entry_decode(entry)}
        catch
          _kind, error -> {:error, "Unexpected error while decoding pem entry: #{inspect(error)}"}
        end

      entries ->
        {:error, "Invalid PEM entries: #{inspect(entries)}"}
    end
  end

  defp get_public_key(cert) do
    :public_key.der_decode(:RSAPublicKey, cert |> elem(1) |> elem(7) |> elem(2))
  end
end